Querying attributes from Active Directory using ADFS with a 3rd party Identity Provider

I know there are a lot of other blogs explaining how to query Active Directory for attributes. There's even a rule template for it (Send LDAP Attributes as Claims). The only problem with this rule template is that ADFS requires that you authenticate via Active Directory, which in turn issues you a WindowsAccountName (https://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname) claim. What if you need users to authenticate via a 3rd party Claims Provider Trust, but still need to query Active Directory for attributes? I know some may suggest that the 3rd party Identity Provider should issue all the claims (SAML assertions) that your application needs. In some scenarios however, you may not have control over the 3rd party Identity Provider and you may need to connect the dots between Identity Providers. If your 3rd party Identity Provider integrates with Active Directory, then querying for additional attributes is possible. This article solves this scenario using custom claim rules in ADFS.

Before we start off, it's important to have a good understanding of ADFS custom claim rules and their syntax. There is a great TechNet article entitled 'Understanding Claim Rule Language in AD FS 2.0 & Higher' by Joji Oshima from Microsoft, which is a fantastic quick reference guide. I continue to go back to it as needed when I'm writing new custom claim rules and I highly recommend it.

The customer scenario is somewhat complex. A customer utilizes a 3rd party Identity Provider (CA SiteMinder in this example) for both internal and external users. The external users never have Active Directory credentials, but of course the internal users do. The customer requirement was to have internal users with Active Directory accounts authenticate via the 3rd party Identity Provider (CA SiteMinder), but have claims issued that appear to the relying party application as an internal Active Directory user by properly passing UPN & WindowsAccountName. The application checks for the issuance of these two claims and customizes the interface to the user based on whether or not the user is internal or external.

On a regular schedule, the customer synchronizes the SiteMinder directory with Active Directory and has extended the Active Directory schema with a unique custom attribute that we'll call 'siteMinderID'. This ID is a 20 character long integer (number) in this example. When a user is redirected from the application to ADFS and selects the SiteMinder Claims Provider Trust from the Home Realm Discovery page, they are then subsequently redirected to the SiteMinder Claims Provider to be authenticated. Upon a successful authentication, the user is issued a claim that is their NameID (https://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier). This NameID matches the value of the attribute named siteMinderID in Active Directory. All the other claims issued from SiteMinder in this scenario do not map to any other attributes in Active Directory except this siteMinderID. Since it is not possible to know the WindowsAccountName (sAMAccountName in Active Directory) from SiteMinder, we must query Active Directory based on the siteMinderID only.

To start things off, create a new rule using the rule template mentioned above to query Active Directory for UPN & WindowsAccountName. You should map 'User-Principal-Name' to 'UPN', and 'SAM-Account-Name' to 'Windows account name'. After saving this rule, click 'Edit Rule' and then click 'View Rule Language' in the bottom left of the dialog window. This is a great method to use as an example and helps to more clearly understand the ADFS rule language. Here is that example:

 c:[Type == "https://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"]
=> issue(store = "Active Directory", types = ("https://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn", "https://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"), query = ";userPrincipalName,sAMAccountName;{0}", param = c.Value);

As you can see, the condition of this rule (as denoted between the brackets in c:[ ]) is that you already have a WindowsAccountName claim issued by Active Directory (AD AUTHORITY). Since we don't have that available when authenticating via SiteMinder, we need to adjust the condition of the rule. Copy this to your favorite text editor and delete the previous rule. Then create a new rule and select the 'Send Claims Using a Custom Rule' template and paste the rule from your text editor. The first thing we need to do is to modify the condition to check if SiteMinder authentication was used. We do this by checking if a claim was issued for NameID (https://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier), which contains the unique siteMinderID value. This may vary depending on your environment and 3rd party configuration.

The next change we need to make is to adjust the LDAP query. There is a detailed explanation on TechNet showing how to 'Author custom rules for Active Directory and LDAP attribute stores'. This explains the format of the query string we pass to Active Directory. The format of the query is:

 QUERY = "<QUERY_FILTER>;<ATTRIBUTES>;<DOMAIN_NAME>\<USERNAME>"

As explained in the article above, the attribute store executes the LDAP query using the QUERY_FILTER as the query that is targeted at the LDAP server, and it requests the return attributes whose names are available in the ATTRIBUTES string. The WindowsAccountName (DOMAIN\USERNAME) required at the end of the query string identifies and locates the domain controller to connect to for execution of the LDAP query. The value of the USERNAME is ignored, and is only used to locate the domain controller to connect to. If you do not specify the QUERY_FILTER, the default filter of sAMAccountName={0} is used. In this case, you must provide at least one parameter to the query (using the PARAM keyword) to substitute for the sAMAccountName value.

In the example from the rule above, the query filter is not specified, which results in the default query filter of sAMAccountName={0}. The text {0} is the parameter value passed from the condition, and is replaced on the fly during the rule's execution. We want to specify a different query filter which targets the Active Directory attribute of siteMinderID with the value of the NameID claim. We then must specify the attributes we're retrieving (userPrincipalName and samAccountName in this example), and a valid DOMAIN\USERNAME to use to locate a domain controller. Remember, you must pass a valid (active) WindowsAccountName (DOMAIN\USERNAME) at the end of the query string to successfully query a domain controller. In this example, we used the ADFS Service Account, since it should always be an active/enabled account. Here's how we query Active Directory for a user using their siteMinderID attribute:

 c:[Type == "https://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"]
=> issue(store = "Active Directory", types = ("https://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn", "https://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"), query = "siteMinderID={0};userPrincipalName,sAMAccountName;DOMAIN\adfs-svc", param = c.Value);

As illustrated by this custom rule, we change the query filter to pass the siteMinderID from the NameID in the condition as the parameter. We are now able to issue claims for the UPN and WindowsAccountName of the Active Directory account associated with the siteMinderID.

If you've found this helpful please positively rate this article at the top of the page. For questions about implementing this in your environment, please leave a comment below. Thanks!