using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; namespace SequenceAuth.Lib; public class SequenceAuthMiddleware(RequestDelegate next, IOptions options) { private readonly SequenceAuthOptions _options = options?.Value ?? throw new InvalidOperationException("SequenceAuthOptions must be registered in DI using AddSequenceAuth."); public async Task InvokeAsync(HttpContext context, SequenceManager sequenceManager) { await (context.Request.Method switch { "OPTIONS" => next(context), _ => ProcessPath(context, sequenceManager) }); } private async Task ProcessPath(HttpContext context, SequenceManager sequenceManager) { var path = context.Request.Path.Value?.ToLowerInvariant() ?? ""; var isIgnored = _options.IgnoredPaths.FirstOrDefault(x => path.StartsWith(x)); await (isIgnored switch { null => CheckHeader(context, sequenceManager), _ => next(context) }); } private async Task CheckHeader(HttpContext context, SequenceManager sequenceManager) { var headerCount = context.Request.Headers[_options.AuthHeaderName].Count; await (headerCount switch { 0 => HandleMissingHeader(context), > 0 => ProcessWithHeader(context, sequenceManager, context.Request.Headers[_options.AuthHeaderName].ToString()!), _ => HandleMissingHeader(context) }); } private Task HandleMissingHeader(HttpContext context) { context.Response.StatusCode = 401; return Task.CompletedTask; } private async Task ProcessWithHeader(HttpContext context, SequenceManager sequenceManager, string sequenceId) { var result = await sequenceManager.ValidateAndRotateAsync(sequenceId); await (result.Outcome switch { ValidationOutcome.Success => HandleSuccess(context, result.NextSequence, result.UserId, result.RequestsRemaining), _ => HandleFailure(context, result.Outcome) }); } private async Task HandleSuccess(HttpContext context, Option nextSequenceOpt, Option userIdOpt, Option requestsRemainingOpt) { _ = nextSequenceOpt.State switch { OptionState.Some => AddHeader(context, nextSequenceOpt.Value!), OptionState.None => 0, _ => 0 }; _ = userIdOpt.State switch { OptionState.Some => AddUserContext(context, userIdOpt.Value!), OptionState.None => 0, _ => 0 }; _ = requestsRemainingOpt.State switch { OptionState.Some => AddRemainingHeader(context, requestsRemainingOpt.Value), OptionState.None => 0, _ => 0 }; await next(context); } private int AddRemainingHeader(HttpContext context, int remaining) { context.Response.Headers.Append(_options.RequestsRemainingHeaderName, remaining.ToString()); return 1; } private int AddHeader(HttpContext context, string value) { context.Response.Headers.Append(_options.NextHeaderName, value); return 1; } private int AddUserContext(HttpContext context, string userId) { context.Items[_options.UserIdItemKey] = userId; return 1; } private Task HandleFailure(HttpContext context, ValidationOutcome outcome) { context.Response.StatusCode = 401; return Task.CompletedTask; } }