206 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			206 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using System;
 | |
| using System.Collections.Generic;
 | |
| using System.Linq;
 | |
| using StellaOps.Auth.Abstractions;
 | |
| 
 | |
| namespace StellaOps.Auth.Client;
 | |
| 
 | |
| /// <summary>
 | |
| /// Options controlling the StellaOps authentication client.
 | |
| /// </summary>
 | |
| public sealed class StellaOpsAuthClientOptions
 | |
| {
 | |
|     private static readonly TimeSpan[] DefaultRetryDelays =
 | |
|     {
 | |
|         TimeSpan.FromSeconds(1),
 | |
|         TimeSpan.FromSeconds(2),
 | |
|         TimeSpan.FromSeconds(5)
 | |
|     };
 | |
|     private static readonly TimeSpan DefaultOfflineTolerance = TimeSpan.FromMinutes(10);
 | |
| 
 | |
|     private readonly List<string> scopes = new();
 | |
|     private readonly List<TimeSpan> retryDelays = new(DefaultRetryDelays);
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Authority (issuer) base URL.
 | |
|     /// </summary>
 | |
|     public string Authority { get; set; } = string.Empty;
 | |
| 
 | |
|     /// <summary>
 | |
|     /// OAuth client identifier (optional for password flow).
 | |
|     /// </summary>
 | |
|     public string ClientId { get; set; } = string.Empty;
 | |
| 
 | |
|     /// <summary>
 | |
|     /// OAuth client secret (optional for public clients).
 | |
|     /// </summary>
 | |
|     public string? ClientSecret { get; set; }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Default scopes requested for flows that do not explicitly override them.
 | |
|     /// </summary>
 | |
|     public IList<string> DefaultScopes => scopes;
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Retry delays applied by HTTP retry policy (empty uses defaults).
 | |
|     /// </summary>
 | |
|     public IList<TimeSpan> RetryDelays => retryDelays;
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Gets or sets a value indicating whether HTTP retry policies are enabled.
 | |
|     /// </summary>
 | |
|     public bool EnableRetries { get; set; } = true;
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Timeout applied to discovery and token HTTP requests.
 | |
|     /// </summary>
 | |
|     public TimeSpan HttpTimeout { get; set; } = TimeSpan.FromSeconds(30);
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Lifetime of cached discovery metadata.
 | |
|     /// </summary>
 | |
|     public TimeSpan DiscoveryCacheLifetime { get; set; } = TimeSpan.FromMinutes(10);
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Lifetime of cached JWKS metadata.
 | |
|     /// </summary>
 | |
|     public TimeSpan JwksCacheLifetime { get; set; } = TimeSpan.FromMinutes(30);
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Buffer applied when determining cache expiration (default: 30 seconds).
 | |
|     /// </summary>
 | |
|     public TimeSpan ExpirationSkew { get; set; } = TimeSpan.FromSeconds(30);
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Gets or sets a value indicating whether cached discovery/JWKS responses may be served when the Authority is unreachable.
 | |
|     /// </summary>
 | |
|     public bool AllowOfflineCacheFallback { get; set; } = true;
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Additional tolerance window during which stale cache entries remain valid if offline fallback is allowed.
 | |
|     /// </summary>
 | |
|     public TimeSpan OfflineCacheTolerance { get; set; } = DefaultOfflineTolerance;
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Parsed Authority URI (populated after validation).
 | |
|     /// </summary>
 | |
|     public Uri AuthorityUri { get; private set; } = null!;
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Normalised scope list (populated after validation).
 | |
|     /// </summary>
 | |
|     public IReadOnlyList<string> NormalizedScopes { get; private set; } = Array.Empty<string>();
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Normalised retry delays (populated after validation).
 | |
|     /// </summary>
 | |
|     public IReadOnlyList<TimeSpan> NormalizedRetryDelays { get; private set; } = Array.Empty<TimeSpan>();
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Validates required values and normalises scope entries.
 | |
|     /// </summary>
 | |
|     public void Validate()
 | |
|     {
 | |
|         if (string.IsNullOrWhiteSpace(Authority))
 | |
|         {
 | |
|             throw new InvalidOperationException("Auth client requires an Authority URL.");
 | |
|         }
 | |
| 
 | |
|         if (!Uri.TryCreate(Authority.Trim(), UriKind.Absolute, out var authorityUri))
 | |
|         {
 | |
|             throw new InvalidOperationException("Auth client Authority must be an absolute URI.");
 | |
|         }
 | |
| 
 | |
|         if (HttpTimeout <= TimeSpan.Zero)
 | |
|         {
 | |
|             throw new InvalidOperationException("Auth client HTTP timeout must be greater than zero.");
 | |
|         }
 | |
| 
 | |
|         if (DiscoveryCacheLifetime <= TimeSpan.Zero)
 | |
|         {
 | |
|             throw new InvalidOperationException("Discovery cache lifetime must be greater than zero.");
 | |
|         }
 | |
| 
 | |
|         if (JwksCacheLifetime <= TimeSpan.Zero)
 | |
|         {
 | |
|             throw new InvalidOperationException("JWKS cache lifetime must be greater than zero.");
 | |
|         }
 | |
| 
 | |
|         if (ExpirationSkew < TimeSpan.Zero || ExpirationSkew > TimeSpan.FromMinutes(5))
 | |
|         {
 | |
|             throw new InvalidOperationException("Expiration skew must be between 0 seconds and 5 minutes.");
 | |
|         }
 | |
| 
 | |
|         if (OfflineCacheTolerance < TimeSpan.Zero)
 | |
|         {
 | |
|             throw new InvalidOperationException("Offline cache tolerance must be greater than or equal to zero.");
 | |
|         }
 | |
| 
 | |
|         AuthorityUri = authorityUri;
 | |
|         NormalizedScopes = NormalizeScopes(scopes);
 | |
|         NormalizedRetryDelays = EnableRetries ? NormalizeRetryDelays(retryDelays) : Array.Empty<TimeSpan>();
 | |
|     }
 | |
| 
 | |
|     private static IReadOnlyList<string> NormalizeScopes(IList<string> values)
 | |
|     {
 | |
|         if (values.Count == 0)
 | |
|         {
 | |
|             return Array.Empty<string>();
 | |
|         }
 | |
| 
 | |
|         var unique = new HashSet<string>(StringComparer.Ordinal);
 | |
| 
 | |
|         for (var index = values.Count - 1; index >= 0; index--)
 | |
|         {
 | |
|             var entry = values[index];
 | |
| 
 | |
|             if (string.IsNullOrWhiteSpace(entry))
 | |
|             {
 | |
|                 values.RemoveAt(index);
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             var normalized = StellaOpsScopes.Normalize(entry);
 | |
|             if (normalized is null)
 | |
|             {
 | |
|                 values.RemoveAt(index);
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             if (!unique.Add(normalized))
 | |
|             {
 | |
|                 values.RemoveAt(index);
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             values[index] = normalized;
 | |
|         }
 | |
| 
 | |
|         return values.Count == 0
 | |
|             ? Array.Empty<string>()
 | |
|             : values.OrderBy(static scope => scope, StringComparer.Ordinal).ToArray();
 | |
|     }
 | |
| 
 | |
|     private static IReadOnlyList<TimeSpan> NormalizeRetryDelays(IList<TimeSpan> values)
 | |
|     {
 | |
|         for (var index = values.Count - 1; index >= 0; index--)
 | |
|         {
 | |
|             var delay = values[index];
 | |
|             if (delay <= TimeSpan.Zero)
 | |
|             {
 | |
|                 values.RemoveAt(index);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (values.Count == 0)
 | |
|         {
 | |
|             foreach (var delay in DefaultRetryDelays)
 | |
|             {
 | |
|                 values.Add(delay);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return values.ToArray();
 | |
|     }
 | |
| }
 |