using Microsoft.Extensions.Logging; using StellaOps.Messaging; namespace StellaOps.Auth.Security.Dpop; public sealed partial class MessagingDpopNonceStore { /// public async ValueTask 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); } }