lies on a modular pipeline where schemes are registered, middleware invokes handlers, and the result is a ClaimsPrincipal. The solution requires precise service registration, middleware ordering, and advanced patterns for claims enrichment and policy evaluation.
Step 1: Service Registration and Scheme Configuration
Register authentication services with explicit default schemes and scheme-specific options. Avoid implicit defaults in multi-scheme scenarios.
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
// Register Cookie Authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
options.SlidingExpiration = true;
options.Events.OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync;
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Auth:Issuer"],
ValidAudience = builder.Configuration["Auth:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Auth:SecretKey"]))
};
// Critical: Hook into token validation for custom checks
options.Events.OnTokenValidated = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Token validated for user: {UserId}",
context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value);
return Task.CompletedTask;
};
});
Step 2: Middleware Pipeline Ordering
Middleware order is deterministic. Authentication must precede Authorization. UseRouting should wrap these calls to ensure endpoint routing is available.
var app = builder.Build();
app.UseHttpsRedirection();
app.UseRouting();
// Authentication populates HttpContext.User
app.UseAuthentication();
// Authorization evaluates policies against HttpContext.User
app.UseAuthorization();
app.MapControllers();
app.Run();
Claims loaded at login may become stale. Implement IClaimsTransformation to enrich or refresh claims on every request without hitting the database repeatedly. This is essential for applying organization-level permissions or feature flags.
public class ClaimsTransformer : IClaimsTransformation
{
private readonly IUserRepository _userRepository;
private readonly IFeatureService _featureService;
public ClaimsTransformer(IUserRepository userRepository, IFeatureService featureService)
{
_userRepository = userRepository;
_featureService = featureService;
}
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
if (principal.Identity?.IsAuthenticated != true)
return principal;
var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId)) return principal;
// Clone to avoid mutating the original principal
var clone = new ClaimsPrincipal((ClaimsIdentity)principal.Identity.Clone());
var identity = (ClaimsIdentity)clone.Identity!;
// Add dynamic claims based on current state
var roles = await _userRepository.GetRolesAsync(userId);
foreach (var role in roles)
{
identity.AddClaim(new Claim(ClaimTypes.Role, role));
}
var features = await _featureService.GetActiveFeaturesAsync(userId);
foreach (var feature in features)
{
identity.AddClaim(new Claim("feature", feature));
}
return clone;
}
}
// Registration
builder.Services.AddTransient<IClaimsTransformation, ClaimsTransformer>();
Step 4: Policy-Based Authorization
Move beyond [Authorize(Roles="Admin")]. Policies encapsulate complex logic and support dependency injection.
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireElevatedAccess", policy =>
policy.RequireClaim("access_level", "elevated", "superuser"));
options.AddPolicy("MinimumAge", policy =>
policy.RequireAssertion(context =>
{
var dobClaim = context.User.FindFirst(ClaimTypes.DateOfBirth);
if (dobClaim == null) return false;
var dob = DateTime.Parse(dobClaim.Value);
var age = DateTime.Today.Year - dob.Year;
return age >= 18;
}));
});
// Usage
[Authorize(Policy = "RequireElevatedAccess")]
public IActionResult SensitiveData() => Ok();
Step 5: Resource-Based Authorization
For operations requiring evaluation against a specific resource, use IAuthorizationService.
public class DocumentHandler : AuthorizationHandler<DocumentRequirement, Document>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
DocumentRequirement requirement,
Document resource)
{
if (resource.OwnerId == context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
// Registration
builder.Services.AddSingleton<IAuthorizationHandler, DocumentHandler>();
Pitfall Guide
- Middleware Ordering Errors: Placing
UseAuthorization before UseAuthentication results in HttpContext.User being unauthenticated during policy evaluation. Always ensure UseAuthentication precedes UseAuthorization.
- Ignoring Cookie Security Flags: Failing to set
Secure, HttpOnly, and SameSite attributes exposes cookies to XSS, session hijacking, and CSRF. For modern browsers, SameSite=Lax or Strict is mandatory; None requires Secure.
- Storing Sensitive Data in JWT Payload: JWTs are base64 encoded, not encrypted. Storing PII, passwords, or sensitive roles in claims allows any client to decode the token. Use encrypted tokens or reference tokens for sensitive data.
- Overlooking Token Validation Events: Not implementing
OnTokenValidated or OnAuthenticationFailed prevents logging, custom validation (e.g., IP checks), and graceful error handling. This blinds operations to attack patterns.
- Static Role Checks: Using
[Authorize(Roles="Admin")] hardcodes authorization logic. This breaks when roles change or require dynamic evaluation. Policies provide testability and flexibility.
- Large JWT Size: Adding excessive claims to JWTs increases header size, impacting network latency and potentially exceeding HTTP header limits. Monitor token size; if >4KB, switch to reference tokens or reduce claims.
- Key Rotation Neglect: Failing to rotate signing keys or configure key expiration leads to security risks. Use
AddDataProtection with persistent key rings and implement key rollover strategies in production.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Single Page App (SPA) | JWT Bearer + Refresh Tokens | Stateless, works across domains, integrates with IdP | Medium (Token management overhead) |
| Server-Rendered Razor/MVC | Cookie Authentication | Built-in CSRF protection, efficient session management | Low (Standard framework support) |
| Internal Microservices | Reference Tokens or Mutual TLS | Minimal payload, instant revocation, high security | Low (Cache lookup cost) |
| Public API with Social Login | OAuth2/OIDC via IdentityServer/Duende | Delegates auth complexity, supports multiple IdPs | High (IdP infrastructure) |
| High-Throughput API | Cookie Auth or Reference Tokens | Avoids CPU overhead of JWT signature validation | Low (Reduced CPU usage) |
Configuration Template
Copy this template for a robust, production-ready authentication setup with multiple schemes and security hardening.
// Program.cs
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
// Data Protection for key management
builder.Services.AddDataProtection()
.PersistKeysToAzureBlobStorage(new Uri(builder.Configuration["DataProtection:BlobUri"]))
.SetApplicationName("MySecureApp");
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Cookie.Name = "__Secure-Auth";
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
options.SlidingExpiration = true;
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
})
.AddJwtBearer(options =>
{
options.Authority = builder.Configuration["Auth:Authority"];
options.Audience = builder.Configuration["Auth:Audience"];
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
options.Events.OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers["Token-Expired"] = "true";
}
return Task.CompletedTask;
};
});
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-CSRF-TOKEN";
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;
});
var app = builder.Build();
app.UseHsts();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.MapControllers();
app.Run();
Quick Start Guide
- Initialize Project: Run
dotnet new webapi -n AuthDemo and navigate to the directory.
- Add Packages: Execute
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer and dotnet add package Microsoft.AspNetCore.Authentication.Cookies.
- Configure Services: In
Program.cs, add builder.Services.AddAuthentication(...) and builder.Services.AddAuthorization(...) using the template above.
- Add Middleware: Insert
app.UseAuthentication(); and app.UseAuthorization(); between UseRouting() and endpoint mapping.
- Protect Endpoint: Add
[Authorize] to a controller method and test with a valid token or login flow. Verify unauthorized requests return 401.