Authentication
Arc.Core provides a flexible authentication system that allows you to implement custom authentication handlers for your application. This is particularly useful for scenarios where you need to authenticate requests based on custom headers, tokens, or other mechanisms without relying on ASP.NET Core’s authentication middleware.
Overview
Section titled “Overview”The authentication system in Arc.Core is built around the IAuthenticationHandler interface. Multiple authentication handlers can be registered, and they’re executed in sequence until one successfully authenticates the request or returns a failure.
Authentication Flow
Section titled “Authentication Flow”The authentication system processes handlers in sequence:
- Each registered
IAuthenticationHandleris called in order - If a handler returns an authenticated result, the process stops and that result is used
- If a handler returns a failure, the process stops and the failure is returned
- If a handler returns anonymous, the next handler is tried
- If all handlers return anonymous, the request is considered anonymous
Authentication Results
Section titled “Authentication Results”Authentication handlers return an AuthenticationResult with one of three possible outcomes:
| Outcome | Description | Usage |
|---|---|---|
| Succeeded | Authentication was successful | Return AuthenticationResult.Succeeded(principal) with a ClaimsPrincipal |
| Failed | Authentication failed with a reason | Return AuthenticationResult.Failed(reason) with a failure reason |
| Anonymous | Handler cannot authenticate this request | Return AuthenticationResult.Anonymous to let other handlers try |
Microsoft Identity Platform (Azure)
Section titled “Microsoft Identity Platform (Azure)”When deploying to Azure — Static Web Apps, Container Apps, App Service, or any platform that injects the EasyAuth headers — the framework provides a ready-made handler so you do not need to write one yourself.
ASP.NET Core (Arc package)
Section titled “ASP.NET Core (Arc package)”Call AddMicrosoftIdentityPlatformIdentityAuthentication() during service registration:
builder.Services.AddMicrosoftIdentityPlatformIdentityAuthentication();This registers MicrosoftIDentityPlatformAuthHandler, which reads the standard EasyAuth headers:
| Header | Purpose |
|---|---|
x-ms-client-principal-id | User ID |
x-ms-client-principal-name | Display name |
x-ms-client-principal | Base64-encoded JSON payload with roles and claims |
See Microsoft Identity Platform for the full setup guide, including how to test locally with a generated principal.
Arc.Core (non-ASP.NET Core)
Section titled “Arc.Core (non-ASP.NET Core)”The AddMicrosoftIdentityPlatformIdentityAuthentication() extension is only available in the Arc ASP.NET Core package. If you are using ArcApplication (the non-ASP.NET Core host), implement IAuthenticationHandler directly to read the same EasyAuth headers:
using System.Security.Claims;using System.Text;using System.Text.Json;using Cratis.Arc.Authentication;using Cratis.Arc.Http;using Cratis.Arc.Identity;
public class MicrosoftIdentityPlatformAuthenticationHandler : IAuthenticationHandler{ public Task<AuthenticationResult> HandleAuthentication(IHttpRequestContext context) { if (!context.Headers.TryGetValue(MicrosoftIdentityPlatformHeaders.IdentityIdHeader, out var userId)) { return Task.FromResult(AuthenticationResult.Anonymous); }
var claims = new List<Claim> { new(ClaimTypes.NameIdentifier, userId) };
if (context.Headers.TryGetValue(MicrosoftIdentityPlatformHeaders.IdentityNameHeader, out var userName)) { claims.Add(new Claim(ClaimTypes.Name, userName)); }
if (context.Headers.TryGetValue(MicrosoftIdentityPlatformHeaders.PrincipalHeader, out var encoded)) { var json = Encoding.UTF8.GetString(Convert.FromBase64String(encoded)); var principal = JsonSerializer.Deserialize<ClientPrincipal>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (principal is not null) { foreach (var role in principal.UserRoles ?? []) claims.Add(new Claim(ClaimTypes.Role, role));
foreach (var claim in principal.Claims ?? []) claims.Add(new Claim(claim.typ, claim.val)); } }
var identity = new ClaimsIdentity(claims, "MicrosoftIdentityPlatform"); return Task.FromResult(AuthenticationResult.Succeeded(new ClaimsPrincipal(identity))); }}The handler is discovered automatically — no explicit registration required.
Implementing an Authentication Handler
Section titled “Implementing an Authentication Handler”Here’s a basic example of implementing a custom authentication handler:
using System.Security.Claims;using Cratis.Arc.Authentication;using Cratis.Arc.Http;
public class ApiKeyAuthenticationHandler : IAuthenticationHandler{ const string ApiKeyHeader = "X-API-Key";
public Task<AuthenticationResult> HandleAuthentication(IHttpRequestContext context) { // Check if the API key header is present if (!context.Headers.TryGetValue(ApiKeyHeader, out var apiKey)) { // No API key present, let other handlers try return Task.FromResult(AuthenticationResult.Anonymous); }
// Validate the API key if (!IsValidApiKey(apiKey)) { // Invalid API key, fail authentication return Task.FromResult( AuthenticationResult.Failed( new AuthenticationFailureReason("Invalid API key"))); }
// Create a claims principal for the authenticated user var claims = new[] { new Claim(ClaimTypes.Name, "API User"), new Claim(ClaimTypes.NameIdentifier, "api-user-123"), new Claim("api_key", apiKey) };
var identity = new ClaimsIdentity(claims, "ApiKey"); var principal = new ClaimsPrincipal(identity);
return Task.FromResult(AuthenticationResult.Succeeded(principal)); }
bool IsValidApiKey(string apiKey) { // Your API key validation logic return apiKey == "your-secret-api-key"; }}Common Authentication Patterns
Section titled “Common Authentication Patterns”Bearer Token Authentication
Section titled “Bearer Token Authentication”public class BearerTokenAuthenticationHandler : IAuthenticationHandler{ const string AuthorizationHeader = "Authorization"; const string BearerPrefix = "Bearer ";
public async Task<AuthenticationResult> HandleAuthentication(IHttpRequestContext context) { if (!context.Headers.TryGetValue(AuthorizationHeader, out var authHeader)) { return AuthenticationResult.Anonymous; }
if (!authHeader.StartsWith(BearerPrefix, StringComparison.OrdinalIgnoreCase)) { return AuthenticationResult.Anonymous; }
var token = authHeader[BearerPrefix.Length..].Trim();
try { var principal = await ValidateAndDecodeToken(token); return AuthenticationResult.Succeeded(principal); } catch (Exception ex) { return AuthenticationResult.Failed( new AuthenticationFailureReason($"Token validation failed: {ex.Message}")); } }
async Task<ClaimsPrincipal> ValidateAndDecodeToken(string token) { // Your token validation logic (e.g., JWT validation) // This is a simplified example await Task.CompletedTask;
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, "user-id"), new Claim(ClaimTypes.Name, "User Name") };
return new ClaimsPrincipal(new ClaimsIdentity(claims, "Bearer")); }}Basic Authentication
Section titled “Basic Authentication”public class BasicAuthenticationHandler : IAuthenticationHandler{ const string AuthorizationHeader = "Authorization"; const string BasicPrefix = "Basic ";
public Task<AuthenticationResult> HandleAuthentication(IHttpRequestContext context) { if (!context.Headers.TryGetValue(AuthorizationHeader, out var authHeader)) { return Task.FromResult(AuthenticationResult.Anonymous); }
if (!authHeader.StartsWith(BasicPrefix, StringComparison.OrdinalIgnoreCase)) { return Task.FromResult(AuthenticationResult.Anonymous); }
var encodedCredentials = authHeader[BasicPrefix.Length..].Trim(); var credentials = Encoding.UTF8.GetString( Convert.FromBase64String(encodedCredentials));
var parts = credentials.Split(':', 2); if (parts.Length != 2) { return Task.FromResult( AuthenticationResult.Failed( new AuthenticationFailureReason("Invalid credentials format"))); }
var username = parts[0]; var password = parts[1];
if (!ValidateCredentials(username, password)) { return Task.FromResult( AuthenticationResult.Failed( new AuthenticationFailureReason("Invalid username or password"))); }
var claims = new[] { new Claim(ClaimTypes.Name, username), new Claim(ClaimTypes.NameIdentifier, username) };
var identity = new ClaimsIdentity(claims, "Basic"); var principal = new ClaimsPrincipal(identity);
return Task.FromResult(AuthenticationResult.Succeeded(principal)); }
bool ValidateCredentials(string username, string password) { // Your credential validation logic return username == "admin" && password == "secret"; }}Custom Header Authentication
Section titled “Custom Header Authentication”public class CustomHeaderAuthenticationHandler : IAuthenticationHandler{ const string UserIdHeader = "X-User-ID"; const string UserRoleHeader = "X-User-Role";
public Task<AuthenticationResult> HandleAuthentication(IHttpRequestContext context) { if (!context.Headers.TryGetValue(UserIdHeader, out var userId)) { return Task.FromResult(AuthenticationResult.Anonymous); }
var role = context.Headers.TryGetValue(UserRoleHeader, out var roleValue) ? roleValue : "User";
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, userId), new Claim(ClaimTypes.Role, role) };
var identity = new ClaimsIdentity(claims, "CustomHeader"); var principal = new ClaimsPrincipal(identity);
return Task.FromResult(AuthenticationResult.Succeeded(principal)); }}Registering Authentication Handlers
Section titled “Registering Authentication Handlers”Authentication handlers are automatically discovered and registered by Arc.Core. Simply ensure your handler implements IAuthenticationHandler and is in a discoverable location:
// The handler will be automatically registeredpublic class MyAuthenticationHandler : IAuthenticationHandler{ public Task<AuthenticationResult> HandleAuthentication(IHttpRequestContext context) { // Implementation }}If you need manual registration:
builder.Services.AddSingleton<IAuthenticationHandler, MyAuthenticationHandler>();Multiple Authentication Handlers
Section titled “Multiple Authentication Handlers”You can register multiple authentication handlers, and they’ll be executed in sequence:
public class ApiKeyHandler : IAuthenticationHandler { /* ... */ }public class BearerTokenHandler : IAuthenticationHandler { /* ... */ }public class BasicAuthHandler : IAuthenticationHandler { /* ... */ }The handlers are tried in order until one returns either:
- A successful authentication result
- A failed authentication result
If all handlers return Anonymous, the request is considered anonymous.
Handler Execution Order
Section titled “Handler Execution Order”Handlers are executed in the order they’re discovered or registered. To control order, you can use explicit registration:
// Register in specific orderbuilder.Services.AddSingleton<IAuthenticationHandler, PrimaryAuthHandler>();builder.Services.AddSingleton<IAuthenticationHandler, FallbackAuthHandler>();Working with Request Context
Section titled “Working with Request Context”Authentication handlers receive an IHttpRequestContext that provides access to request information including headers, query parameters, URL, and HTTP method.
Best Practices
Section titled “Best Practices”Return Anonymous for Non-Applicable Requests
Section titled “Return Anonymous for Non-Applicable Requests”If your handler doesn’t apply to a request, return Anonymous to let other handlers try:
if (!context.Headers.ContainsKey("X-My-Auth-Header")){ return Task.FromResult(AuthenticationResult.Anonymous);}Provide Clear Failure Reasons
Section titled “Provide Clear Failure Reasons”When authentication fails, provide clear, actionable error messages:
return AuthenticationResult.Failed( new AuthenticationFailureReason("API key is expired. Please generate a new key."));Use Dependency Injection
Section titled “Use Dependency Injection”Handlers can use dependency injection for services they need:
public class JwtAuthenticationHandler( ILogger<JwtAuthenticationHandler> logger, ITokenValidator tokenValidator) : IAuthenticationHandler{ public async Task<AuthenticationResult> HandleAuthentication(IHttpRequestContext context) { logger.LogDebug("Validating JWT token"); // Use injected services }}Handle Exceptions Gracefully
Section titled “Handle Exceptions Gracefully”Catch and handle exceptions within your handler:
try{ var principal = await ValidateToken(token); return AuthenticationResult.Succeeded(principal);}catch (SecurityTokenException ex){ return AuthenticationResult.Failed( new AuthenticationFailureReason($"Token validation failed: {ex.Message}"));}catch (Exception ex){ logger.LogError(ex, "Unexpected error during authentication"); return AuthenticationResult.Failed( new AuthenticationFailureReason("Authentication error occurred"));}Integration with Authorization
Section titled “Integration with Authorization”Once a request is authenticated, the ClaimsPrincipal is available for authorization checks. See the Authorization documentation for how to protect endpoints using the [Authorize] and [Roles] attributes.
Testing Authentication Handlers
Section titled “Testing Authentication Handlers”When testing authentication handlers, use the IHttpRequestContext interface:
public class ApiKeyAuthenticationHandlerTests{ [Fact] public async Task should_authenticate_with_valid_api_key() { var handler = new ApiKeyAuthenticationHandler(); var context = new TestHttpRequestContext { Headers = new Dictionary<string, string> { ["X-API-Key"] = "valid-key" } };
var result = await handler.HandleAuthentication(context);
result.IsAuthenticated.ShouldBeTrue(); }}Next Steps
Section titled “Next Steps”- Authorization - Learn how to protect endpoints with authorization attributes
- Identity - Integrate with Arc’s identity system
- Commands - Protect commands with authentication and authorization
- Queries - Protect queries with authentication and authorization