Search code examples
c#asp.net-identityblazoridentityserver4blazor-server-side

@context.user.identity.name is null when using .mapuniquejsonkey


I have an two applications, Blazor and IdentityServer. I noticed that inside of my Startup.cs file under ConfigureServices method options.ClaimActions.MapUniqueJsonKey("role","role") and inside my index.razor file I use @context.user.identity.name it returns null. But when I comment that claimactions line out and replace it with the following:

options.TokenValidationParameters = new TokenValidationParameters
                    {
                        NameClaimType = "name"
                    };

                    options.UseTokenLifetime = false;

it will return the current user's email. Is there a reason why it only returns a value when I replace that claimactions.mapuniquejsonkey line with the code above? I am trying to understand why that is. I saw on a documentation about TokenValidationParameters but still not understanding it like I would like to.

When I have the line options.ClaimActions.MapUniqueJsonKey("role","role") it returns null:

enter image description here

When I comment that line out and replace it with those two lines above:

enter image description here

If you would like to see the full code please see below:

Startup.cs:

public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddServerSideBlazor();


            services.AddSingleton(sp => new HttpClient { BaseAddress = new Uri("http://localhost:36626") }); // WebApi project

            services.AddTransient<IWeatherForecastServices, WeatherForecastServices>();

            services.AddAuthentication(options =>
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
                .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)

                .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
                {
                    options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.SignOutScheme = OpenIdConnectDefaults.AuthenticationScheme;

                    options.Authority = "https://localhost:5443"; // IdentityServer Project
                    options.ClientId = "interactive";
                    options.ClientSecret = "KEY";

                    options.ResponseType = "code";
                  
                    options.Scope.Add("profile"); // default scope
                    options.Scope.Add("scope2");
                    options.Scope.Add("roles");
                    options.Scope.Add("permissions");
                    options.Scope.Add("email");
                    options.ClaimActions.MapUniqueJsonKey("role", "role");

                  /*  options.TokenValidationParameters = new TokenValidationParameters
                      {
                         NameClaimType = "name"
                      };

                    options.UseTokenLifetime = false;  */

                    options.SaveTokens = true;
                    options.GetClaimsFromUserInfoEndpoint = true;
                   
                });

            services.AddScoped<TokenProvider>();

            services.AddCors(options =>
            {
                options.AddPolicy("Open", builder => builder.AllowAnyOrigin().AllowAnyHeader());
            }

            );
            
            services.AddAuthorization(options =>
            {
                options.AddPolicy(Policy.Policies.IsUser, 
              Policy.Policies.IsUserPolicy());
            });                     

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseCors("Open");

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapBlazorHub();
                endpoints.MapFallbackToPage("/_Host");
            });
        }

Index.razor:

Index.razor

<AuthorizeView Policy="@Policy.Policies.IsUser">
    <h3>Welcome, <b>@context.User.Identity.Name</b></h3>
    <p>You can only see this if you satisfy the IsUser policy.</p>
</AuthorizeView>

Config.cs from IdentityServer:

                .......
                new Client
                {
                    ClientId = "interactive",
                    ClientSecrets = { new Secret("KEY".Sha256()) },
                    RequirePkce = true,
                    AllowedGrantTypes = GrantTypes.Code,

                    RedirectUris = { "https://localhost:5445/signin-oidc", "https://localhost:44327/signin-oidc" },
                    FrontChannelLogoutUri = "https://localhost:5445/signout-oidc",
                    PostLogoutRedirectUris = { "https://localhost:5445/signout-callback-oidc" },
                    AlwaysIncludeUserClaimsInIdToken = true,
                    AllowOfflineAccess = true,
                    AllowedScopes = { "openid", "profile", "email" , "scope2" ,"weatherforecast-api","roles","permissions"}
                },
            };

Solution

  • The Name claim and the Role claim are mapped to default properties in the ASP.NET Core HTTP context. Sometimes it is required to use different claims for the default properties, or the name claim and the role claim do not match the default values. The claims can be mapped using the TokenValidationParameters property and set to any claim as required.

    The following code snippet illustrates the use of the TokenValidationParameters:

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

    As you can see, the Name claim is mapped to the "email" field, though usually it'll be mapped to the "name" field.

    Note: As your settings request the profile scope (options.Scope.Add("profile");), no additional claims mapping is required. That is to say that NameClaimType = "name" is superfluous.

    Another way to get the user claims is to use the OpenID Connect User Info API. The ASP.NET Core client application uses the GetClaimsFromUserInfoEndpoint property to configure this. One important difference from the first settings, is that you must specify the claims you require using the MapUniqueJsonKey method, otherwise only the name, given_name and email standard claims will be available in the client application. The claims included in the id_token are mapped per default. This is the major difference to the first option. You must explicitly define some of the claims you require:

       // YOU MUST HAVE THE FIRST LINE WITHOUT WHICH YOU'LL GET NULL VALUES
       options.GetClaimsFromUserInfoEndpoint = true;
       options.ClaimActions.MapUniqueJsonKey("preferred_username", 
                                           "preferred_username");
       options.ClaimActions.MapUniqueJsonKey("gender", "gender");
       options.ClaimActions.MapUniqueJsonKey("role","role");
    

    Note: You should look for answers by me and by Brian Parker, related to authentication and authorization. We've answered many questions in WebAssembly and Blazor Server Apps. These are not articles about the subject, but solutions we provided to developers, and they encompass many aspects of the subject, including custom claims, transformation of claims etc. Brian Parker, also have complete apps in Github...see it.