Role-Based Authorization — Roles, Claims and Policies

ASP.NET Core authorisation supports three levels of control: role-based (user is in a named role), claims-based (user has a specific claim value), and policy-based (complex business rules expressed as requirements and handlers). Role-based authorisation is the simplest and covers most applications. Policy-based authorisation handles cases that roles alone cannot express: “the user can edit this post only if they are its author,” “the user must have agreed to terms of service,” or “requests from the EU must have cookie consent.” All three integrate seamlessly with [Authorize] attributes and the DI system.

Roles and Claims

// ── Seed roles on startup ─────────────────────────────────────────────────
public class RoleSeeder(RoleManager<IdentityRole> roleManager) : IHostedService
{
    private static readonly string[] Roles = ["Admin", "Editor", "Subscriber"];

    public async Task StartAsync(CancellationToken ct)
    {
        foreach (var role in Roles)
        {
            if (!await roleManager.RoleExistsAsync(role))
                await roleManager.CreateAsync(new IdentityRole(role));
        }
    }

    public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}

// ── Assign a role to a user ────────────────────────────────────────────────
var user = await userManager.FindByEmailAsync("admin@blogapp.com");
await userManager.AddToRoleAsync(user!, "Admin");

// ── Add a custom claim to a user ──────────────────────────────────────────
await userManager.AddClaimAsync(user!, new Claim("department", "engineering"));
await userManager.AddClaimAsync(user!, new Claim("subscription", "premium"));

// ── Role-based authorisation ──────────────────────────────────────────────
[Authorize(Roles = "Admin")]
public IActionResult AdminDashboard() => View();

[Authorize(Roles = "Admin,Editor")]  // Admin OR Editor
public IActionResult CreatePost()    => View();

// ── Claims-based authorisation ────────────────────────────────────────────
[Authorize(Policy = "PremiumSubscriber")]
public IActionResult PremiumContent() => View();
Note: Roles in ASP.NET Core Identity are stored as claims in the authentication cookie (ClaimTypes.Role). When you call User.IsInRole("Admin"), it checks whether the cookie’s claims include a role claim with value “Admin”. This means role checks are fast (in-memory claim lookup) and do not require a database query per request. However, if a user’s roles change after they log in, the change is not reflected until their cookie is refreshed (next login or Security Stamp invalidation).
Tip: Use policy-based authorisation for complex rules rather than combining multiple [Authorize(Roles = "...")] attributes. A policy encapsulates the logic in one place and is reusable: [Authorize(Policy = "CanEditPost")]. The policy handler has access to the full AuthorizationHandlerContext including the resource (the post being edited) and the user’s claims, enabling resource-based authorisation: “the user can edit this post if they are its author OR are an Admin.”
Warning: Never rely solely on client-side role checks in views (e.g., @if (User.IsInRole("Admin"))) for security. Views control visibility; they do not enforce access. A determined user can bypass the view and send an HTTP request directly to the controller action. Always combine view-level hiding (for UX) with controller-level [Authorize] attributes (for security). Both are necessary; neither alone is sufficient.

Policy-Based Authorisation

// ── Define policies in Program.cs ─────────────────────────────────────────
builder.Services.AddAuthorization(options =>
{
    // Simple claim requirement
    options.AddPolicy("PremiumSubscriber", policy =>
        policy.RequireClaim("subscription", "premium"));

    // Multiple requirements (all must pass)
    options.AddPolicy("VerifiedEditor", policy =>
    {
        policy.RequireRole("Editor");
        policy.RequireClaim("email_verified", "true");
    });

    // Custom requirement — resource-based authorisation
    options.AddPolicy("CanEditPost", policy =>
        policy.Requirements.Add(new PostOwnerRequirement()));
});

// ── Register the custom handler ───────────────────────────────────────────
builder.Services.AddScoped<IAuthorizationHandler, PostOwnerHandler>();

// ── Custom requirement and handler ────────────────────────────────────────
public class PostOwnerRequirement : IAuthorizationRequirement { }

public class PostOwnerHandler(IPostRepository postRepo)
    : AuthorizationHandler<PostOwnerRequirement, Post>
{
    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        PostOwnerRequirement requirement,
        Post resource)
    {
        var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);

        if (resource.AuthorId == userId || context.User.IsInRole("Admin"))
            context.Succeed(requirement);
        // else: requirement not met — authorisation fails
    }
}

// ── Resource-based authorisation in the controller ────────────────────────
public class PostsController(IAuthorizationService authorizationService,
                             IPostService service) : Controller
{
    [HttpGet("{id:int}/edit"), Authorize]
    public async Task<IActionResult> Edit(int id)
    {
        var post   = await service.GetByIdAsync(id);
        if (post is null) return NotFound();

        var result = await authorizationService.AuthorizeAsync(User, post, "CanEditPost");
        if (!result.Succeeded) return Forbid();

        return View(post.ToEditViewModel());
    }
}

Common Mistakes

Mistake 1 — Only hiding UI elements without protecting controller actions

❌ Wrong — removing the Edit button from the view but not adding [Authorize] to the Edit action.

✅ Correct — always protect controller actions with [Authorize]; view hiding is a UX addition, not a security measure.

Mistake 2 — Using [Authorize(Roles = “Admin,Editor”)] expecting AND semantics (it is OR)

❌ Wrong — thinking “Admin,Editor” means the user must be both Admin AND Editor.

✅ Correct — “Admin,Editor” means Admin OR Editor. For AND semantics, stack multiple [Authorize] attributes or use a policy with multiple RequireRole calls.

🧠 Test Yourself

A user is assigned the Admin role at runtime. Their existing cookie has no Admin role claim. When does the Admin access take effect?