Roles and Claims Management with ASP.NET Core Identity
Manage roles and claims with ASP.NET Core Identity: RoleManager, assigning roles to users, role-based vs claim-based authorization, seeding roles on startup, and hierarchical role patterns.
Roles vs Claims in ASP.NET Core Identity
Roles:
Named group membership: "Doctor", "Pharmacist", "NurseAdmin"
Simple assignment: user either has the role or doesn't
Good for coarse-grained access control
Stored in AspNetRoles + AspNetUserRoles tables
Claims:
Key-value facts about the user: "department=Cardiology", "prescribe_schedule_ii=true"
Fine-grained, expressive
Stored in AspNetUserClaims table
Can also come from the JWT payload (transient, not stored)Use roles for "who are you" decisions. Use claims for "what can you do in this context" decisions.
Custom Role Entity
// Domain/Users/AppRole.cs
public sealed class AppRole : IdentityRole<Guid>
{
public string Description { get; set; } = null!;
public DateTime CreatedAt { get; set; }
}Seeding Roles on Startup
// Infrastructure/Persistence/DatabaseSeeder.cs
public static async Task SeedRolesAsync(RoleManager<AppRole> roles)
{
string[] required =
[
"Doctor", "Pharmacist", "Nurse", "Admin", "Auditor"
];
foreach (var name in required)
{
if (!await roles.RoleExistsAsync(name))
{
await roles.CreateAsync(new AppRole
{
Name = name,
CreatedAt = DateTime.UtcNow,
Description = $"{name} role"
});
}
}
}
// Program.cs — after app build
using var scope = app.Services.CreateScope();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<AppRole>>();
await DatabaseSeeder.SeedRolesAsync(roleManager);Assigning and Removing Roles
// Application/Users/Commands/AssignRole/AssignRoleHandler.cs
public sealed class AssignRoleHandler
{
private readonly UserManager<AppUser> _users;
private readonly RoleManager<AppRole> _roles;
public async Task<Result> Handle(AssignRoleCommand cmd, CancellationToken ct)
{
var user = await _users.FindByIdAsync(cmd.UserId.ToString());
if (user is null)
return Result.Failure(UserErrors.NotFound);
if (!await _roles.RoleExistsAsync(cmd.Role))
return Result.Failure(UserErrors.InvalidRole);
if (await _users.IsInRoleAsync(user, cmd.Role))
return Result.Success(); // idempotent
var result = await _users.AddToRoleAsync(user, cmd.Role);
return result.Succeeded
? Result.Success()
: Result.Failure(Error.Failure("Role.AssignFailed",
string.Join(", ", result.Errors.Select(e => e.Description))));
}
}
// Remove role
await _users.RemoveFromRoleAsync(user, roleName);User Claims Management
// Add a claim to a user (stored in AspNetUserClaims)
await _users.AddClaimAsync(user, new Claim("department", "Cardiology"));
await _users.AddClaimAsync(user, new Claim("prescribe_schedule_ii", "true"));
// Get all claims for a user
var claims = await _users.GetClaimsAsync(user);
// Remove a claim
var existing = claims.FirstOrDefault(c => c.Type == "department");
if (existing is not null)
await _users.RemoveClaimAsync(user, existing);
// Replace a claim (update value)
await _users.ReplaceClaimAsync(
user,
existing,
new Claim("department", "Oncology"));Including Identity Claims in JWT
// TokenService.cs — merge Identity claims into the JWT
public async Task<string> GenerateAccessTokenAsync(AppUser user)
{
var identityClaims = await _users.GetClaimsAsync(user);
var roles = await _users.GetRolesAsync(user);
var roleClaims = roles.Select(r => new Claim(ClaimTypes.Role, r));
var allClaims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new(JwtRegisteredClaimNames.Email, user.Email!),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
allClaims.AddRange(identityClaims);
allClaims.AddRange(roleClaims);
// Sign and return token
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: allClaims,
expires: DateTime.UtcNow.AddMinutes(15),
signingCredentials: _signingCredentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}Authorization Policies Using Roles and Claims
builder.Services.AddAuthorizationBuilder()
// Role-based
.AddPolicy("DoctorsOnly", p => p.RequireRole("Doctor"))
.AddPolicy("PharmacyStaff", p => p.RequireRole("Pharmacist", "PharmacyAdmin"))
.AddPolicy("AdminOrAuditor", p => p.RequireRole("Admin", "Auditor"))
// Claim-based
.AddPolicy("ScheduleIIPrescriber", p =>
p.RequireClaim("prescribe_schedule_ii", "true"))
// Combined
.AddPolicy("SeniorDoctorOnly", p =>
p.RequireRole("Doctor")
.RequireClaim("seniority", "Senior", "Consultant"));Hierarchical Roles Pattern
When you want role inheritance (Senior Doctor can do everything Doctor can):
Option A: Assign multiple roles
user.Roles = ["Doctor", "SeniorDoctor"]
Policies check RequireRole("Doctor") → matches both
Option B: Custom IAuthorizationHandler
DoctorOrHigherHandler checks: Doctor OR SeniorDoctor OR Consultant
Single role per user, hierarchy enforced in the handler
Option C: Claims for seniority
role = "Doctor"
seniority claim = "Senior"
Policy: RequireRole("Doctor").RequireClaim("seniority", "Senior")
Best for clinical systems: Option C — role + contextual claimsRed Flag / Green Answer
Red Flag: "We check roles in the controller: if (User.IsInRole("Admin")) { ... } inside the action method."
Role checks scattered in controller code cannot be audited systematically, cannot be tested in isolation, and will be duplicated. When the "Admin" role is renamed or split, every controller needs to be updated.
Green Answer:
Named authorization policies defined at startup. Controllers and endpoints use
.RequireAuthorization("PolicyName"). Role logic is in one place — the policy definition.
PRO TIP — Audit Role Changes
Role assignments are a security event. Log every
AddToRoleAsyncandRemoveFromRoleAsynccall with who triggered it, when, and from what IP. In a clinical system, a pharmacist being elevated to Admin without an audit trail is a compliance violation. Use an EF Core interceptor or domain event to capture this automatically.
Key Takeaway
ASP.NET Core Identity's role and claim system covers most authorization needs: coarse-grained role membership, fine-grained user claims, and policies that combine both. Seed roles at startup, include them in JWT claims at token generation, and define authorization policies once at startup. Never check roles inline in controllers — use named policies that are testable and auditable.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.