77 lines
2.7 KiB
C#
77 lines
2.7 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using StellaOps.Messaging;
|
|
|
|
namespace StellaOps.Auth.Security.Dpop;
|
|
|
|
public sealed partial class MessagingDpopNonceStore
|
|
{
|
|
/// <inheritdoc />
|
|
public async ValueTask<DpopNonceIssueResult> IssueAsync(
|
|
string audience,
|
|
string clientId,
|
|
string keyThumbprint,
|
|
TimeSpan ttl,
|
|
int maxIssuancePerMinute,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(audience);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(clientId);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(keyThumbprint);
|
|
|
|
if (ttl <= TimeSpan.Zero)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(ttl), "Nonce TTL must be greater than zero.");
|
|
}
|
|
|
|
if (maxIssuancePerMinute < 1)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(maxIssuancePerMinute), "Max issuance per minute must be at least 1.");
|
|
}
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
var storageKey = DpopNonceUtilities.ComputeStorageKey(audience, clientId, keyThumbprint);
|
|
var rateKey = $"{storageKey}:rate";
|
|
|
|
var ratePolicy = new RateLimitPolicy(maxIssuancePerMinute, _rateLimitWindow);
|
|
var rateLimitResult = await _rateLimiter
|
|
.TryAcquireAsync(rateKey, ratePolicy, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (!rateLimitResult.IsAllowed)
|
|
{
|
|
_logger?.LogDebug(
|
|
"DPoP nonce issuance rate-limited for key {StorageKey}. Current: {Current}, Max: {Max}",
|
|
storageKey,
|
|
rateLimitResult.CurrentCount,
|
|
maxIssuancePerMinute);
|
|
return DpopNonceIssueResult.RateLimited("rate_limited");
|
|
}
|
|
|
|
var nonce = DpopNonceUtilities.GenerateNonce();
|
|
var nonceHash = DpopNonceUtilities.EncodeHash(DpopNonceUtilities.ComputeNonceHash(nonce));
|
|
|
|
var now = _timeProvider.GetUtcNow();
|
|
var expiresAt = now.Add(ttl);
|
|
|
|
var metadata = new DpopNonceMetadata
|
|
{
|
|
IssuedAt = now,
|
|
Ttl = ttl
|
|
};
|
|
|
|
var storeResult = await _tokenStore
|
|
.StoreAsync(storageKey, nonceHash, metadata, ttl, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (!storeResult.Success)
|
|
{
|
|
_logger?.LogWarning("Failed to store DPoP nonce for key {StorageKey}", storageKey);
|
|
return DpopNonceIssueResult.Failure("storage_error");
|
|
}
|
|
|
|
_logger?.LogDebug("Issued DPoP nonce for key {StorageKey}, expires at {ExpiresAt:o}", storageKey, expiresAt);
|
|
return DpopNonceIssueResult.Success(nonce, expiresAt);
|
|
}
|
|
}
|