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.
- Claims come into the Claims Provider Trust as the incoming claim set
- Claims are processed using claim rules and become part of the outgoing claim set
- The outgoing claim set is passed to the Relying Party Trust as the incoming claim set for the Relying Party Trust
- 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 sensitive. 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 evaluate 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:
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
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
**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
String Processing Attribute Store: toUpper() toLower()
**
**http://msdn.microsoft.com/en-us/library/hh599320.aspx