Files
tiku-backend.net/Tiku.Api/Middleware/AuthRateLimitPartitionMiddleware.cs

106 lines
3.4 KiB
C#

using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.RateLimiting;
using Tiku.Api.Options;
namespace Tiku.Api.Middleware;
public sealed class AuthRateLimitPartitionMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context)
{
var policy = context.GetEndpoint()?
.Metadata
.GetMetadata<EnableRateLimitingAttribute>()?
.PolicyName;
var propertyName = policy switch
{
AuthRateLimitPolicies.Password => "identifier",
AuthRateLimitPolicies.Sms => "phone",
_ => null
};
if (HttpMethods.IsPost(context.Request.Method) && propertyName is not null)
{
await CaptureAccountHashAsync(context, propertyName);
}
await next(context);
}
private static async Task CaptureAccountHashAsync(HttpContext context, string propertyName)
{
context.Request.EnableBuffering(bufferThreshold: 4096, bufferLimit: 16_384);
try
{
using var document = await JsonDocument.ParseAsync(
context.Request.Body,
cancellationToken: context.RequestAborted);
var captured = TryGetStringProperty(document.RootElement, propertyName) ??
(propertyName == "identifier" ? TryGetStringProperty(document.RootElement, "phone") : null);
if (captured is { } value &&
!string.IsNullOrWhiteSpace(value))
{
context.Items[AuthRateLimitPartitionKey.AccountHashItemKey] = Hash(value.Trim());
}
}
catch (JsonException)
{
// MVC will produce the canonical malformed JSON response.
}
catch (IOException)
{
// Oversized or unreadable bodies share the IP-only fallback partition.
}
finally
{
if (context.Request.Body.CanSeek)
{
context.Request.Body.Position = 0;
}
}
}
private static string? TryGetStringProperty(JsonElement element, string propertyName)
{
if (element.ValueKind != JsonValueKind.Object)
{
return null;
}
foreach (var property in element.EnumerateObject())
{
if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase) &&
property.Value.ValueKind == JsonValueKind.String)
{
return property.Value.GetString();
}
}
return null;
}
private static string Hash(string value)
{
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value)))
.ToLowerInvariant();
}
}
public static class AuthRateLimitPartitionKey
{
internal const string AccountHashItemKey = "tiku.auth_rate_limit.account_hash";
public static string Resolve(HttpContext context, string policyName)
{
var ipAddress = context.Connection.RemoteIpAddress?.ToString() ?? "unknown-ip";
var accountHash = context.Items.TryGetValue(AccountHashItemKey, out var value) &&
value is string hash &&
!string.IsNullOrWhiteSpace(hash)
? hash
: "unknown-account";
return $"{policyName}:{ipAddress}:{accountHash}";
}
}