Missing Claims in the ASP.NET Core 2 OpenID Connect Handler?

The new OpenID Connect handler in ASP.NET Core 2 has a different (aka breaking) behavior when it comes to mapping claims from an OIDC provider to the resulting ClaimsPrincipal.

This is especially confusing and hard to diagnose since there are a couple of moving parts that come together here. Let’s have a look.

You can use my sample OIDC client here to observe the same results.

Mapping of standard claim types to Microsoft proprietary ones
The first annoying thing is, that Microsoft still thinks they know what’s best for you by mapping the OIDC standard claims to their proprietary ones.

This can be fixed elegantly by clearing the inbound claim type map on the Microsoft JWT token handler:

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

A basic OpenID Connect authentication request
Next – let’s start with a barebones scenario where the client requests the openid scope only.

First confusing thing is that Microsoft pre-populates the Scope collection on the OpenIdConnectOptions with the openid and the profile scope (don’t get me started). This means if you only want to request openid, you first need to clear the Scope collection and then add openid manually.

services.AddAuthentication(options =>
{
    options.DefaultScheme = "Cookies";
    options.DefaultChallengeScheme = "oidc";
})
    .AddCookie("Cookies", options =>
    {
        options.AccessDeniedPath = "/account/denied";
    })
    .AddOpenIdConnect("oidc", options =>
    {
        options.Authority = "https://demo.identityserver.io";
        options.ClientId = "server.hybrid";
        options.ClientSecret = "secret";
        options.ResponseType = "code id_token";
 
        options.SaveTokens = true;
                    
        options.Scope.Clear();
        options.Scope.Add("openid");
                    
        options.TokenValidationParameters = new TokenValidationParameters
        {
            NameClaimType = "name", 
            RoleClaimType = "role"
        };
    });

With the ASP.NET Core v1 handler, this would have returned the following claims: nbf, exp, iss, aud, nonce, iat, c_hash, sid, sub, auth_time, idp, amr.

In V2 we only get sid, sub and idp. What happened?

Microsoft added a new concept to their OpenID Connect handler called ClaimActions. Claim actions allow modifying how claims from an external provider are mapped (or not) to a claim in your ClaimsPrincipal. Looking at the ctor of the OpenIdConnectOptions, you can see that the handler will now skip the following claims by default:

ClaimActions.DeleteClaim("nonce");
ClaimActions.DeleteClaim("aud");
ClaimActions.DeleteClaim("azp");
ClaimActions.DeleteClaim("acr");
ClaimActions.DeleteClaim("amr");
ClaimActions.DeleteClaim("iss");
ClaimActions.DeleteClaim("iat");
ClaimActions.DeleteClaim("nbf");
ClaimActions.DeleteClaim("exp");
ClaimActions.DeleteClaim("at_hash");
ClaimActions.DeleteClaim("c_hash");
ClaimActions.DeleteClaim("auth_time");
ClaimActions.DeleteClaim("ipaddr");
ClaimActions.DeleteClaim("platf");
ClaimActions.DeleteClaim("ver");

If you want to “un-skip” a claim, you need to delete a specific claim action when setting up the handler. The following is the very intuitive syntax to get the amr claim back:

options.ClaimActions.Remove("amr");

If you want to see the raw claims from the token in the principal, you need to clear the whole claims action collection.

Requesting more claims from the OIDC provider
When you are requesting more scopes, e.g. profile or custom scopes that result in more claims, there is another confusing detail to be aware of.

Depending on the response_type in the OIDC protocol, some claims are transferred via the id_token and some via the userinfo endpoint. I wrote about the details here.

So first of all, you need to enable support for the userinfo endpoint in the handler:

options.GetClaimsFromUserInfoEndpoint = true;

If the claims are being returned by userinfo, ClaimsActions are used again to map the claims from the returned JSON document to the principal. The following default settings are used here:

ClaimActions.MapUniqueJsonKey("sub""sub");
ClaimActions.MapUniqueJsonKey("name""name");
ClaimActions.MapUniqueJsonKey("given_name""given_name");
ClaimActions.MapUniqueJsonKey("family_name""family_name");
ClaimActions.MapUniqueJsonKey("profile""profile");
ClaimActions.MapUniqueJsonKey("email""email");

IOW – if you are sending a claim to your client that is not part of the above list, it simply gets ignored, and you need to do an explicit mapping. Let’s say your client application receives the website claim via userinfo (one of the standard OIDC claims, but unfortunately not mapped by Microsoft) – you need to add the mapping yourself:

options.ClaimActions.MapUniqueJsonKey("website""website");

The same would apply for any other claims you return via userinfo.

I hope this helps. In short – you want to be explicit about your mappings, because I am sure that those default mappings will change at some point in the future which will lead to unexpected behavior in your client applications.

This entry was posted in ASP.NET Core, IdentityServer, OpenID Connect, WebAPI. Bookmark the permalink.

2 Responses to Missing Claims in the ASP.NET Core 2 OpenID Connect Handler?

  1. Great post — wish it was there a few weeks ago, as an earlier reading of it might have spared me a few sorrows. I recall all too clearly losing a day wondering what I was doing wrong, getting back so few claims as compared to what I was seeing in videos and the IdentityServer4 docs. I bet the problem I was dealing with was related to what you shared in this post.

  2. Ruard van Elburg says:

    Yesterday I’ve upgraded my IdentityServer to core 2.0. This post helped me a lot, but I ran into a few issues that did cost me some time. As I was trying to understand what changed and what I did do wrong.

    Since this post concentrates on the client side, I forgot to check the server side. Eventually I noticed that the issue I was having was that claims weren’t even added to the ClaimsIdentity on the server.

    I’ve tested scenarios using the samples to see if something in the configuration changed. But that wasn’t the case. In the samples all works well. The only difference is that I’m using a database to store the user information.

    Then it occurred to me that claims are added by the profile service. In core 1.1 this is done automatically, but in core 2.0 this seems to be a breaking change. I have to implement (or call) the profile service. After I’d implemented the profile service the claims are present.

    But then I had a problem with the hardcoded mappings. Having one application that runs on multiple domains, I need to add domain specific claims, e.g. http_domain/role. Since this is a dynamic collection, I cannot hardcode the mapping.

    This was nearly a show stopper, but luckily I found a post on StackOverflow where they mention the ‘OnUserInformationReceived’ event. I don’t know if this is the best way, but it solved my problem of mapping dynamic claims.

    options.Events = new OpenIdConnectEvents()
    {
    OnUserInformationReceived = (context) =>
    {
    var settings = context.Request.HttpContext.RequestServices.GetService();

    ClaimsIdentity claimsId = context.Principal.Identity as ClaimsIdentity;

    var role = context.User[settings.Role]?.Value();
    if (role != null)
    claimsId.AddClaim(new Claim(settings.Role, role));

    return Task.FromResult(0);
    }
    };

    One thing I would like to add, I’ve noticed in my PolicyHandler that the issuer (in the claim) has changed. For me this was also a breaking change. If I’m not mistaken, in core 1.1 all claims were issued by the same issuer, now Access token claims have the issuer url, identity claims have “oidc” as issuer and the dynamic role has ‘LOCAL AUTHORITY’. I can match the issuer and still check it in my PolicyHandler, but I have to check the issuer in this event as well, as I am losing information when adding the claim. Though I wonder if it makes sense to check the issuer at all.

    I’m glad I’ve got everything working now. Upgrading to this new version felt like having to start all over again. But the more I work with IdentityServer, the more I appreciate it.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s