Skip to content

Problem adding custom ClaimsPrincipalFactory when using roles #46593

Closed
@Artiom-Evs

Description

@Artiom-Evs

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

I am trying to implement role based authentication and custom user claims in my application.
I using ASP.NET Core with React.js project template.

To generate custom user claims, I use the ApplicationUserClaimsPrincipalFactory class, which is inherited from the UserClaimsPrincipalFactory class with the GenerateClaimsAsync method overridden.

The problem is that if AddClaimsPrincipalFactory is called after AddRoles, only the FullName field is added to the ClaimsPrincipal, and if AddClaimsPrincipalFactory is called before AddRoles, then only the role field is added to the ClaimsPrincipal.

I have created repository that represent this issue.

This issue was also raised in the this issue, but I am not use Blazor and this solution not works for me.
This problem is also in this post on StackOverflow. This solution doesn't work for me either. I have implemented this in the implement-profile-service branch.

I have debugged in different steps but didn't find the place where the problem occurs, only one required claim existed in each of them.

Expected Behavior

The claims role and FullName are added to the ClaimsPrincipal of the authorized user.

Steps To Reproduce

Add support of roles in Program.cs:

builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

Add new custom property to the ApplicationUser class:

public class ApplicationUser : IdentityUser
{
    public string FullName { get; set; }
}

Override Register page of Identity UI using aspnet-codegenerator tool:

dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet aspnet-codegenerator identity -dc SpaWithAuth.Data.ApplicationDbContext --files "Account.Register"

Add full name editor in the Register.cshtml:

...
<div class="form-floating mb-3">
    <input asp-for="Input.FullName" class="form-control" aria-required="true" placeholder="Enter your full name" />
    <label asp-for="Input.FullName">Full name</label>
    <span asp-validation-for="Input.FullName" class="text-danger"></span>
</div>
...

Edit InputModel class in the Register.cshtml.cs:

public class InputModel
{        {
    ...
    [Required]
    public string FullName { get; set; }    }        
}

Edit User object creation in the OnPostAsync method in the Register.cshtml.cs:

public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    ...
    var user = CreateUser();
    user.FullName = Input.FullName;
    ...
}

Add code for automatically adding all users in the 'SomeRole' role in the Register.cshtml.cs:

public class RegisterModel : PageModel
{
    ...
    private readonly RoleManager<IdentityRole> _roleManager;

    public RegisterModel(
        ...
        RoleManager<IdentityRole> roleManager)
    {
        ...
        _roleManager = roleManager;
    }

    ...

    public async Task<IActionResult> OnPostAsync(string returnUrl = null)
    {
        ...
        if (ModelState.IsValid)
        {
            ...
            if (result.Succeeded)
            {
                if (!await _roleManager.RoleExistsAsync("SomeRole"))
                    await _roleManager.CreateAsync(new IdentityRole("SomeRole"));
                await _userManager.AddToRoleAsync(user, "SomeRole");
                ...
            }
            ...
        }
        ...
    }
    ...
}

Edit Home.js to display JWT data:

import React, { Component } from 'react';
import authService from './api-authorization/AuthorizeService'

export class Home extends Component {
    constructor(props) {
      super(props);
      this.state = { user: null };
    }

    componentDidMount() {
      this.loadData();
    }

    async loadData() {
        this.setState({ user: await authService.getUser() });
    }

    render() {
      let { user } = this.state;
      return (
        <div>
          <h1>Claims</h1>
          {JSON.stringify(user)}
        </div>
      );
    }
}

Add roles and custom profile claim in the client scopes in the Program.cs:

builder.Services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
    {
        options.IdentityResources.Add(new IdentityResource("roles", "Roles", new[] { JwtClaimTypes.Role, ClaimTypes.Role }));
        options.IdentityResources.Add(new IdentityResource("custom", "Custom profile data", new[] { nameof(ApplicationUser.FullName) }));
        options.Clients.ToList().ForEach(c =>
        {
            c.AllowedScopes.Add("roles");
            c.AllowedScopes.Add("custom");
        });
    });

Add ApplicationUserClaimsPrincipalFactory class:

public class ApplicationUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser>
{
    public ApplicationUserClaimsPrincipalFactory(
        UserManager<ApplicationUser> userManager, 
        IOptions<IdentityOptions> optionsAccessor) 
        : base(userManager, optionsAccessor) { }

    protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user)
    {
        var identity = await base.GenerateClaimsAsync(user);
        identity.AddClaim(new Claim(nameof(ApplicationUser.FullName), user.FullName));
        return identity;
    }
}

Add ApplicationUserClaimsPrincipalFactory usage in the services configuration in the Program.cs:

builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddClaimsPrincipalFactory<ApplicationUserClaimsPrincipalFactory>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

Exceptions (if any)

No response

.NET Version

7.0.100

Anything else?

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-identityIncludes: Identity and providers

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions