Some checks failed
		
		
	
	Build Test Deploy / authority-container (push) Has been cancelled
				
			Build Test Deploy / docs (push) Has been cancelled
				
			Build Test Deploy / deploy (push) Has been cancelled
				
			Build Test Deploy / build-test (push) Has been cancelled
				
			Docs CI / lint-and-preview (push) Has been cancelled
				
			
		
			
				
	
	
		
			116 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			116 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| 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;
 | |
| 
 | |
| /// <summary>
 | |
| /// DI helpers for the StellaOps auth client.
 | |
| /// </summary>
 | |
| public static class ServiceCollectionExtensions
 | |
| {
 | |
|     /// <summary>
 | |
|     /// Registers the StellaOps auth client with the provided configuration.
 | |
|     /// </summary>
 | |
|     public static IServiceCollection AddStellaOpsAuthClient(this IServiceCollection services, Action<StellaOpsAuthClientOptions> configure)
 | |
|     {
 | |
|         ArgumentNullException.ThrowIfNull(services);
 | |
|         ArgumentNullException.ThrowIfNull(configure);
 | |
| 
 | |
|         services.AddOptions<StellaOpsAuthClientOptions>()
 | |
|             .Configure(configure)
 | |
|             .PostConfigure(static options => options.Validate());
 | |
| 
 | |
|         services.TryAddSingleton<IStellaOpsTokenCache, InMemoryTokenCache>();
 | |
| 
 | |
|         services.AddHttpClient<StellaOpsDiscoveryCache>((provider, client) =>
 | |
|         {
 | |
|             var options = provider.GetRequiredService<IOptionsMonitor<StellaOpsAuthClientOptions>>().CurrentValue;
 | |
|             client.Timeout = options.HttpTimeout;
 | |
|         }).AddPolicyHandler(static (provider, _) => CreateRetryPolicy(provider));
 | |
| 
 | |
|         services.AddHttpClient<StellaOpsJwksCache>((provider, client) =>
 | |
|         {
 | |
|             var options = provider.GetRequiredService<IOptionsMonitor<StellaOpsAuthClientOptions>>().CurrentValue;
 | |
|             client.Timeout = options.HttpTimeout;
 | |
|         }).AddPolicyHandler(static (provider, _) => CreateRetryPolicy(provider));
 | |
| 
 | |
|         services.AddHttpClient<IStellaOpsTokenClient, StellaOpsTokenClient>((provider, client) =>
 | |
|         {
 | |
|             var options = provider.GetRequiredService<IOptionsMonitor<StellaOpsAuthClientOptions>>().CurrentValue;
 | |
|             client.Timeout = options.HttpTimeout;
 | |
|         }).AddPolicyHandler(static (provider, _) => CreateRetryPolicy(provider));
 | |
| 
 | |
|         return services;
 | |
|     }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Registers a file-backed token cache implementation.
 | |
|     /// </summary>
 | |
|     public static IServiceCollection AddStellaOpsFileTokenCache(this IServiceCollection services, string cacheDirectory)
 | |
|     {
 | |
|         ArgumentNullException.ThrowIfNull(services);
 | |
|         ArgumentException.ThrowIfNullOrWhiteSpace(cacheDirectory);
 | |
| 
 | |
|         services.Replace(ServiceDescriptor.Singleton<IStellaOpsTokenCache>(provider =>
 | |
|         {
 | |
|             var logger = provider.GetService<Microsoft.Extensions.Logging.ILogger<FileTokenCache>>();
 | |
|             var options = provider.GetRequiredService<IOptionsMonitor<StellaOpsAuthClientOptions>>().CurrentValue;
 | |
|             return new FileTokenCache(cacheDirectory, TimeProvider.System, options.ExpirationSkew, logger);
 | |
|         }));
 | |
| 
 | |
|         return services;
 | |
|     }
 | |
| 
 | |
|     private static IAsyncPolicy<HttpResponseMessage> CreateRetryPolicy(IServiceProvider provider)
 | |
|     {
 | |
|         var options = provider.GetRequiredService<IOptionsMonitor<StellaOpsAuthClientOptions>>().CurrentValue;
 | |
|         var delays = options.NormalizedRetryDelays;
 | |
|         if (delays.Count == 0)
 | |
|         {
 | |
|             return Policy.NoOpAsync<HttpResponseMessage>();
 | |
|         }
 | |
| 
 | |
|         var logger = provider.GetService<ILoggerFactory>()?.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);
 | |
|                     }
 | |
|                 });
 | |
|     }
 | |
| }
 |