From da6d6afcedaa3d56b9a6085a7491a2059a568103 Mon Sep 17 00:00:00 2001 From: Luiz Adolfo Date: Thu, 18 Sep 2025 15:28:27 -0300 Subject: [PATCH] Added extension method for IHttpClientBuilder that allows to inject a custom implementation of the IAuthenticationHandler to AuthorizationInterceptorHandler. --- README.md | 10 ++-- samples/SourceApi/Program.cs | 46 +++++++++++++++++++ ...nInterceptor.Extensions.HybridCache.csproj | 2 +- .../Extensions/HttpClientBuilderExtensions.cs | 23 ++++++++++ src/Directory.Build.props | 4 +- .../HttpClientBuilderExtensionsTests.cs | 35 +++++++++++++- 6 files changed, 112 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c44f2ba..a681410 100644 --- a/README.md +++ b/README.md @@ -29,12 +29,16 @@ dotnet add package AuthorizationInterceptor When adding a new HttpClient, call the extension method `AddAuthorizationInterceptorHandler`, passing in the authentication class for the target API: ```csharp services.AddHttpClient("TargetApi") - .AddAuthorizationInterceptorHandler() + .AddAuthorizationInterceptorHandler(); ``` -This will make the `TargetApi` HttpClient use the Authorization Interceptor handler to generate and manage authorization headers. +If you have different authentication options for the same integration within the same project, you can use the overload method by passing an implementation invoker for the authentication class, like this: +```csharp +services.AddHttpClient("TargetApi") + .AddAuthorizationInterceptorHandler((provider) => ActivatorUtilities.CreateInstance(provider, someOtherDependency)); +``` -> By default, the package will not use any interceptor to store authorization headers so the recommendation is to use at least [AuthorizationInterceptor.Extensions.MemoryCache](https://nuget.org/packages/AuthorizationInterceptor.Extensions.MemoryCache) package to store and manage the authorization headers lifecycle. Checkout [Interceptors section](#interceptors). +This will make the `TargetApi` HttpClient use the Authorization Interceptor handler to generate and manage authorization headers. The `TargetApiAuthClass` must implement the `IAuthenticationHandler` interface, so that the package can perform the necessary dependency and know where and when to generate the authorization headers. An example implementation of the class would look like this: diff --git a/samples/SourceApi/Program.cs b/samples/SourceApi/Program.cs index 9dd6be4..74ae50e 100644 --- a/samples/SourceApi/Program.cs +++ b/samples/SourceApi/Program.cs @@ -62,6 +62,14 @@ }) .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://localhost:5121")); +builder.Services.AddHttpClient("TargetApiWithData1") + .AddAuthorizationInterceptorHandler(provider => ActivatorUtilities.CreateInstance(provider, new SomeData("data1"))) + .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://localhost:5121")); + +builder.Services.AddHttpClient("TargetApiWithData2") + .AddAuthorizationInterceptorHandler(provider => ActivatorUtilities.CreateInstance(provider, new SomeData("data2"))) + .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://localhost:5121")); + var app = builder.Build(); if (app.Environment.IsDevelopment()) @@ -100,6 +108,18 @@ return await client.GetAsync("/data"); }); +app.MapGet("/test/TargetApiWithData1", async (IHttpClientFactory factory) => +{ + var client = factory.CreateClient("TargetApiWithData1"); + return await client.GetAsync("/data"); +}); + +app.MapGet("/test/TargetApiWithData2", async (IHttpClientFactory factory) => +{ + var client = factory.CreateClient("TargetApiWithData2"); + return await client.GetAsync("/data"); +}); + app.Run(); public class User @@ -140,6 +160,30 @@ public TargetApiAuthClass(IHttpClientFactory httpClientFactory) return new OAuthHeaders(user.AccessToken, user.TokenType, user.ExpiresIn, user.RefreshToken, user.RefreshTokenExpiresIn); } } + +public class TargetApiWithDataAuthClass : IAuthenticationHandler +{ + private readonly SomeData _data; + private readonly HttpClient _client; + + public TargetApiWithDataAuthClass(SomeData data, IHttpClientFactory httpClientFactory) + { + _data = data; + _client = httpClientFactory.CreateClient("TargetApiAuth"); + } + + public async ValueTask AuthenticateAsync(AuthorizationHeaders? expiredHeaders, CancellationToken cancellationToken) + { + var response = expiredHeaders != null && expiredHeaders.OAuthHeaders != null + ? await _client.PostAsync($"refresh?refresh={expiredHeaders.OAuthHeaders?.RefreshToken}&data={_data.Data}", null) + : await _client.PostAsync($"auth?data={_data.Data}", null); + + var content = await response.Content.ReadAsStringAsync(); + var user = JsonSerializer.Deserialize(content); + return new OAuthHeaders(user.AccessToken, user.TokenType, user.ExpiresIn, user.RefreshToken, user.RefreshTokenExpiresIn); + } +} + public class CustomInterceptor1 : IAuthorizationInterceptor { public ValueTask GetHeadersAsync(string name, CancellationToken cancellationToken) @@ -178,3 +222,5 @@ public ValueTask UpdateHeadersAsync(string name, AuthorizationHeaders? expiredHe return ValueTask.CompletedTask; } } + +public record SomeData(string Data); diff --git a/src/AuthorizationInterceptor.Extensions.HybridCache/AuthorizationInterceptor.Extensions.HybridCache.csproj b/src/AuthorizationInterceptor.Extensions.HybridCache/AuthorizationInterceptor.Extensions.HybridCache.csproj index ab131b2..0547b21 100644 --- a/src/AuthorizationInterceptor.Extensions.HybridCache/AuthorizationInterceptor.Extensions.HybridCache.csproj +++ b/src/AuthorizationInterceptor.Extensions.HybridCache/AuthorizationInterceptor.Extensions.HybridCache.csproj @@ -3,7 +3,7 @@ net8.0;net9.0 - + diff --git a/src/AuthorizationInterceptor/Extensions/HttpClientBuilderExtensions.cs b/src/AuthorizationInterceptor/Extensions/HttpClientBuilderExtensions.cs index b3026be..2dfa6af 100644 --- a/src/AuthorizationInterceptor/Extensions/HttpClientBuilderExtensions.cs +++ b/src/AuthorizationInterceptor/Extensions/HttpClientBuilderExtensions.cs @@ -35,6 +35,29 @@ public static IHttpClientBuilder AddAuthorizationInterceptorHandler(this IHtt return builder; } + /// + /// Init a new authorization interceptor handler configuration for IHttpClientBuilder + /// + /// + /// A delegate that provides an implementation of using the given . + /// Configuration options of Authorization interceptor. + /// Returns + public static IHttpClientBuilder AddAuthorizationInterceptorHandler(this IHttpClientBuilder builder, Func authHandlerImpl, Action? options = null) + { + ArgumentNullException.ThrowIfNull(authHandlerImpl); + + var optionsInstance = RequireOptions(options); + builder.AddHttpMessageHandler(provider => new AuthorizationInterceptorHandler( + builder.Name, + optionsInstance.UnauthenticatedPredicate, + authHandlerImpl.Invoke(provider), + CreateStrategy(provider, builder, optionsInstance.Interceptors), + provider.GetRequiredService() + )); + + return builder; + } + private static AuthorizationInterceptorOptions RequireOptions(Action? options) { var optionsInstance = new AuthorizationInterceptorOptions(); diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 7e38d3c..646af5a 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,7 +3,7 @@ 13 enable enable - 5.1.0 + 5.2.0 net6.0;net7.0;net8.0;net9.0 Authorization Interceptor Adolfok3 @@ -42,7 +42,7 @@ - + diff --git a/tests/AuthorizationInterceptor.Tests/Extensions/HttpClientBuilderExtensionsTests.cs b/tests/AuthorizationInterceptor.Tests/Extensions/HttpClientBuilderExtensionsTests.cs index 79b5deb..ae7229c 100644 --- a/tests/AuthorizationInterceptor.Tests/Extensions/HttpClientBuilderExtensionsTests.cs +++ b/tests/AuthorizationInterceptor.Tests/Extensions/HttpClientBuilderExtensionsTests.cs @@ -41,8 +41,6 @@ public void AddAuthorizationInterceptorHandler_WithOptions_ShouldExecuteSuccessf Assert.Null(Record.Exception(act)); } - - [Fact] public void AddAuthorizationInterceptorHandler_WithOptions_ShouldBuildServiceProviderSucessfully() { @@ -62,4 +60,37 @@ public void AddAuthorizationInterceptorHandler_WithOptions_ShouldBuildServicePro // Assert Assert.Null(Record.Exception(act)); } + + [Fact] + public void AddAuthorizationInterceptorHandler_WithNullAuthHandler_ShouldThrowsArgumentNullException() + { + // Arrange + var services = new ServiceCollection(); + + // Act + IHttpClientBuilder act() => services.AddHttpClient("Test").AddAuthorizationInterceptorHandler(authHandlerImpl: null); + + // Assert + Assert.Throws((Func)act); + } + + [Fact] + public void AddAuthorizationInterceptorHandler_WithOptionsAndAuthHandler_ShouldBuildServiceProviderSucessfully() + { + // Arrange + var services = new ServiceCollection(); + services.AddHttpClient("Test") + .AddAuthorizationInterceptorHandler((provider) => ActivatorUtilities.CreateInstance(provider), options => + { + options.UseCustomInterceptor(func => func.AddSingleton()); + }); + + // Act + var provider = services.BuildServiceProvider(); + var httpClientFactory = provider.GetRequiredService(); + var act = () => httpClientFactory.CreateClient("Test"); + + // Assert + Assert.Null(Record.Exception(act)); + } }