Understanding Claim Rule Language in AD FS 2.0 & Higher

Understanding Claim Rule Language in AD FS 2.0 & Higher




Introduction


Claims Rules follow a basic pipeline.  The rules define which claims are accepted, processed, and eventually sent to the relying party.  Claim Rules are defined as a property of the Claims Provider Trust (incoming claims) and the Relying Party Trust (outgoing claims).  Basic claim passing and transformations can be handled using the built in Claim Rule Templates.

 

Understanding Claim Sets

It is important to understand claim sets as part of the claims pipeline.  When claims come in, they are a part of the incoming claim set.  After claims are processed by claim rules, they become part of the outgoing claim set.  An important piece to understand is there is an incoming and outgoing claim set for the Claims Provider Trust and for the Relying Party Trust, so there are two places claims can be processed before leaving AD FS.

  1. Claims come into the Claims Provider Trust as the incoming claim set
  2. Claims are processed using claim rules and become part of the outgoing claim set
  3. The outgoing claim set is passed to the Relying Party Trust as the incoming claim set for the Relying Party Trust
  4. The set of claims are processed using claim rules and become part of the final outgoing claim set

Read more about the claims pipeline here.


General Syntax of the Claim Rule Language


There are two parts to each rule.
  • Condition statement
  • Issuance statement

If the condition statement is true, the issuance statement will be executed.  If the condition statement is false, the engine will move on to the next rule.

Example: Simple Claims Rule Syntax

 c:[Type == "http://contoso.com/department"]
