Search code examples
asp.net-core-identity

How are we meant to use IUserClaimsPrincipalFactory<T>?


I'm confused by the example in the documentation here that describes how to add claims using IUserClaimsPrincipalFactory.

The sample code shows how to extend the ApplicationUser class:

public class ApplicationUser : IdentityUser
{
    public bool IsAdmin { get; set; }
}

...and then implement a UserClaimsPrincipalFactory that tests that property to determine which claims to add:

if (user.IsAdmin)
{
    claims.Add(new Claim(JwtClaimTypes.Role, "admin"));
}
else
{
    claims.Add(new Claim(JwtClaimTypes.Role, "user"));
}

It's not stated, but I think the implication is that something else (not shown) will set the IsAdmin property for a user in the database. I think they could have made that clear. (Also, it's disappointing that the example uses roles when there's so much confusion around roles versus claims, but I digress...)

Anyway, we have added some "role" claims to the user based on the value of that new IsAdmin property. So far, so good. What I don't understand is the next bit:

The additional claim can then be used in the app. In a Razor Page, the IAuthorizationService instance can be used to access the claim value.

Sounds like the Razor page is going to access our claim then - but here's the code:

@if ((await AuthorizationService.AuthorizeAsync(User, "IsAdmin")).Succeeded)
{
    ...
}

Is that really accessing the claim? It looks to me like it's accessing the IsAdmin property of the user instead. I don't see how the claim we added is referenced at all - unless there's something else that's not being explained.

That overload of AuthorizeAsync describes the last parameter as 'policyName'. Are we meant to assume that there's a policy called "IsAdmin" that checks for our new role claim?

What a terrible piece of documentation this is - and I'm ignoring the fact that it's also in the wrong place.


Solution

  • It's not stated, but I think the implication is that something else (not shown) will set the IsAdmin property for a user in the database.

    You can set the IsAdmin where you want,For example you can set it when register.Here is a demo: enter image description here

    Input Model in register:

     public class InputModel
            {
                ...
                public bool IsAdmin { get; set; }
            }
    

    Post handler:

    public async Task<IActionResult> OnPostAsync(string returnUrl = null)
            {
                returnUrl ??= Url.Content("~/");
                ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
                if (ModelState.IsValid)
                {
                    var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email ,IsAdmin=Input.IsAdmin};
                    var result = await _userManager.CreateAsync(user, Input.Password);
                    if (result.Succeeded)
                    {
                        _logger.LogInformation("User created a new account with password.");
    
                        var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                        code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                        var callbackUrl = Url.Page(
                            "/Account/ConfirmEmail",
                            pageHandler: null,
                            values: new { area = "Identity", userId = user.Id, code = code, returnUrl = returnUrl },
                            protocol: Request.Scheme);
    
                        //await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                        //    $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
    
                        if (_userManager.Options.SignIn.RequireConfirmedAccount)
                        {
                            return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
                        }
                        else
                        {
                            await _signInManager.SignInAsync(user, isPersistent: false);
                            return LocalRedirect(returnUrl);
                        }
                    }
                    foreach (var error in result.Errors)
                    {
                        ModelState.AddModelError(string.Empty, error.Description);
                    }
                }
    
                // If we got this far, something failed, redisplay form
                return Page();
            }
    

    Is that really accessing the claim? It looks to me like it's accessing the IsAdmin property of the user instead. I don't see how the claim we added is referenced at all - unless there's something else that's not being explained.

    That overload of AuthorizeAsync describes the last parameter as 'policyName'. Are we meant to assume that there's a policy called "IsAdmin" that checks for our new role claim?

    IsAdmin is a ploicy name in the code,you need to add a policy which name is IsAdmin,and check new role claim in it.

     public void ConfigureServices(IServiceCollection services)
            {
                services.AddRazorPages();
                services.AddAuthorization(options =>
                {
                    options.AddPolicy("IsAdmin", policy => policy.RequireClaim("role", "admin"));
                });
            } 
    

    result: enter image description here