In my previous post I used the allatclaims scope to get all claims in the ID token. That works, but there’s a catch: every claim you add makes the token bigger. With a lot of group memberships, the token grows quickly, and it gets sent along with every request that carries it.
The alternative is to keep the ID token small and ask ADFS for the extra claims when you need them. That’s what the userinfo endpoint is for.
How does it work?
The userinfo endpoint is part of the OpenID Connect standard. Your application sends the access token as a bearer token, and ADFS returns the claims for that user as JSON.
In ADFS the endpoint lives here:
https://<adfs-server>/adfs/userinfo
You can find it in the discovery document as well:
https://<adfs-server>/adfs/.well-known/openid-configuration
There is one important detail. ADFS treats userinfo as a resource of its own, with the identifier urn:microsoft:userinfo. The access token you send to the endpoint must be issued for that resource. And the claims you get back are decided by the claim rules on that resource.
Step 1: Request an access token for userinfo
The good news is that this is the default. If you don’t pass a resource, ADFS uses urn:microsoft:userinfo. So a normal request works:
https://<adfs-server>/adfs/oauth2/authorize/
?response_type=code
&client_id=<client-id>
&redirect_uri=<redirect-uri>
&scope=openid
No allatclaims needed this time.
Step 2: Define the claim rules for userinfo
Add an issuance transform rule to the userinfo resource. A basic rule that sends a few LDAP attributes looks like this:
c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"]
=> issue(store = "Active Directory",
types = ("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"),
query = ";userPrincipalName,mail,displayName;{0}",
param = c.Value);
Step 3: Call the endpoint
With the access token in hand, a quick test with curl:
curl -H "Authorization: Bearer <access-token>" https://<adfs-server>/adfs/userinfo
The response is a JSON document with the claims from your rule.
Step 4: Let ASP.NET Core do it for you
If you use the Microsoft.AspNetCore.Authentication.OpenIdConnect middleware, you don’t have to write the call yourself. One setting does all the work:
.AddOpenIdConnect(options =>
{
options.Authority = "https://<adfs-server>/adfs";
options.ClientId = "<client-id>";
options.ResponseType = "code";
options.Scope.Add("openid");
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
})
After sign-in, the claims from the userinfo response are merged into the user’s principal.
Remark: The middleware only maps claims it knows by default. For custom claim types, add a claim action, for example options.ClaimActions.MapJsonKey("department", "department").
The audience gotcha
Here is where it gets tricky. Because the userinfo endpoint only accepts tokens issued for urn:microsoft:userinfo, you can’t use the token that you got for your own Web API. If you pass a resource for your API and then call userinfo with that token, ADFS rejects the request with an error like this:
MSIS9921: Received invalid UserInfo request. Audience 'https://resourceurl.com' in the access token
is not same as the identifier of the UserInfo relying party trust 'urn:microsoft:userinfo'.
So the question becomes: how do you get both a token for your API and a token for userinfo?
The answer is simple, you request them separately. The access token for userinfo stays in the web application. The token for your API goes in the Authorization header of your API calls.
Remember this: The userinfo token is only for the userinfo call. Don’t forward it to your API.
allatclaims or userinfo?
So, when to choose what approach?
- Use
allatclaimswhen you have a small set of claims and want to avoid an extra network call - Use the userinfo endpoint when the claim set is large, or changes over time and you don’t want it baked in the ID token
- Both can be combined, but then you maintain claim rules in two places
