using System.Collections.Immutable; using System.Collections.Generic; using System.Linq; namespace StellaOps.Auth.Security.Dpop; /// /// Configures acceptable algorithms and replay windows for DPoP proof validation. /// public sealed class DpopValidationOptions { private readonly HashSet allowedAlgorithms = new(StringComparer.Ordinal); public DpopValidationOptions() { allowedAlgorithms.Add("ES256"); allowedAlgorithms.Add("ES384"); } /// /// Maximum age a proof is considered valid relative to . /// public TimeSpan ProofLifetime { get; set; } = TimeSpan.FromMinutes(2); /// /// Allowed clock skew when evaluating iat. /// public TimeSpan AllowedClockSkew { get; set; } = TimeSpan.FromSeconds(30); /// /// Duration a successfully validated proof is tracked to prevent replay. /// public TimeSpan ReplayWindow { get; set; } = TimeSpan.FromMinutes(5); /// /// Algorithms (JWA) permitted for DPoP proofs. /// public ISet AllowedAlgorithms => allowedAlgorithms; /// /// Normalised, upper-case representation of allowed algorithms. /// public IReadOnlySet NormalizedAlgorithms { get; private set; } = ImmutableHashSet.Empty; public void Validate() { if (ProofLifetime <= TimeSpan.Zero) { throw new InvalidOperationException("DPoP proof lifetime must be greater than zero."); } if (AllowedClockSkew < TimeSpan.Zero || AllowedClockSkew > TimeSpan.FromMinutes(5)) { throw new InvalidOperationException("DPoP allowed clock skew must be between 0 seconds and 5 minutes."); } if (ReplayWindow < TimeSpan.Zero) { throw new InvalidOperationException("DPoP replay window must be greater than or equal to zero."); } if (allowedAlgorithms.Count == 0) { throw new InvalidOperationException("At least one allowed DPoP algorithm must be configured."); } NormalizedAlgorithms = allowedAlgorithms .Select(static algorithm => algorithm.Trim().ToUpperInvariant()) .Where(static algorithm => algorithm.Length > 0) .ToImmutableHashSet(StringComparer.Ordinal); if (NormalizedAlgorithms.Count == 0) { throw new InvalidOperationException("Allowed DPoP algorithms cannot be empty after normalization."); } } }