using System; using System.Net; using System.Net.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Http.Resilience; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Polly; using StellaOps.AirGap.Policy; namespace StellaOps.Auth.Client; /// /// DI helpers for the StellaOps auth client. /// public static class ServiceCollectionExtensions { /// /// Registers the StellaOps auth client with the provided configuration. /// public static IServiceCollection AddStellaOpsAuthClient(this IServiceCollection services, Action configure) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configure); services.AddOptions() .Configure(configure) .PostConfigure(static options => options.Validate()); services.TryAddSingleton(); services.AddHttpClient((provider, client) => { var options = provider.GetRequiredService>().CurrentValue; EnsureEgressAllowed(provider, options, "authority-discovery"); client.Timeout = options.HttpTimeout; }).AddResilienceHandler("authority-discovery", ConfigureResilience); services.AddHttpClient((provider, client) => { var options = provider.GetRequiredService>().CurrentValue; EnsureEgressAllowed(provider, options, "authority-jwks"); client.Timeout = options.HttpTimeout; }).AddResilienceHandler("authority-jwks", ConfigureResilience); services.AddHttpClient((provider, client) => { var options = provider.GetRequiredService>().CurrentValue; EnsureEgressAllowed(provider, options, "authority-token"); client.Timeout = options.HttpTimeout; }).AddResilienceHandler("authority-token", ConfigureResilience); return services; } /// /// Registers a file-backed token cache implementation. /// public static IServiceCollection AddStellaOpsFileTokenCache(this IServiceCollection services, string cacheDirectory) { ArgumentNullException.ThrowIfNull(services); ArgumentException.ThrowIfNullOrWhiteSpace(cacheDirectory); services.Replace(ServiceDescriptor.Singleton(provider => { var logger = provider.GetService>(); var options = provider.GetRequiredService>().CurrentValue; var timeProvider = provider.GetService(); return new FileTokenCache(cacheDirectory, timeProvider, options.ExpirationSkew, logger); })); return services; } /// /// Adds authentication and tenancy header handling for an registered via . /// public static IHttpClientBuilder AddStellaOpsApiAuthentication(this IHttpClientBuilder builder, Action configure) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(configure); builder.Services.AddOptions(builder.Name) .Configure(configure) .PostConfigure(static options => options.Validate()); builder.AddHttpMessageHandler(provider => new StellaOpsBearerTokenHandler( builder.Name, provider.GetRequiredService>(), provider.GetRequiredService>(), provider.GetRequiredService(), provider.GetService(), provider.GetService>())); return builder; } private static void ConfigureResilience(ResiliencePipelineBuilder builder, ResilienceHandlerContext context) { context.EnableReloads(); var options = context.GetOptions(); if (!options.EnableRetries || options.NormalizedRetryDelays.Count == 0) { return; } var delays = options.NormalizedRetryDelays; builder.AddRetry(new HttpRetryStrategyOptions { MaxRetryAttempts = delays.Count, DelayGenerator = args => { var index = args.AttemptNumber < delays.Count ? args.AttemptNumber : delays.Count - 1; return ValueTask.FromResult(delays[index]); }, BackoffType = DelayBackoffType.Constant, ShouldHandle = static args => ValueTask.FromResult( args.Outcome.Exception is not null || args.Outcome.Result?.StatusCode is HttpStatusCode.RequestTimeout or HttpStatusCode.TooManyRequests or >= HttpStatusCode.InternalServerError) }); } private static void EnsureEgressAllowed( IServiceProvider provider, StellaOpsAuthClientOptions options, string intent) { ArgumentNullException.ThrowIfNull(provider); ArgumentNullException.ThrowIfNull(options); ArgumentException.ThrowIfNullOrWhiteSpace(intent); if (options.AuthorityUri is null) { return; } var policy = provider.GetService(); if (policy is null) { return; } var request = new EgressRequest("StellaOpsAuthClient", options.AuthorityUri, intent); policy.EnsureAllowed(request); } }