Learnixo
Back to blog
AI Systemsintermediate

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.

Asma Hafeez KhanMay 16, 20264 min read
ASP.NET Core IdentityRolesClaims.NETAuthorization
Share:𝕏

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

C#
// 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

C#
// 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

C#
// 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

C#
// 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

C#
// 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

C#
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 claims

Red 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 AddToRoleAsync and RemoveFromRoleAsync call 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.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.