locked
How to add custom claim in ADFS 4 based on employeeId attribute and OU membership RRS feed

  • Question

  • We have a need to pass on a claim from ADFS 4.0 to a relying party based on the combination of Active Directory employee Id attribute and OU membership. I guess the best practice could be to use security group membership, but in our case groups are just not set up exactly right, hence this need.

    For instance, if a person with employee Id VX224400 (employeeId AD attribute is set to VX224400) is present in OU=SAXTechs,DC=london,DC=fabrikam,DC=com OU   then the claim "LondonSAXTechs" should be added to the list of role claims being passed to RP.

    In other words, the following should be in the list of claims on RP side:

    http://schemas.microsoft.com/ws/2008/06/identity/claims/role | LondonSAXTechs

    Not exactly sure how to do this using the claims rule language. Any help appreciated.



    Sunday, February 2, 2020 9:39 PM

All replies

  • OU=X,DC=Y,DC=fabrikam,DC=com

    So you want the claim to be issued with the value YX, right?

    Are they other OUs under X? Or is X the deepest level? Are the list of X and Y defined and static? Or are they evolving.

    Also why bring up the employeeId if you don't take it into consideration in building the role claim? Or do you mean you want to issue the role claim only if employeeId exist and regardless of its value?


    Note: Posts are provided “AS IS” without warranty of any kind, either expressed or implied, including but not limited to the implied warranties of merchantability and/or fitness for a particular purpose.

    Monday, February 3, 2020 5:09 PM
  • X and Y are defined and static. For now let's assume X is deepest level. 

    EmployeeId check is required. Presence in OU should be checked by employee Id, not their samAccountName or UPN. Because depending on the app or the user's role, they may sign in with their employee Id, samaccountname or UPN. The only common identification among them is their employee Id. So when a user signs in - claim rule should check if this employee Id exists in...

    • OU=SAXTechs,DC=london,DC=fabrikam,DC=com then SAXTechs role claim is added
    • OU=PrimaTechs,DC=london,DC=fabrikam,DC=com then PrimaTechs role claim is added
    Monday, February 3, 2020 7:01 PM
  • Alright then, one way to do it:

    Rule 1, extract EmployeeID and canonicalname (easier to parse than DN):

    c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"]
     => add(store = "Active Directory", types = ("temp:/claim/employeeid", "temp:/claim/cn"), query = ";employeeID,canonicalname;{0}", param = c.Value);
    

    Rule 2, issue the claim for the app parsing the OU only if the user has an employeeId:

    c1:[Type == "temp:/claim/employeeid"]
     && c2:[Type == "temp:/claim/cn"]
     => issue(Type = "temp:/claim/app", Value = RegExReplace(c2.Value, "(?<domain>[^,$]*)\/(?<topou>[^,$]*)\/(?<user>[^,$]*)", "${topou}"));

    Of course you can change the claim types...


    Note: Posts are provided “AS IS” without warranty of any kind, either expressed or implied, including but not limited to the implied warranties of merchantability and/or fitness for a particular purpose.

    Wednesday, February 5, 2020 1:38 AM
  • In the Rule 2, I guess I need to replace <domain> and <topou> with their values? What do I need to replace <user> with?
    Sunday, February 9, 2020 3:34 AM
  • Nope. Those are regexp tokens. You use them as-is. They work without having to hardcode anything. 

    Note: Posts are provided “AS IS” without warranty of any kind, either expressed or implied, including but not limited to the implied warranties of merchantability and/or fitness for a particular purpose.


    Sunday, February 9, 2020 1:45 PM
  • Sorry if my question is not clear. Here is the requirement:

     if this employee Id exists in...

    • OU=SAXTechs,DC=london,DC=fabrikam,DC=com then SAXTechs role claim is added

    How will the role claim be issued if no OU is hardcoded? No claim value is hard coded? 

    Sunday, February 9, 2020 4:12 PM
  • Yes.

    That is what the rule does.


    c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"]
     => add(store = "Active Directory", types = ("temp:/claim/employeeid", "temp:/claim/cn"), query = ";employeeID,canonicalname;{0}", param = c.Value);


    This extracts the employeeId and the canonicalname of the current user. If the user is bob in the OU SAXTEchs, the canonicalname (cn) in your case will be london.fabrikam.com/SAXTechs/Bob.


    Then:
    c1:[Type == "temp:/claim/employeeid"]
     && c2:[Type == "temp:/claim/cn"]
     => issue(Type = "temp:/claim/app", Value = RegExReplace(c2.Value, "(?<domain>[^,$]*)\/(?<topou>[^,$]*)\/(?<user>[^,$]*)", "${topou}"));

    This rules will issue a claim called temp:/claim/app only if C1 and C2 exist. C2 always exists, all objects have CN. But C1 will exist in the pipeline only if the the user has a value in AD for the attribute employeeId. Then the value of temp:/claim/app will be the result of the regular expression replace operation that follows:
    c2.value = london.fabrikam.com/SAXTechs/Bob > it is the first argument of the RegExpReplace operation. The string that we want to modify.


    (?<domain>[^,$]*)\/(?<topou>[^,$]*)\/(?<user>[^,$]*) > it is the regular expression corresponding on a struture composed of one top OU and a user under it (like I asked in my previous questions). You can use this site if you are not famillar with reg exp and token: https://regex101.com/. At this end of the evaluation, it will create three variables that you can call later in the operation. One variable will be called "domain", it will contain the part of the string before the first /. Then a variable called "topou" that will contain the string between the domain part of the CN and the next object. And then the "user" variable contaning the user part of the CN. For my example: domain = london.fabrikam.com, topou = SAXTechs and user = Bob.


    ${topou} = SAXTechs > it is what we want to replace c2.value by. If you use ${domain} the actual output string of the operation will be london.fabrikam.com.


    So using regular expressions you can create a string replace operation which has no hardceded dependencies with the actual names. So if another user in the forum want to use the same logic, and its domain is lala.com and the top OU Accounts, the exact same rule will return "Accounts".


    Give it a try.


    Note: Posts are provided “AS IS” without warranty of any kind, either expressed or implied, including but not limited to the implied warranties of merchantability and/or fitness for a particular purpose.


    Sunday, February 9, 2020 10:32 PM
  • Just added both rules.

    Scenario: Two AD accounts exist for Bob Smith. One under SAXTechs and another under PrimaTechs. Both accounts have same value for employeeId

    When Bob logs in, on relying party side I'm seeing this value for new claim: SAXTechs, Bob, but I was hoping to see SAXTechs, PrimaTechs

    Looks like we're almost there!

    Monday, February 10, 2020 12:28 AM
  • Update - my apologies, the OU structure is slightly different that what I specified above. Here is what the structure actually looks like (additional level bold font):

    So when a user signs in - claim rule should check if this employee Id exists in...

    • OU=SAXTechs,OU=TypeATechs,DC=london,DC=fabrikam,DC=com then SAXTechs role claim is added
    • OU=PrimaTechs,OU=TypeBTechs,DC=london,DC=fabrikam,DC=com then PrimaTechs role claim is added

    Will the regular expression change due to this?

    Tuesday, February 11, 2020 6:08 PM
  • This one should do the trick:

    (?<domain>[^,$]*)\/(?<topou>[^,$]*)\/(?<subou>[^,$]*)\/(?<user>[^,$]*)

    And you return

    ?{subou}
    


    Note: Posts are provided “AS IS” without warranty of any kind, either expressed or implied, including but not limited to the implied warranties of merchantability and/or fitness for a particular purpose.

    Tuesday, February 11, 2020 7:35 PM
  • In the return does it need to be ${subou} instead of ?{subou}
    Tuesday, February 11, 2020 7:58 PM
  • Still getting SAXTechs, Bob as the claim value, after adding subou to the rule. I know it's updated because for a short while I got an incorrect claim value due to a typo
    Tuesday, February 11, 2020 8:34 PM
  • Can you send us a screen shot of your rules?

    Note: Posts are provided “AS IS” without warranty of any kind, either expressed or implied, including but not limited to the implied warranties of merchantability and/or fitness for a particular purpose.

    Wednesday, February 12, 2020 4:23 AM
  • Screenshot of claim rule in ADFS
    Wednesday, February 12, 2020 12:28 PM
  • I've tried on regex101.com and it gives same result.

    Also not sure how the rule is processing multiple sub OUs where Bob lives.

    In SAXTechs sub OU Bob's givenName is Bob. In PrimaTechs sub OU, I changed his givenName to Bob1.

    The claim value on relying party is SAXTechs, Bob

    Which means the PrimaTechs sub OU is not being processed? And givenName from SAXTechs is being added.



    Thursday, February 13, 2020 3:47 PM
  • I am missing something here. Do you mind sharing the actual CN? and maybe just replace all letters with Xes?

    Because this works:


    Note: Posts are provided “AS IS” without warranty of any kind, either expressed or implied, including but not limited to the implied warranties of merchantability and/or fitness for a particular purpose.

    Thursday, February 13, 2020 7:42 PM
  • The screenshot you added is exactly what I get on regex101.com

    Here is the canonical name using "get-aduser -ldapfilter "(employeeid=VX224400)" -pr canonicalname"

    london.fabricam.com/TypeATechs/SAXTechs/Smith, Bob

    london.fabricam.com/TypeBTechs/PrimaTechs/VX224400

    Thursday, February 13, 2020 8:46 PM
  • But there are more sub OUs?

    I am really confused here. If your user really has the cn: london.fabricam.com/TypeATechs/SAXTechs/Smith, Bob then it works.

    What is the first rule? Can you copy/paste the claim rule too?


    Note: Posts are provided “AS IS” without warranty of any kind, either expressed or implied, including but not limited to the implied warranties of merchantability and/or fitness for a particular purpose.

    Thursday, February 13, 2020 8:51 PM
  • Sorry it's confusing you. Maybe you are focused on getting the regular expression to work? It works perfect. I agree.

    But my question is about a receiving the correct role claims on relying party side.

    The user exists in two OUs.

    Just like

    get-aduser -ldapfilter "(employeeid=123)"

    returns two results, I need ADFS to return two claims for this user. But right now I'm getting only one claim.

    Thursday, February 13, 2020 9:43 PM
  • Oooohhhhhhhhhhhhhhhh got it :) The connected user exist only once. But once connected you want ADFS to look for the employeeID and return the OU of the connected user and the OU of the user with the same employeeId. Correct?

    Do you have more than one domain in your AD forest?


    Note: Posts are provided “AS IS” without warranty of any kind, either expressed or implied, including but not limited to the implied warranties of merchantability and/or fitness for a particular purpose.

    Thursday, February 13, 2020 9:47 PM
  • Only one domain.

    And yes, rule finds all OUs using connected user's employee ID, and returns one role for each OU. I need to use User.IsInRole or the AuthorizeAttribute, on relying party side, .



    Thursday, February 13, 2020 11:29 PM
  • Ok, then we are getting closer.

    Rule 1

    c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"]
     => add(store = "Active Directory", types = ("temp:/claim/employeeId"), query = ";employeeID;{0}", param = c.Value);
    


    Rule 2

    c1:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"] && c2:[Type == "temp:/claim/employeeId"]
     => add(store = "Active Directory", types = ("temp:/claim/role"), query = "(employeeId={1});canonicalName;{0}", param = c1.Value, param = c2.Value);


    Rule 3

    c:[Type == "temp:/claim/role"]
     => issue(Type = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", Value = RegExReplace(c.Value, "(?<domain>[^,$]*)\/(?<topou>[^,$]*)\/(?<user>[^,$]*)", "${topou}"));
    
    

    Now, some comments. This is a very odd design that you have here. Why are user objects created in different places if they are the same user. Why not using groups for that? Adding user to Groupe1 give it the role Role1.

    Note that employeeId is not indexed in AD. So if you AD is large (lot of objects), the LDAP filter (employeeId=X) will take awhile. And could be optimized.


    Note: Posts are provided “AS IS” without warranty of any kind, either expressed or implied, including but not limited to the implied warranties of merchantability and/or fitness for a particular purpose.

    Friday, February 14, 2020 2:51 PM

  • Thanks so much for this. I agree this is not standard design or best practice. ERP is the source of all data. The business roles (SAXTechs, PrimaTechs etc.) reside in ERP. When Active Directory is populated using ERP data, scripting needs to be perfect. And if it's not, water gets muddied. Hence these workarounds. I really appreciate your assistance.

    Based on your feedback we've started populating business roles from ERP into an AD attribute - extensionAttribute5. The value of this attribute may look like
     - SAXTechs
     - PrimaTechs
     - SAXTechs,PrimaTechs

    I wish I could understand the claim rules language. Now I need the role claims to be based on employeeId and extensionAttribute5 (instead of employeeId and canonicalName).

    If a person with employee Id VX224400 (employeeId AD attribute is set to VX224400) has SAXTechs,PrimaTechs value in extensionAttribute5, then two claims should be issued like this:
     - Claim 1: Type: http://schemas.microsoft.com/ws/2008/06/identity/claims/role, Value: SAXTechs
     - Claim 2: Type: http://schemas.microsoft.com/ws/2008/06/identity/claims/role, Value: PrimaTechs




    Thursday, February 20, 2020 6:20 PM
  • On a second thought, employeeId attribute will not be required.

    If a person has SAXTechs,PrimaTechs value in extensionAttribute5, then two claims should be issued like this:
     - Claim 1: Type: http://schemas.microsoft.com/ws/2008/06/identity/claims/role, Value: SAXTechs
     - Claim 2: Type: http://schemas.microsoft.com/ws/2008/06/identity/claims/role, Value: PrimaTechs

    Friday, February 21, 2020 12:03 AM
  • So, now you need the extensionAttributes5 sent as Role and if there are multi-values, then send as many Role claims?

    Note: Posts are provided “AS IS” without warranty of any kind, either expressed or implied, including but not limited to the implied warranties of merchantability and/or fitness for a particular purpose.

    Friday, February 21, 2020 2:45 PM
  • Exactly. Sorry for the confusion. Ideally it should be security groups like you suggested but not sure when we will get there.
    Friday, February 21, 2020 5:02 PM
  • It's alright, we are getting there :)

    I suggest you post your question on the new platform. And post it with the new requirements. It will be easier for the community to find it too.

    https://docs.microsoft.com/answers/topics/adfs.html?WT.mc_id=msdnredirect-web-msdn


    Note: Posts are provided “AS IS” without warranty of any kind, either expressed or implied, including but not limited to the implied warranties of merchantability and/or fitness for a particular purpose.

    Monday, February 24, 2020 9:01 PM