Description
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