using System; using System.Net; using System.Net.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Polly; using Polly.Extensions.Http; 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; client.Timeout = options.HttpTimeout; }).AddPolicyHandler(static (provider, _) => CreateRetryPolicy(provider)); services.AddHttpClient((provider, client) => { var options = provider.GetRequiredService>().CurrentValue; client.Timeout = options.HttpTimeout; }).AddPolicyHandler(static (provider, _) => CreateRetryPolicy(provider)); services.AddHttpClient((provider, client) => { var options = provider.GetRequiredService>().CurrentValue; client.Timeout = options.HttpTimeout; }).AddPolicyHandler(static (provider, _) => CreateRetryPolicy(provider)); 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; return new FileTokenCache(cacheDirectory, TimeProvider.System, options.ExpirationSkew, logger); })); return services; } private static IAsyncPolicy CreateRetryPolicy(IServiceProvider provider) { var options = provider.GetRequiredService>().CurrentValue; var delays = options.NormalizedRetryDelays; if (delays.Count == 0) { return Policy.NoOpAsync(); } var logger = provider.GetService()?.CreateLogger("StellaOps.Auth.Client.HttpRetry"); return HttpPolicyExtensions .HandleTransientHttpError() .OrResult(static response => response.StatusCode == HttpStatusCode.TooManyRequests) .WaitAndRetryAsync( delays.Count, attempt => delays[attempt - 1], (outcome, delay, attempt, _) => { if (logger is null) { return; } if (outcome.Exception is not null) { logger.LogWarning( outcome.Exception, "Retrying Authority HTTP call ({Attempt}/{TotalAttempts}) after exception; waiting {Delay}.", attempt, delays.Count, delay); } else { logger.LogWarning( "Retrying Authority HTTP call ({Attempt}/{TotalAttempts}) due to status {StatusCode}; waiting {Delay}.", attempt, delays.Count, outcome.Result!.StatusCode, delay); } }); } }