Adding Authentication
Problem
Your Modulus modules expose HTTP endpoints that need to be protected. You want to authenticate users with JWT bearer tokens and authorize access to specific endpoints based on claims or roles.
Solution
Add JWT bearer authentication to the host application, apply authorization to endpoints using endpoint filters, and optionally create an AuthorizationBehavior pipeline behavior that enforces authorization rules at the mediator level.
Step 1: Install Packages
Add the authentication package to the host project:
dotnet add src/EShop.Host/ package Microsoft.AspNetCore.Authentication.JwtBearerStep 2: Configure JWT Authentication
In the host's Program.cs, configure the authentication and authorization middleware:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Configure JWT authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
// Add middleware -- order matters
app.UseAuthentication();
app.UseAuthorization();
app.Run();Add the JWT configuration to appsettings.json:
{
"Jwt": {
"Issuer": "https://your-app.com",
"Audience": "https://your-app.com",
"Key": "your-secret-key-at-least-32-characters-long"
}
}Secret management
Never commit secret keys to source control. Use .NET User Secrets, environment variables, or a vault service for production deployments.
Step 3: Secure Endpoints
Apply RequireAuthorization() to endpoint definitions in your module's Api layer:
public class CreateProductEndpoint : IEndpoint
{
public void MapEndpoint(IEndpointRouteBuilder app)
{
app.MapPost("/catalog", async (
CreateProduct command,
IMediator mediator,
CancellationToken ct) =>
{
var result = await mediator.Send(command, ct);
return result.Match(
onSuccess: id => Results.Created($"/catalog/{id}", id),
onFailure: errors => Results.BadRequest(errors));
})
.RequireAuthorization()
.WithName("CreateProduct")
.WithTags("Catalog");
}
}For role-based authorization:
app.MapDelete("/catalog/{id:guid}", async (
Guid id,
IMediator mediator,
CancellationToken ct) =>
{
var result = await mediator.Send(new DeleteProductCommand(id), ct);
return result.Match(
onSuccess: () => Results.NoContent(),
onFailure: errors => Results.NotFound(errors));
})
.RequireAuthorization(policy => policy.RequireRole("Admin"));Step 4: Authorization Pipeline Behavior (Optional)
For more granular authorization that lives at the command/query level rather than the endpoint level, create a pipeline behavior:
// Marker interface for commands/queries that require authorization
public interface IAuthorized
{
string RequiredPermission { get; }
}// Command that requires a specific permission
public sealed record DeleteProductCommand(Guid ProductId) : ICommand, IAuthorized
{
public string RequiredPermission => "catalog:delete";
}using System.Security.Claims;
public sealed class AuthorizationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IAuthorized
where TResponse : Result
{
private readonly IHttpContextAccessor _httpContextAccessor;
public AuthorizationBehavior(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
var user = _httpContextAccessor.HttpContext?.User;
if (user?.Identity?.IsAuthenticated != true)
{
return (TResponse)(object)Result.Failure(
Error.Unauthorized(
"Auth.NotAuthenticated",
"Authentication is required."));
}
var permissions = user.FindAll("permission")
.Select(c => c.Value)
.ToHashSet();
if (!permissions.Contains(request.RequiredPermission))
{
return (TResponse)(object)Result.Failure(
Error.Forbidden(
"Auth.InsufficientPermission",
$"Permission '{request.RequiredPermission}' is required."));
}
return await next();
}
}Register the behavior and IHttpContextAccessor:
builder.Services.AddHttpContextAccessor();
builder.Services.AddPipelineBehavior(typeof(AuthorizationBehavior<,>));Register after validation
Place the AuthorizationBehavior after the ValidationBehavior in the pipeline so invalid requests are rejected before the authorization check runs. This avoids unnecessary authorization lookups for bad input.
services.AddPipelineBehavior(typeof(UnhandledExceptionBehavior<,>));
services.AddPipelineBehavior(typeof(LoggingBehavior<,>));
services.AddPipelineBehavior(typeof(ValidationBehavior<,>));
services.AddPipelineBehavior(typeof(AuthorizationBehavior<,>));
services.AddPipelineBehavior(typeof(MetricsBehavior<,>));Discussion
There are two complementary approaches to authorization in a Modulus solution:
Endpoint-level authorization via
RequireAuthorization()-- simple, declarative, and sufficient for most use cases. Use this when authorization is based on authentication status or broad roles.Pipeline-level authorization via
AuthorizationBehavior-- more flexible, testable, and composable. Use this when authorization depends on fine-grained permissions or when the same command can be dispatched from multiple entry points (endpoints, background jobs, event handlers).
You can combine both approaches. Use endpoint authorization as the first line of defense (rejecting unauthenticated requests before they reach the mediator) and the pipeline behavior for fine-grained permission checks.
See Also
- Pipeline Behaviors -- How pipeline behaviors work
- Result Pattern --
Error.UnauthorizedandError.Forbidden