Unable to Retrieve Claims in .NET Core 2.0

asp.net-core-2.0identityserver4openid-connect

I am using an OpenId Connect Authentication Server, specifically Identity Server 4 (version 1.5.2) on .NET Core 1.1. I have this running with ASP.NET Framework MVC 5 and ASP.NET Core 1.1 MVC web applications. The following configuration is from a .NET Core 1.1 web application:

public void Configure(
    IApplicationBuilder app,
    IHostingEnvironment env,
    ILoggerFactory loggerFactory)
{
    app.UseDeveloperExceptionPage();
    app.UseStatusCodePages();
    app.UseRewriter(new RewriteOptions().AddRedirectToHttps());
    app.UseStaticFiles();

    #region Configure Authentication
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
    
    app.UseCookieAuthentication(
        new CookieAuthenticationOptions
        {
            AuthenticationScheme = "Cookies",
            AutomaticAuthenticate = true,
            AccessDeniedPath = "/AccessDenied"
        });
    app.UseOpenIdConnectAuthentication(
        new OpenIdConnectOptions
        {
            AuthenticationScheme = "oidc",
            SignInScheme = "Cookies",

            Authority = "https://localhost:44316",
            ClientId = "test-mule",
            ClientSecret = "secret",
            ResponseType = "code id_token",

            SaveTokens = true,
            GetClaimsFromUserInfoEndpoint = true,

            PostLogoutRedirectUri = "https://localhost:44324",
            RequireHttpsMetadata = true,

            Scope = { "openid", "profile", "name", "email", "org", "role" },

            TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = "name",
                RoleClaimType = "role"
            }
        });
    #endregion Configure Authentication

    app.UseMvc();
}

Once logged in, I can list the claims associated with the authenticated user:

var claims = User.Claims.OrderBy(c => c.Type).ToList();

On the ASP.NET 1.1 application, this delivers the following claims list:

amr         pwd
aud         test-mule
auth_time   1504529067
c_hash      nouhsuXtd5iKT7B33zxkxg
email       tom@
exp         1504532668
family_name Cobley
given_name  Tom
iat         1504529068
idp         local
iss         https://localhost:44316
name        tom
nbf         1504529068
nonce       6364012...
org         IBX
role        SysAdmin
role        TeleMarketing
role        AccountManager
role        DataManager
role        Member
sid         2091...
sub         1b19...440fa

Which, is what I want/expect.

I am now trying to replicate this behaviour in my first ASP.NET Core 2.0 application with the following Startup configuration:

public Startup()
{
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
}

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication("Cookies")
        .AddCookie("Cookies", options =>
            {
                options.LoginPath = "/SignIn";
                options.AccessDeniedPath = "/AccessDenied";
            })
        .AddOpenIdConnect("oidc", options =>
            {
                options.SignInScheme = "Cookies";
                options.Authority = "https://localhost:44316";
                options.ClientId = "test-mule";
                options.ClientSecret = "secret";
                options.ResponseType = "code id_token";

                options.SaveTokens = true;
                options.GetClaimsFromUserInfoEndpoint = true;

                options.SignedOutRedirectUri = "https://localhost:44367";
                options.RequireHttpsMetadata = true;

                options.Scope.Clear();
                options.Scope.Add("openid");
                options.Scope.Add("profile");
                options.Scope.Add("name");
                options.Scope.Add("email");
                options.Scope.Add("org");
                options.Scope.Add("role");

                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = "name",
                    RoleClaimType = "role"
                };
            });

    // Add framework services.
    services.AddMvc();
}

For reference the /Signin controller action looks like this:

[Route("/SignIn")]
public IActionResult SignIn(string returnUrl = null)
{
    if (!Url.IsLocalUrl(returnUrl)) returnUrl = "/";
    
    var props = new AuthenticationProperties
    {
        RedirectUri = returnUrl
    };
    
    return Challenge(props, "oidc");
}

In this environment, after a successful login, if I list the User's claims I only see a subset of what is available in Core 1.1:

email       tom@...
family_name Cobley
given_name  Tom
idp         local
name        tom.cobley
sid         2091...
sub         1b19...440fa

I have run trace logs on both the client and the server but can't see/identify anything untoward). I am also assuming that it is not an Identity Server problem as it is 'just' an Open Id Connect service that should be consistent against any client?

Can anyone point me in the right direction as to where I am going wrong?

Thanks.

Update

Following MVCutter's suggestion I added an `OnUserInformationReceived event handler as I had noticed that not all of my custom claims were being correctly mapped to the User Identity. I am not sure why this is necessary or if there is a better place to do it, but it appears to give me what I want for now.

private Task OnUserInformationReceivedHandler(
    UserInformationReceivedContext context)
{
    if (!(context.Principal.Identity is ClaimsIdentity claimsId))
    {
        throw new Exception();
    }

    // Get a list of all claims attached to the UserInformationRecieved context
    var ctxClaims = context.User.Children().ToList();

    foreach (var ctxClaim in ctxClaims)
    {
        var claimType = ctxClaim.Path;
        var token = ctxClaim.FirstOrDefault();
        if (token == null)
        {
            continue;
        }

        var claims = new List<Claim>();
        if (token.Children().Any())
        {
            claims.AddRange(
                token.Children()
                    .Select(c => new Claim(claimType, c.Value<string>())));
        }
        else
        {
            claims.Add(new Claim(claimType, token.Value<string>()));
        }

        foreach (var claim in claims)
        {
            if (!claimsId.Claims.Any(
                c => c.Type == claim.Type &&
                     c.Value == claim.Value))
            {
                claimsId.AddClaim(claim);
            }
        }
    }

    return Task.CompletedTask;
}

Best Answer

Please see below, I ran into the same issue as you. I'm sure there is a configuration issue we are missing, but for the time being I parsed the values coming back from the UserInfoEndpoint in the OnUserInformationReceived event handler.

    public override void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(sharedOptions =>
        {
            sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        })
            .AddCookie()
            .AddOpenIdConnect(o =>
            {
                o.ClientId = "sss";
                o.ClientSecret = "sss";
                o.RequireHttpsMetadata = false;
                o.Authority = "http://localhost:60000/";
                o.MetadataAddress = "http://localhost:60000/IdSrv/.well-known/openid-configuration";
                o.ResponseType = OpenIdConnectResponseType.IdTokenToken;
                o.CallbackPath = new PathString("/CGI/Home/Index");
                o.SignedOutCallbackPath = new PathString("/CGI/Account/LoggedOut");
                o.Scope.Add("openid");
                o.Scope.Add("roles");
                o.SaveTokens = true;
                o.GetClaimsFromUserInfoEndpoint = true;
                o.Events = new OpenIdConnectEvents()
                {
                    OnUserInformationReceived = (context) =>
                    {
                        ClaimsIdentity claimsId = context.Principal.Identity as ClaimsIdentity;

                        var roles = context.User.Children().FirstOrDefault(j => j.Path == JwtClaimTypes.Role).Values().ToList();
                        claimsId.AddClaims(roles.Select(r => new Claim(JwtClaimTypes.Role, r.Value<String>())));

                        return Task.FromResult(0);
                    }
                };
                o.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = JwtClaimTypes.Name,
                    RoleClaimType = JwtClaimTypes.Role,
                };
            });
    }

Edit:

I found that there is an extension method for ClaimsAction property called MapUniqueJsonKey, this seems to work for custom single valued keys but bombs on array types like roles... getting closer

o.ClaimActions.MapUniqueJsonKey("UserType", "UserType");
Related Topic