Restructure solution layout by module
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user