Authorization
Foundatio.Mediator provides built-in, unified authorization that works for both HTTP endpoints and direct mediator.InvokeAsync() calls. Authorization requirements are baked into the handler's HandlerExecutionInfo at compile time, ensuring zero-reflection enforcement at runtime.
Quick Start
Add [HandlerAuthorize] to any handler that requires authentication:
using Foundatio.Mediator;
[HandlerAuthorize]
public class SecureHandler
{
public Task<Result<Secret>> HandleAsync(GetSecret query, ISecretStore store, CancellationToken ct)
{
return store.GetAsync(query.Id, ct);
}
}That's it. The source generator emits an authorization check before the handler runs. If the caller isn't authenticated, Result-returning handlers receive Result.Unauthorized() and non-Result handlers throw UnauthorizedAccessException.
How It Works
Compile time — The source generator reads
[HandlerAuthorize]and[HandlerAllowAnonymous]attributes and assembly-levelAuthorizationRequired/AuthorizationPolicies/AuthorizationRolesproperties, then bakes the requirements into the generated handler wrapper as anAuthorizationRequirementsinstance onHandlerExecutionInfo.Runtime — Before calling the handler method, the generated code:
- Resolves
IAuthorizationContextProviderto get the currentClaimsPrincipal - Resolves
IHandlerAuthorizationServiceto perform the check - Calls
AuthorizeAsync(principal, requirements, cancellationToken) - Short-circuits with the appropriate unauthorized/forbidden result if the check fails
- Resolves
Zero overhead when not used — If a handler has no authorization requirements, no authorization code is generated at all.
Attributes
[HandlerAuthorize]
Apply to a handler class or method to require authorization:
// Class-level: all methods in this handler require auth
[HandlerAuthorize]
public class AdminHandler
{
public Task<Result> HandleAsync(DeleteUser command) { ... }
public Task<Result<User>> HandleAsync(GetUser query) { ... }
}
// Method-level: only this specific handler requires auth
public class MixedHandler
{
[HandlerAuthorize]
public Task<Result> HandleAsync(SensitiveCommand command) { ... }
public Task<Result<PublicData>> HandleAsync(PublicQuery query) { ... }
}Properties
| Property | Type | Description |
|---|---|---|
Roles | string[]? | Array of required roles (any-of semantics) |
Policies | string[]? | Array of authorization policy names to evaluate |
[HandlerAuthorize(Roles = ["Admin", "Manager"])]
public class AdminHandler { ... }
[HandlerAuthorize(Policies = ["CanEditProducts", "IsVerified"])]
public class ProductHandler { ... }
[HandlerAuthorize(Roles = ["Admin"], Policies = ["MfaRequired"])]
public class HighSecurityHandler { ... }[HandlerAllowAnonymous]
Apply to a handler class or method to bypass authorization, even when global AuthorizationRequired = true is set:
[HandlerAllowAnonymous]
public class PublicHandler
{
public Task<Result<Status>> HandleAsync(HealthCheck query) => ...;
}ASP.NET Core's [AllowAnonymous] attribute is also recognized and has the same effect.
Global Configuration
Set AuthorizationRequired = true on the assembly attribute to require auth for all handlers by default:
[assembly: MediatorConfiguration(
AuthorizationRequired = true,
AuthorizationPolicies = ["DefaultPolicy"],
AuthorizationRoles = "User"
)]Then use [HandlerAllowAnonymous] to opt out specific handlers:
[HandlerAllowAnonymous]
public class HealthHandler
{
public string Handle(HealthCheck query) => "OK";
}Precedence
Authorization requirements are resolved in this order (most specific wins):
- Method-level
[HandlerAuthorize]or[HandlerAllowAnonymous] - Class-level
[HandlerAuthorize]or[HandlerAllowAnonymous] - Assembly-level
AuthorizationRequired/AuthorizationPolicies/AuthorizationRoles
If a handler has an explicit [HandlerAuthorize] or [HandlerAllowAnonymous], the assembly-level defaults are not merged in.
Authorization Result Handling
The behavior when authorization fails depends on the handler's return type:
| Return Type | On Unauthorized | On Forbidden |
|---|---|---|
Result, Result<T> | Result.Unauthorized("Authentication required.") | Result.Forbidden("Access denied.") |
| Other types | throw UnauthorizedAccessException | throw UnauthorizedAccessException |
The distinction between Unauthorized (not authenticated) and Forbidden (authenticated but lacking permissions) is made by the authorization service based on whether a principal is present.
Extensibility
IAuthorizationContextProvider
Provides the ClaimsPrincipal for the current execution context:
public interface IAuthorizationContextProvider
{
ClaimsPrincipal? GetCurrentPrincipal();
}Auto-registration: In ASP.NET Core apps (where IHttpContextAccessor is available), a provider that reads from HttpContext.User is automatically registered. For non-web scenarios, implement and register your own:
public class WorkerAuthProvider : IAuthorizationContextProvider
{
public ClaimsPrincipal? GetCurrentPrincipal()
{
// Return a service identity or read from ambient context
return new ClaimsPrincipal(new ClaimsIdentity(
new[] { new Claim(ClaimTypes.Name, "worker-service") },
"ServiceAuth"));
}
}
// Register in DI
services.AddSingleton<IAuthorizationContextProvider, WorkerAuthProvider>();IHandlerAuthorizationService
Performs the actual authorization check:
public interface IHandlerAuthorizationService
{
ValueTask<AuthorizationResult> AuthorizeAsync(
ClaimsPrincipal? principal,
AuthorizationRequirements? requirements,
CancellationToken cancellationToken = default);
}The default implementation checks:
- Whether the principal is authenticated (identity is not null and
IsAuthenticatedis true) - Whether the principal has the required roles (via
IsInRole) - Whether the principal has claims matching the required policies
Replace it to integrate with your own authorization system:
public class CustomAuthService : IHandlerAuthorizationService
{
private readonly IAuthorizationService _aspNetAuth;
public CustomAuthService(IAuthorizationService aspNetAuth)
{
_aspNetAuth = aspNetAuth;
}
public async ValueTask<AuthorizationResult> AuthorizeAsync(
ClaimsPrincipal? principal,
AuthorizationRequirements? requirements,
CancellationToken cancellationToken = default)
{
if (principal?.Identity?.IsAuthenticated != true)
return AuthorizationResult.Unauthorized();
if (requirements?.Policies != null)
{
foreach (var policy in requirements.Policies)
{
var result = await _aspNetAuth.AuthorizeAsync(principal, policy);
if (!result.Succeeded)
return AuthorizationResult.Forbidden($"Policy '{policy}' not satisfied.");
}
}
return AuthorizationResult.Success();
}
}
services.AddSingleton<IHandlerAuthorizationService, CustomAuthService>();Middleware vs Built-in Authorization
You can still use middleware for authorization if you prefer:
[Middleware(Order = 0)]
public class AuthorizationMiddleware
{
public HandlerResult Before(object message, HandlerExecutionInfo info)
{
if (!IsAuthorized(message, info))
return HandlerResult.ShortCircuit(Result.Unauthorized());
return HandlerResult.Continue();
}
}When to use built-in authorization: For standard role/policy-based checks that follow a consistent pattern across handlers.
When to use middleware: For complex, cross-cutting authorization logic that needs access to the full pipeline context, or when you need to authorize based on the message content itself.