110 lines
3.6 KiB
C#
110 lines
3.6 KiB
C#
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace SequenceAuth.Lib;
|
|
|
|
public class SequenceAuthMiddleware(RequestDelegate next, IOptions<SequenceAuthOptions> 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<string> nextSequenceOpt, Option<string> userIdOpt, Option<int> 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;
|
|
}
|
|
}
|