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);
}
}