Code examples in ASP.NET Core
Program.cs (JWT + policies for roles and claims)
// using Microsoft.AspNetCore.Authentication.JwtBearer;
// using Microsoft.AspNetCore.Authorization;
// using Microsoft.IdentityModel.Tokens;
// using System.Text;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("super-secret-signing-key")),
NameClaimType = "name",
RoleClaimType = "role"
};
});
builder.Services.AddAuthorization(options =>
{
// Role-based policy
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
// Claims-based policy
options.AddPolicy("CanExportReports", policy =>
policy.RequireClaim("permission", "export:reports"));
// Composite policy
options.AddPolicy("FinanceOrExporter", policy =>
policy.RequireAssertion(ctx =>
ctx.User.IsInRole("Finance") ||
ctx.User.HasClaim("permission", "export:reports")));
});
builder.Services.AddControllers();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Controller usage
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class ReportsController : ControllerBase
{
// Role-based
[HttpDelete("{id}")]
[Authorize(Roles = "Admin")]
public IActionResult DeleteReport(int id) => Ok($"Deleted {id}");
// Claims-based
[HttpGet("export")]
[Authorize(Policy = "CanExportReports")]
public IActionResult Export() => Ok("Exported CSV");
// Composite policy
[HttpPost("finance-export")]
[Authorize(Policy = "FinanceOrExporter")]
public IActionResult FinanceExport() => Ok("Finance export done");
}
Issuing claims/roles with Identity (on sign-in)
// using Microsoft.AspNetCore.Identity;
// using System.Security.Claims;
public static async Task SeedAsync(UserManager<IdentityUser> users, RoleManager<IdentityRole> roles)
{
if (!await roles.RoleExistsAsync("Admin"))
await roles.CreateAsync(new IdentityRole("Admin"));
var user = await users.FindByEmailAsync("user@example.com")
?? new IdentityUser { UserName = "user@example.com", Email = "user@example.com", EmailConfirmed = true };
if (user.Id == null) await users.CreateAsync(user, "Pass@1234");
await users.AddToRoleAsync(user, "Admin"); // Role
await users.AddClaimAsync(user, new Claim("permission", "export:reports")); // Claim
await users.AddClaimAsync(user, new Claim("tenant", "42")); // Claim
}
Building a JWT with role and permission claims
// using System.IdentityModel.Tokens.Jwt;
// using System.Security.Claims;
// using Microsoft.IdentityModel.Tokens;
var claims = new List<Claim>
{
new("name", "shiv"),
new("role", "Admin"), // aligns with RoleClaimType = "role"
new("permission", "export:reports"), // app-specific permission
new("tenant", "42")
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("super-secret-signing-key"));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
claims: claims,
expires: DateTime.UtcNow.AddHours(1),
signingCredentials: creds);
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
Advanced guidance and common pitfalls
- Prefer policies: Define policies that express business intent (e.g., "CanRefund"). Fulfil via RequireRole, RequireClaim, or custom IAuthorizationHandler.
- Custom requirements: Use resource-based handlers when authorization depends on data (e.g., user owns order).
- JWT mapping: Align RoleClaimType/NameClaimType with the claim names in your token (e.g., "role", "name"). Don’t assume defaults.
- Minimize token size: Avoid issuing every permission as a claim. Consider coarse “scopes” + server-side checks, or use reference tokens.
- Centralize decisions: Encapsulate complex logic in handlers/services so controllers remain declarative.
- Auditing: Log the evaluated policy, user id/tenant, and decision for traceability.
- Migration path: Start with roles → introduce policies → gradually replace [Authorize(Roles=...)] with [Authorize(Policy=...)] and map roles to claims behind the scenes.
- Multi-tenant: Include tenant in claims and policies to prevent cross-tenant access; validate on every request.
- UI hints: Use claims/roles to drive feature flags in UI, but never rely on UI to enforce security—always enforce server-side.
Sample custom requirement and handler
public class SameTenantRequirement : IAuthorizationRequirement {}
public class SameTenantHandler : AuthorizationHandler<SameTenantRequirement, string>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
SameTenantRequirement requirement,
string resourceTenantId)
{
var userTenant = context.User.FindFirst("tenant")?.Value;
if (!string.IsNullOrEmpty(userTenant) && userTenant == resourceTenantId)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
// Registration
builder.Services.AddAuthorization(o =>
{
o.AddPolicy("SameTenantOnly", p => p.AddRequirements(new SameTenantRequirement()));
});
builder.Services.AddSingleton<IAuthorizationHandler, SameTenantHandler>();
// Usage (e.g., in controller)
[Authorize(Policy = "SameTenantOnly")]
public async Task<IActionResult> Get(string tenantId, [FromServices] IAuthorizationService auth)
{
var ok = await auth.AuthorizeAsync(User, tenantId, "SameTenantOnly");
return ok.Succeeded ? Ok() : Forbid();
}