Microsoft Active Directory Federation Services (AD FS) uses the Claims Rule Language to issue and transform claims between claims providers and relying parties. Dynamic Access Control, introduced with Windows Server 2012, also uses this common language. The flow of claims follows a basic pipeline. The rules we create define which claims are accepted, processed, and eventually sent to the relying party. In this article, I’ll go over the basics of how AD FS builds claims then dive deep into the language that makes it all work. At the end, you should be able to read a claim rule, understand its function, and write custom rules.

The Basics

Before diving into the language used to manipulate and issue claims, it’s important to understand the basics. A claim is information about a user from a trusted source. The trusted source is asserting that the information is true, and that source has authenticated the user in some manner. The claims provider is the source of the claim. This can be information pulled from an attribute store such as Active Directory (AD), or it can be a partner's federation service. The relying party is the destination for the claims. This can be an application such as Microsoft SharePoint or another partner's federation service.

A simple scenario would be AD FS authenticating the user, pulling attributes about the user from AD, and directing the user to the application to consume. The scenario can be more complex by adding partner federation services. In any scenario, we’re taking information from some location and sending it somewhere else. Figure 1 shows a sample relationship between federation servers and an application.

Figure 1: Sample Relationship Between Federation Servers and an Application

 

Claim Sets

You need to understand claim sets in relation to the claims pipeline. When claims come in, they’re part of the incoming claim set. The claims engine is responsible for processing each claim rule. It examines the incoming claim set for possible matches and issues claims as necessary. Each issued claim becomes part of the outgoing claim set. Because we have claim rules for claims providers and relying parties, there are claim sets associated with each of them.

  1. Claims come in to the claims provider trust as the incoming claim set.
  2. Claim rules are processed, and the output becomes part of the outgoing claim set.
  3. The outgoing claim set moves to the respective relying party trust and becomes the incoming claim set for the relying party.
  4. Claim rules are processed, and the output becomes part of the outgoing claim set.

 

General Syntax of the Claims Rule Language

A claim rule consists of two parts: a condition statement and an issuance statement. If the condition statement evaluates true, the issuance statement will execute. The sample claim rule that Figure 2 shows takes an incoming Contoso department claim and issues an Adatum department claim with the same value. These claim types are uniform resource identifiers (URIs) in the HTTP format. URIs aren’t URLs and don’t need to be pages that are accessible on the Internet.

Figure 2: A Simple Claim Rule

 

Condition Statements

When a rule fires, the claims engine evaluates all data currently in the incoming claim set against the condition statement. Any property of the claim can be used in the condition statement, but the most common are the claim type and the claim value. The format of the condition statement is c:[query], where the variable c represents a claim currently in the incoming claim set.

The simple condition statement

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

checks for an incoming claim with the claim type http://contoso.com/department, and the condition statement

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

checks for an incoming claim with the claim type http://contoso.com/department with the value of sales. Condition statements are optional. If you know you want to issue a claim to everyone, you can simply create a rule with the issuance statement.

Issuance Statements

There are two types of issuance statements. The first is ADD, which adds the claim to the incoming claim set, but not the outgoing set. A typical use for ADD is to store data that will be pulled in subsequent claim rules. The second is ISSUE, which adds the claim to the incoming and outgoing claim sets. The ISSUE example

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

issues a claim with the type http://contoso.com/department with the value of marketing. The ADD example

=> add(type = "http://contoso.com/partner", value = "adatum");

adds a claim with the type http://contoso.com/partner with the value of adatum. The issuance statement can pull information from the claim found in the condition statement, or it can use static information. The static data example

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

checks for an incoming claim type http://contoso.com/emailaddress and, if it finds it, issues a claim http://contoso.com/role with the value of Exchange User. The static data example

c:[type == "http://contoso.com/role"]
=> issue(claim = c);

checks for an incoming claim type http://contoso.com/role and, if it finds it, issues the exact same claim to the outgoing claim set. An example of pulling data from the claim,

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

checks for an incoming claim type http://contoso.com/role and, if it finds it, issues the exact same claim to the outgoing claim set.

Multiple Conditions

Another possibility is to use multiple conditions in the condition statement. The issuance statement will fire only if all conditions are met. Each separate condition is joined with the && operator. For example,

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");

checks for an incoming claim with the type http://contoso.com/role with a value of Editor and another incoming claim with the type http://contoso.com/role with a value of Manager. If the claims engine finds both, it will issue a claim with the type http://contoso.com/role with the value of Managing Editor.

The values of claims in any condition can be accessed and joined using the + operator. For example,

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

checks for an incoming claim with the type http://contoso.com/location and separate incoming claim with the type http://contoso.com/role. If it finds both, it will issue a claim with the type http://contoso.com/targetedrole, combining the values of the incoming roles.

Aggregate Functions

