Handling optional claims with the ADFS Claims Rule Language

It is a perfectly normal scenario for claims to be optional in a token. For example, a SAML assertion may contain the mandatory claims:

and optionally the claim:

The ADFS Claims Rule Language is designed to allow claims from incoming tokens to be used to query data stores for additional claims.

At first glance it seems easy enough to construct claim rules that would execute for both the case where only the mandatory claims are present, and also the case where the optional claim is included.

The first rule handles the case where all three claims are present:

 c:[Type == "https://www.contoso.com/claims/givenname"]
&& c1:[Type == "https://www.contoso.com/claims/surname"]
&& c2:[Type == "https://www.contoso.com/claims/dateofbirth"]

=> ISSUE(store = "Contoso Attribute Store", 
TYPES = ("https://www.contoso.com/claims/privatepersonalidentifier"),
QUERY = "Name={0},Surname={1},DateOfBirth={2}",PARAM = c.Value, PARAM = c1.Value, PARAM = c2.Value);

and the second rule handles the case where only the mandatory claims are present:

 c:[Type == "https://www.contoso.com/claims/givenname"]
&& c1:[Type == "https://www.contoso.com/claims/surname"]

=> ISSUE(store = "Contoso Attribute Store", 
TYPES = (“https://www.contoso.com/claims/privatepersonalidentifier"),
QUERY = "Name={0},Surname={1}",PARAM = c.Value, PARAM = c1.Value);

So that the rules as viewed through the ADFS GUI look like the following:

image

Unfortunately, this does not work as expected because of the principle that states:

“All claim rules will fire whose query parameters are satisfied by the claims in the pipeline”

This is fine for the mandatory claim case as only the second rule fires.

However, if the optional claim is also present, both rules fire because the full set of input claims satisfy each set of search criteria. This is still ok if both rules return the same result because ADFS will condense any claims with identical namespace AND value into a single claim. But, it may be the case that the less specific rule, containing only the mandatory claims, will return multiple results (i.e - there are two people with the same name but different dates of birth). As previously stated, ADFS will condense any identical claims but there could still be additional results left over; clearly not ideal as it is possible, using the full set of input claims, to uniquely identify a single user.

This problem has no solution without knowledge of the little documented '”NOT EXISTS” function, in conjunction with the better known ADD function.

The trick is to create three rules.

The first rule checks for the non-presence of the optional claim and adds a dummy claim into the claims pipeline, as follows:

 NOT EXISTS([Type == "https://www.contoso.com/claims/dateofbirth"])
=> ADD(Type = "https://dummy", Value = "dummy");

The second rule checks for the existence of the dummy rule and, if present, queries on the mandatory claims only:

 c:[Type == "https://www.contoso.com/claims/givenname"]
&& c1:[Type == "https://www.contoso.com/claims/surname”]
&& c2:[Type == "https://dummy", Value == "dummy"]
=> ISSUE(store = "Contoso Attribute Store",TYPES = ("https://www.contoso.com/claims/privatepersonalidentifier"),QUERY = "Forename={0},Surname={1}",PARAM = c.Value, PARAM = c1.Value);

The third rule simply queries on all three input claims:

 c:[Type == "https://www.contoso.com/claims/givenname"]
&& c1:[Type == "https://www.contoso.com/claims/surname"]
&& c2:[Type == "https://www.contoso.com/claims/dateofbirth"]
=> ISSUE(store = "Contoso Attribute Store",
TYPES = ("https://www.contoso.com/claims/privatepersonalidentifier"),
QUERY = "Forename={0},Surname={1},DateOfBirth={2}",PARAM = c.Value, PARAM = c1.Value, PARAM = c2.Value);

The combination works because the two rules that query the Contoso Attribute Store are orthogonal to each other and so only one will fire for a given set of input claims.

Finally, it is worth noting that the ADD function in the first rule only adds a claim to the claims pipeline that will not be present in the resulting token.

I hope you find this useful.

written by [bradleycotier]