Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Problem adding custom ClaimsPrincipalFactory when using roles #46593

Closed
1 task done
Artiom-Evs opened this issue Feb 12, 2023 · 4 comments
Closed
1 task done

Problem adding custom ClaimsPrincipalFactory when using roles #46593

Artiom-Evs opened this issue Feb 12, 2023 · 4 comments
Labels
area-identity Includes: Identity and providers

Comments

@Artiom-Evs
Copy link

Artiom-Evs commented Feb 12, 2023

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

@javiercn javiercn added the area-identity Includes: Identity and providers label Feb 13, 2023
@quicksln
Copy link

Hello @Artiom-Evs

Choosing the UserClaimsPrincipalFactory to add custom claims to a specific logged-in user is a good choice.
However, you cannot expect it, to add the role claim, you have assigned to the user, AFTER it was executed.

If you want the specific role value to be present in claims, after the user has successfully signed in, try adding the claim to the user 'on the fly'. Please try code below and let me know if it worked.

 
  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");
              var result =  await _userManager.AddClaimsAsync(user, new[] {
                      new Claim(ClaimTypes.Role, "SomeRole"),
              });
              ...
          }
          ...
      }
      ...
  }
  ```

@Artiom-Evs
Copy link
Author

Artiom-Evs commented Feb 13, 2023

Thank you @quicksln!
It works for me.

As I understand it, role claims are generated automaticaly by default.
Once I extending the UserClaimsPrincipalFactory functionality the role claims disappear.
So why extending the UserClaimsPrincipalFactory functionality changes the automatic generation of role claims?

I will be very grateful if you give me an answer or tell me where I can read more about this.

@quicksln
Copy link

I'm glad to hear that the code worked for your use case scenario :)

It's not entirely true that claims are “generated automatically by default”. Usually, “default claims” are assigned to the user during authentication (when using SignInManager) or when the developer decides to do so.
I don't think there is such detailed documentation, but you can check the GitHub source code for more information.

When inspecting SignInManager, you will see where IUserClaimsPrincipalFactory is used.
https://github.com/dotnet/aspnetcore/blob/7c810658463f35c39c54d5fb8a8dbbfd463bf747/src/Identity/Core/src/SignInManager.cs

When checking UserStore for EntityFrameworkCore, you will notice that the AddToRoleAsync method does not create or assign claims to the user.
https://github.com/dotnet/aspnetcore/blob/7c810658463f35c39c54d5fb8a8dbbfd463bf747/src/Identity/EntityFrameworkCore/src/UserStore.cs

I think that’s it.

@Artiom-Evs
Copy link
Author

I found my error.

UserClaimsPrincipalFactory<TUser> has a UserClaimsPrincipalFactory<TUser, TRole> inherited class that implements the functionality for role claims generation. Using AddRoles<TRole> replaces the default UserClaimsPrincipalFactory<TUser> implementation with the UserClaimsPrincipalFactory<TUser, TRole> implementation in the app service collection.

I Inherited my ApplicationUserCalimsPrincipalFactory class from UserClaimsPrincipalFactory<TUser> class that does not have this functionality.

Thanks for the links to the source code! This helped me find the problem.

@ghost ghost locked as resolved and limited conversation to collaborators Mar 16, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-identity Includes: Identity and providers
Projects
None yet
Development

No branches or pull requests

3 participants