Up to this point, each claim rule checks individual claims or groups of claims and fires each time there’s a match. There are some circumstances in which this behavior isn’t ideal, however. For example, you might want to look at the entire incoming claim set and make a condition statement based on that. In such cases, you can use the EXISTS, NOT EXISTS, and COUNT functions. The EXISTS function checks whether there are any incoming claims that match; if there are, it fires a rule. The NOT EXISTS function checks whether there are any incoming claims that match; if there aren’t, it fires a rule. The COUNT function counts the number of matches in the incoming claim set.

The EXISTS example

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

checks for any incoming claims with the type http://contoso.com/emailaddress and, if it finds any, issues a single claim with the type http://contoso.com/role and the value of Exchange User. The NOT EXISTS example

NOT EXISTS([type == "http://contoso.com/location"])
=> add(type = "http://contoso/location", value = "Unknown");

checks for any incoming claims with the type http://contoso.com/location and, if it doesn’t find any, adds a single claim with the type http://contoso.com/location with the value of Unknown. The COUNT example

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

checks for any incoming claims with the type http://contoso.com/proxyAddresses and, if there are two or more, issues a single claim with the type http://contoso.com/MultipleEmails with the value of True.

Querying Attribute Stores

By default, AD is the only attribute store created when you install AD FS. You can query LDAP servers or SQL Server systems to pull data to be used in a claim. To utilize another attribute store, you first create the attribute store and enter the appropriate connection string. Figure 3 shows how to create an LDAP server as an attribute store.

Figure 3: Creating an LDAP Server as an Attribute Store

Once you create the store, you can query the store from a claim rule. For an LDAP attribute store, the query should be in this format:

query = ;

The parameter sent into the query is represented with the {0} operator. If multiple parameters are sent, they would be {1}, {2}, etc. For example,

c:[Type == "http://contoso.com/emailaddress"]
=> issue(
  store = "LDAP STORE",
  types = ("http://contoso.com/attribute1", "http://contoso.com/attribute2"),
  query = "mail={0};attribute1;attribute2",
  param = c.Value
  );

queries LDAP STORE for attribute1 and attribute2, where the email address matches, and issues two claims based on the data returned from the query.

A SQL Server attribute store uses the same basic format of the claim rule language; only the query syntax is different. It follows the standard Transact-SQL format, and the {0} operator is used to pass the parameter. For example,

c:[Type == "http://contoso.com/emailaddress"]
=> issue(
  store = "SQL STORE",
  types = ("http://contoso.com/attribute1", "http://contoso.com/attribute2"),
  query = "SELECT attribute1,attribute2 FROM users WHERE email = {0}",
  param = c.Value
  );

queries SQL STORE for attribute1 and attribute2, where the email address matches, and issues two claims based on the data returned from the query.

Regular Expressions

The use of regular expressions (RegEx) lets you search or manipulate data strings in powerful ways to get a desired result. Without RegEx, any comparisons or replacements must be an exact match. This is sufficient for many situations, but if you need to search or replace based on a pattern, you can use RegEx. RegEx uses pattern matching to search inside strings with great precision. You can also use it to manipulate the data inside the claims.

To perform a pattern match, you can change the double equals operator (==) to =~ and use special metacharacters in the condition statement. If you’re unfamiliar with RegEx, let's start with some of the common metacharacters and see what the result is when using them. Table 1 shows basic RegEx metacharacters and their functions.

RegExReplace

You can also use RegEx pattern matching in replacement scenarios. This is similar to a find-and-replace algorithm found in many text editors, but it uses pattern matching instead of exact values. To use this in a claim rule, use the RegExReplace() function in the value section of the issuance statement.

The RegExReplace function accepts three parameters.

  • The first is the string in which you’re searching. You’ll typically want to search the value of the incoming claim (c.Value), but this could be a combination of values (c1.Value + c2.Value).
  • The second is the RegEx pattern you’re searching for in the first parameter.
  • The third is the string value that will replace any matches found.

The RegExReplace example

c:[type == "http://contoso.com/role"]
=> issue (Type = "http://contoso.com/role", Value =  RegExReplace(c.Value, "(?i)director", "Manager");

passes through any role claims. If any of the claims contain the word Director, RegExReplace will change it to Manager. For example, Director of Finance would pass through as Manager of Finance.

If you combine the power of RegEx pattern matching with the concepts mentioned earlier in the article, you can accomplish many tasks using the Claims Rule Language.

Coding Custom Attribute Stores

AD FS gives you the ability to plug in a custom attribute store if the built-in functionality isn’t sufficient to accomplish your goals. You can use standard .NET code such as toUpper() and toLower() or pull data from any source through the code. This code should be a class library and will need references to the Microsoft.IdentityModel and Microsoft.IdentityServer.ClaimsPolicy assemblies.

Try Custom!

Creating custom rules with the Claims Rule Language gives you more flexibility with claims issuance and transformation. It can take a while to familiarize yourself with the syntax, but it becomes much easier with practice. If you want to dive into this language, try writing custom rules instead of using the templates next time.