Wednesday, March 22, 2017

ADFS : Creating a custom attribute store

This is for ADFS 4.0 on Server 2016.

This is a good write-up.

Unfortunately, all the code is in a screen shot which sucks. Somewhat difficult to copy / paste :-).

Luckily, similar code can be found here.

Just standardise the names; one is "ToUpper"; the other is "toUpper".

However, my requirement was for getting claims from a back-end (details unimportant for the purposes of this post) where a user could have many claims of that type returned. Think of a property ID where one person owns a house but an investor owns several.

All the examples were for returning one attribute.

Essentially, you are returning a C# jagged array e.g. string[][] resultData.

My query string was :

c:[Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"]
 => issue(store = "CAS", types = ("HouseID"), query = "House", param = c.Value);

So I assumed that the multiple claims would be in the same row i.e. one row; many columns.

The search to the back-end returned 3 houses.

I then got:

Microsoft.IdentityServer.ClaimsPolicy.Language.PolicyEvaluationException: POLICY0019: Query 'House' to attribute store 'CAS' returned an unexpected number of fields: expected '1', got '3'.

If you look at the query, you'll see there is only one query parameter which will only return one result. A query which returns 3 results would be like:

query = "House", sn, mail

So what I need is 3 rows; one column.

I found some guidance around this here.

One of the things that stumped me for a while was the fact that the array had to be dynamic because there could be any number of houses. That's why you add to a list and then cast to an array.

 Another gotcha was the fact that while you can have a static rule like:

=> issue(type = "HouseID", value = "123456");

you cannot have that in a query string for the attribute store. You get:

System.ArgumentException: ID4216: The ClaimType 'HouseID' must be of format 'namespace'/'name'.

So it needs to be:

c:[Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"]
 => issue(store = "CAS", types = ("http://claim/HouseID"), query = "House", param = c.Value);

i.e. HouseID becomes http://claim/HouseID.

Once you have compiled a new .dll file, you have to copy it over in the ADFS directory on the server.

You will get "Access Denied" because ADFS is running. So you have to stop the ADFS service, copy over the .dll and then start up the service again.

You do not have to delete the custom attribute store in the wizard and reload it. When ADFS starts. it will load the latest .dll.

The gist with the code is here.

But if you want a sneak preview, the important part is:

try
{
    // Dummy values to illustrate the principle.

    List<string> claimValues = new List<string>();
    claimValues.Add("123456");
    claimValues.Add("654321");
    claimValues.Add("456123");
                
    List<string[]> claimData = new List<string[]>();

    // Each claim value is added to its own string array 
    foreach (string claimVal in claimValues)
    {
        claimData.Add(new string[1] { claimVal });
    }

    // The claim value string arrays are added to the string [][] that is 
    // returned by the Custom Attribute Store EndExecuteQuery()
    string[][] resultData = claimData.ToArray();

    TypedAsyncResult<string[][]> asyncResult = new TypedAsyncResult<string[]
    []>(callback, state);
    asyncResult.Complete(resultData, true);
    return asyncResult;
}

catch (Exception ex)
{
    String innerMess = "";
    if (ex.InnerException != null)
        innerMess = ex.InnerException.ToString();
    throw new AttributeStoreQueryExecutionException("CAS exception : " +
       ex.Message + " " + innerMess);
}
 

Enjoy!

3 comments:

Michael Rowe said...

Thanks for this!

I'm hoping to use this as a basis to take multiple incoming group claims (the number is dynamic), and then convert them into a single outgoing "groups" claim that is a string array of group names.

Have you already tried this before? I'm doing it for an implementation of Kubernetes using OpenID Connect (https://kubernetes.io/docs/admin/authentication/#openid-connect-tokens), which requires a group claim to be a string array. I'm using ADFS 2016.

nzpcmad said...

No - I haven't specifically tried this.

It's an interesting one because there are many separate claims.

Michael Rowe said...

As it turns out I won't need it to work with Kubernetes, they already take multiple claims and turn them into an array, we just have to tell it the claim name. I will still give it a go and let you know what I come up with :)