Skip to main content

Using the ADFS userinfo endpoint instead of allatclaims

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 allatclaims when 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

More information

Popular posts from this blog

Podman– Command execution failed with exit code 125

After updating WSL on one of the developer machines, Podman failed to work. When we took a look through Podman Desktop, we noticed that Podman had stopped running and returned the following error message: Error: Command execution failed with exit code 125 Here are the steps we tried to fix the issue: We started by running podman info to get some extra details on what could be wrong: >podman info OS: windows/amd64 provider: wsl version: 5.3.1 Cannot connect to Podman. Please verify your connection to the Linux system using `podman system connection list`, or try `podman machine init` and `podman machine start` to manage a new Linux VM Error: unable to connect to Podman socket: failed to connect: dial tcp 127.0.0.1:2655: connectex: No connection could be made because the target machine actively refused it. That makes sense as the podman VM was not running. Let’s check the VM: >podman machine list NAME         ...

Cache stampede: when our cache turned against us

While investigating some performance issues, we ran into an ASP.NET Core API that cached a fairly expensive aggregation query for 60 seconds. Under normal load, that was fine: one request rebuilds the cache, everyone else reads from it. Under peak load, dozens of requests would arrive in that same expiry window, all see a cache miss, and all fire the same expensive query in parallel. The database didn't like that. That was the moment when our caching layer stopped helping and started hurting. A burst of requests comes in at the same time, all miss the cache, and all go hammer the database or the downstream API at once. That's a cache stampede . The cache was supposed to protect our backend, and for a few hundred milliseconds it did the opposite. Why this happens IMemoryCache.GetOrCreate (and its async sibling) looks like it protects you, but it doesn't add any locking on its own. Look at the naive version: public async Task<Report> GetReportAsync(string key) ...

VS Code Planning mode

After the introduction of Plan mode in Visual Studio , it now also found its way into VS Code. Planning mode, or as I like to call it 'Hannibal mode', extends GitHub Copilot's Agent Mode capabilities to handle larger, multi-step coding tasks with a structured approach. Instead of jumping straight into code generation, Planning mode creates a detailed execution plan. If you want more details, have a look at my previous post . Putting plan mode into action VS Code takes a different approach compared to Visual Studio when using plan mode. Instead of a configuration setting that you can activate but have limited control over, planning is available as a separate chat mode/agent: I like this approach better than how Visual Studio does it as you have explicit control when plan mode is activated. Instead of immediately diving into execution, the plan agent creates a plan and asks some follow up questions: You can further edit the plan by clicking on ‘Open in Editor’: ...