=>issue(Type = “http://adatum.com/department”, Value = c.Value);
This example takes an incoming claim http://contoso.com/department and issues a new claim http://adatum.com/department with the same value as the incoming claim.  These claim types are URIs in the HTTP format but can also be in the URN format.  URIs are not URLs and do not need to be actual pages on the Internet or intranet.

 

Condition Statements

Condition statements look at all incoming claims and determine if there is one that matches the condition. 
The following properties can be queried in an incoming claim:

  • Type
  • Value
  • Issuer
  • OriginalIssuer
  • ValueType

The format for querying an incoming claim is c:[query] where the variable c represents a claim in the incoming claim set.  The query can be more specific and check for more than one property.  See some of the examples below to get an idea of how the format works.  The two examples below are not complete syntax, as they are missing the issuance statement.

Example: Check for an incoming claim type http://contoso.com/department

 c:[type == "http://contoso.com/department"]

Example: Check for an incoming claim type http://contoso.com/department with a value of sales
 c:[type == "http://contoso.com/department", value == "sales"]


Condition statements are optional in the claims rule language.  By leaving the condition statement blank, the claim rule will always evaluate as true.


Example: Issue a claim http://contoso.com/partner with the value of adatum to all incoming claim sets
 =>issue(Type = “http://contoso.com/partner”, Value = "adatum");

 

Issuance Statements

There are two types of issuance statements to use.

  • Add - adds the claim to the incoming claim set
  • Issue - adds the claim to the outgoing claim set

The ADD issuance statement is used to add additional claims to the incoming claim set so that subsequent claim rules can use them for processing.  The ISSUE issuance statement is used to add claims to the outgoing claim.

Example: Issue a claim http://contoso.com/department to the outgoing claim set

=> issue(type = "http://contoso.com/department", value = "marketing");

Example: Add a claim http://contoso.com/partner to the incoming claim set
=> add(type = "http://contoso.com/partner", value = "adatum");

Example: Check for an incoming claim type http://contoso.com/email and if found, issue a claim http://contoso.com/role with the value of Exchange User
c:[type == "http://contoso.com/emailaddress"]
=> issue(type = "http://contoso.com/role", value = "Exchange User");

The entire incoming claim can be passed on or certain values inside the claim can be used in the outgoing claim.  Use the variable c in the issuance statement to pass the entire claim or parts of the claim.

Example: Check for an incoming claim type http://contoso.com/role and if found, issue the exact same claim to the outgoing claim set
c:[type == "http://contoso.com/role"]
=> issue(claim = c);

Example: Check for an incoming claim type http://contoso.com/role and if found, issue a claim http://adatum.com/role with the same value of the incoming claim
c:[type == "http://contoso.com/role"]
=> issue(type = "http://adatum.com/role", value = c.Value);

 

Multiple Conditions

Another possibility is to have multiple conditions, and if all conditions evaluate to true, run the issuance statement.  Each condition is joined using the && special operator.  There is not a logical OR operator.  To accomplish an OR, create separate claim rules.

Example: Check for an incoming claim type http://contoso.com/role with a value of Editor and separate incoming claim type  http://contoso.com/role with a value of Manager. If both are found, issue a claim http://contoso.com/role with the value of Managing Editor

c1:[type == "http://contoso.com/role", value=="Editor"] &&
c2:[type == "http://contoso.com/role", value=="Manager"]
=> issue(type = "http://contoso.com/role", value = "Managing Editor");


 

Combining Values

The values of each individual incoming claim can be accessed and joined using the special operator + in the issuance statement.

Example: Check for an incoming claim type http://contoso.com/location and separate incoming claim type http://contoso.com/role. If both are found, issue a claim http://contoso.com/targetedrole combining the values of the incoming roles

c1:[type == "http://contoso.com/location"] &&
c2:[type == "http://contoso.com/role"]
=> issue(type = "http://contoso/targetedrole", value = c1.Value + " " c2.Value);

Example Incoming Claims:
"http://contoso.com/location" is “Seattle”
“http://contoso.com/role” is “Editor”

Example Outgoing Claim:
http://contoso.com/targetedrole” is “Seattle Editor”


 

Aggregate Functions

Typical claims rules will issue an output claim for each match it finds.  Aggregate functions will issue or add a single claim regardless of the number of matches.  The EXISTS function serves this purpose. 

EXISTS

Example: Claims rule without an Aggregate Function

c:[type == "http://contoso.com/emailaddress"]
=> issue(type = "http://contoso.com/role", value = "Exchange User");

This example would issue multiple http://contoso.com/role claims if the incoming claim set had multiple email addresses.  If that is not desired, use the EXISTS function as shown below.

Example: Check for any incoming claims with the type http://contoso.com/emailaddress and if any are found, issue a single claim type http://contoso.com/role with the value of Exchange User:
EXISTS([type == "http://contoso.com/emailaddress"])
=> issue(type = "http://contoso/role", value = "Exchange User");


NOT EXISTS


There is also an option to use NOT EXISTS to issue claims if there is no incoming claim that matches the condition.  This can be useful for subsequent rules that combine values.

Example: Claim rule that will only work if the incoming claim set has both claims
c1:[type == "http://contoso.com/location"] &&
c2:[type == "http://contoso.com/role"]
=> issue(type = "http://contoso/targetedrole", value = c1.Value + " " c2.Value);

This claim rule will only trigger if the incoming claim set has a http://contoso.com/location and a http://contoso.com/role incoming claim.  If the location claim is not present, the outgoing set will not contain a http://contoso.com/targetedrole claim.  If it is desired that all outgoing claim sets have this particular claim, the NOT EXISTS function can be used in a separate claim rule.

Example: Claim rule that uses the NOT EXISTS Aggregate Function
NOT EXISTS([type == "http://contoso.com/location"])
=> add(type = "http://contoso/location", value = "Unknown");

Here is an example set of incoming and outgoing claims if the NOT EXISTS Aggregate function claim rule is included with the multiple condition claim rule.

Example Incoming Claims:
“http://contoso.com/role” is “Editor”

Example Outgoing Claim:

http://contoso.com/targetedrole” is “Unknown Editor”

Another good use for the NOT EXISTS aggregate function is to restrict access to certain applications based on group membership.

Example: Issuance Authorization claim rule that uses the NOT EXISTS Aggregate Function
NOT EXISTS([type == "http://contoso.com/group",  Value =~ "^(?i)ADFSUser"])
 => issue(type = "http://schemas.microosft.com/authorization/claims/deny", value = "DenyUsersWithClaim");

This claim rule will deny users access to the relying party if they are not a member of a group that starts with ADFSUser.  It group name evaluation is not case sensitve.  The syntax uses Regular Expressions (regex) which is explained in more detail in the next section.


COUNT

Another aggregate function available in AD FS 2.0 is the COUNT function.  The claim will only be issued if the condition statement is true.

Example: Claim Rule that uses the COUNT Aggregate Function

COUNT([type == http://contoso.com/proxyAddresses"]) >= 2
=> issue(type = "http://contoso.com/MultipleEmails", value = "True");

This claim rule will issue the claim if the user has two or more proxy address claims.

Using Regular Expressions

Regular Expressions (regex) can be used in the condition or issuance statements.  In a condition statement, regex allows similar matches to evaluate true.  In issuance statements, regex allows parts of the string values to be used in the outgoing claim.

Regular Expressions use special characters to perform various tasks inside a string.

 Character  Description  Examples
 $  Matches the end of a string


contoso.com$ matches a string that ends with "contoso.com"
bob@contoso.com would evaluate true
bob@contoso2.com would evauluate false
 ^ Matches the beginning of a string


^bob matches a string that starts with "bob"
bob.smith@contoso.com would evaluate true
bonny.smith@contoso.com would evaluate false

Example: Using the $ expression.  Matches strings that end in "contoso.com"
c:[type == "http://contoso.com/email", Value =~ "contoso.com$"]
=> issue (claim = c);

Example: Using the ^ expression.  Matches strings that start with "bob"
c:[type == "http://contoso.com/email", Value =~ "^bob"]
=> issue (claim = c);

Example: Matches strings that contain "bob"
c:[type == "http://contoso.com/email", Value =~ "bob"]
=> issue (claim = c);



The string matching in the above examples are case-sensitive.  To perform a string match that ignores case, use a pattern (?i) in front of the string.

Example: Matches strings that contain "bob" regardless of case
c:[type == "http://contoso.com/email", Value =~ "(?i)bob"]
=> issue (claim = c);

For more advanced RegEx examples, view this article:
http://social.technet.microsoft.com/wiki/contents/articles/16161.ad-fs-2-0-using-regex-in-the-claims-rule-language.aspx

 

Querying Attribute Stores

Active Directory is the default store created when AD FS 2.0 is installed.  SQL attribute stores and LDAP attribute stores can also be defined.  The condition statement remains the same, but the issuance statement changes depending on which attribute store is used.

SQL Attribute Stores

If user data is located in a SQL database, the Claim Rule Language can query the database and generate claims based on the information in the database.

Example: Claim rule using a SQL Attribute Store

c:[type == "http://contoso.com/emailaddress"]
=> issue (store = "Custom SQL Store", types = ("http://contoso.com/age", "http://contoso.com/purchasinglimit"), query = "SELECT age,purchasinglimit FROM users WHERE email={0}",param = c.value);
This rule looks for an incoming http://contoso.com/emailaddress claim, then queries the SQL store Custom SQL Store for the age and purchasing limit associated with the value of the claim (email address).  It then issues two claims, http://contoso.com/age and http://contoso.com/purchasinglimit with the values stored in the SQL database.

As the example shows, multiple claims can be issued from a single rule.  The query is a standard transact-SQL statement.  The {0} variable is associated with the first param value.  If there are multiple param values, they will be associated in order {0}, {1}, {2}, etc.

 

LDAP Attribute Stores

If user data is located in a LDAP store, the Claim Rule Language can query it and generate claims based on the information in the store.

Example: Claim rule using an LDAP Attribute Store

c:[type == "http://contoso.com/emailaddress"]
=> issue (store = "Custom LDAP Store", types = ("http://contoso.com/age", "http://contoso.com/purchasinglimit"), query = "mail={0};age,purchasinglimit", param = c.value);

The example shown is similar to the SQL attribute example.  The difference is the query parameter.  

Format of an LDAP query in a claim rule
QUERY = "<query_filter>;<attributes>"

Read more about Attribute Stores here.


 

Links to Additional Content


There are many good articles that supplement the data in this article.

AD FS 2.0 Content Map
http://social.technet.microsoft.com/wiki/contents/articles/2735.aspx

When to Use a Custom Claim Rule:

http://technet.microsoft.com/en-us/library/ee913558(WS.10).aspx

The Role of the Claim Rule Language:
http://technet.microsoft.com/en-us/library/dd807118(WS.10).aspx

The Role of the Claims Engine:
http://technet.microsoft.com/en-us/library/ee913582(WS.10).aspx

The Role of the Claims Pipeline:
http://technet.microsoft.com/en-us/library/ee913585(WS.10).aspx

Attribute Stores:
http://technet.microsoft.com/en-us/library/adfs2-help-attribute-stores%28WS.10%29.aspx


Advanced Topics


Here are some articles that go over more advanced topics.

AD FS 2.0: Using RegEx in the Claims Rule Language
http://social.technet.microsoft.com/wiki/contents/articles/16161.ad-fs-2-0-using-regex-in-the-claims-rule-language.aspx 

AD FS 2.0: Selectively send group membership(s) as a claim
http://social.technet.microsoft.com/wiki/contents/articles/8008.ad-fs-2-0-sending-certain-group-membership-as-a-claim.aspx  

AD FS 2.0: Claims to work with shadow accounts
http://social.technet.microsoft.com/wiki/contents/articles/8531.ad-fs-2-0-claims-to-work-with-shadow-accounts.aspx

AD FS 2.0: Domain Local Groups in a claim
http://social.technet.microsoft.com/wiki/contents/articles/13829.ad-fs-2-0-domain-local-groups-in-a-claim.aspx 

AD FS 2.0: Dynamic Claim Types
http://social.technet.microsoft.com/wiki/contents/articles/16170.ad-fs-2-0-dynamic-claim-types.aspx

AD FS 2.0 & Higher: Truncate strings in claims using RegEx
http://social.technet.microsoft.com/wiki/contents/articles/19233.ad-fs-2-0-higher-truncate-strings-in-claims-using-regex.aspx

String Processing Attribute Store: toUpper() toLower()
http://msdn.microsoft.com/en-us/library/hh599320.aspx 

Leave a Comment
  • Please add 4 and 3 and type the answer here:
  • Post
Wiki - Revision Comment List(Revision Comment)
Comments
  • Joji Oshima edited Revision 22. Comment: formatting

  • Richard Mueller edited Revision 29. Comment: Fixed duplicate <a name> tags in HTML so TOC works, added tags

  • Joji Oshima edited Revision 30. Comment: removed single quotes from an example

  • Joji Oshima edited Revision 9. Comment: formatting

Page 1 of 1 (4 items)
Wikis - Comment List
Posting comments is temporarily disabled until 10:00am PST on Saturday, December 14th. Thank you for your patience.
Comments
  • Joji Oshima edited Revision 22. Comment: formatting

  • Richard Mueller edited Revision 29. Comment: Fixed duplicate <a name> tags in HTML so TOC works, added tags

  • Joji Oshima edited Revision 30. Comment: removed single quotes from an example

  • This article was highlighted in the Top Contributors Awards - blogs.technet.com/.../top-contributors-awards-project-server-2013-outlook-sql-server-sharepoint-powershell-and-much-more.aspx

  • How do I read all the values of a given claim, http://contoso.com/role, and put them inside a comma delimited list like "Teacher, Student, Principle", so I can pass that list to a store procedure as shown on the sample below:

    c1:[Type == "contoso.com/.../roles"] &&

    c2:[type == "http://contoso.com/AppId"]

    => issue(store = "Some DB", types = ("schemas.xmlsoap.org/.../AppPermissions"),

    query = "exec [dbo].[GetUserAppPermissions] {0}, {1}", param = List of all c1 values: "Teacher,Student,Principle", param = c2.value);

  • Joji Oshima edited Revision 9. Comment: formatting

  • I would like to request that this very nice document be improved by either showing an example of a test for a false condition, or indicate that this is not possible. An example of testing for a false condition would be "if the user initiating the claim is NOT a member of SomeGroup, then send this attribute."

  • Christopher: This is covered under the "Aggregate Functions" section.

    NOT EXISTS([type == "http://contoso.com/group"])

    => issue(type = "http://contoso/other", value = "Something");

  • GREAT Wiki, thanks for standing this up.

    I had a need to do an OR check on an incoming claim. Rather than creating 5 rules to do this, I accomplished this with the following condition:

    c:[Type == "<my incoming claim type>", Value =~ "^(value1|value2|value3|value3)"]

    => issue(Type = "my outgoing claim type", Value = "my desired role");

    In the above example we check the value of the incoming claim type that begins with "value1" OR "value2" OR "value3" and if one or more of these values exist then we issue an outgoing claim with a value of "my desired role"

  • Can you clarify the QUERYsyntax for LDAP searches? This article states the format should be QUERY = "<query filter>;<attributes>" but when I try this I get an error from ADFS (EventID 111, POLICY3826) stating 'The LDAP query to the Active Directory attribute store must have three parts separated by semicolons. The first part is the LDAP filter query, the second part is a comma-separated list of LDAP attribute names, and the third part ist the user name in 'domain\user' format'.

    My problem is I'm trying to authenticate users where the samAccountName doesn't match the UPN. The default claim template 'Send LDAP attributes as claim' seems hardcoded to use the samAccountName attribute (despite this being a throwback to the NT4 days) so I'm trying a custom claim rule as a workaround. The rule I'm trying is;

    c:[Type == "schemas.microsoft.com/.../windowsaccountname"]

    => issue(

    store = "Active Directory",

    types = ("schemas.xmlsoap.org/.../emailaddress"),

    query = "UserPrincipalName ={0};userPrincipalName,givenName,sn,mail", param = (c.Value)

    );

    How is the third field used? Is it required? If it's required and is fixed to domain\user format does that mean I can't use ADFS with an AD with mismatched samAccountName attributes? Are there any official docs on the claim rule language over and above your excellent and very useful blogposts?

    Apologies for the long post and many questions - I'd planned to write a nice succint question!

    Thanks,

    Ed.

Page 1 of 1 (10 items)