Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to this project will be documented in this file.

## [6.0.1] - 2026-06-16

### Fixed

- Resolved `IHttpContextAccessor` directly in the authorization handler instead of creating a runtime service scope for cache key building.
- Registered `IHttpContextAccessor` for all `AddAuthorizationInterceptorHandler` overloads so `CacheKeyBuilder` works consistently.

## [6.0.0] - 2026-06-15

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using AuthorizationInterceptor.Handlers;
using AuthorizationInterceptor.Options;
using AuthorizationInterceptor.Strategies;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -32,7 +33,7 @@ public static IHttpClientBuilder AddAuthorizationInterceptorHandler<T>(this IHtt
CreateAuthenticationHandler<T>(provider),
CreateStrategy(provider, builder, optionsInstance.Interceptors),
provider.GetRequiredService<ILoggerFactory>(),
provider.GetRequiredService<IServiceScopeFactory>(),
provider.GetRequiredService<IHttpContextAccessor>(),
optionsInstance.CacheKeyBuilder
));

Expand All @@ -50,14 +51,16 @@ public static IHttpClientBuilder AddAuthorizationInterceptorHandler(this IHttpCl
{
ArgumentNullException.ThrowIfNull(authHandlerImpl);

builder.Services.AddHttpContextAccessor();

var optionsInstance = RequireOptions(options);
builder.AddHttpMessageHandler(provider => new AuthorizationInterceptorHandler(
builder.Name,
optionsInstance.UnauthenticatedPredicate,
authHandlerImpl.Invoke(provider),
CreateStrategy(provider, builder, optionsInstance.Interceptors),
provider.GetRequiredService<ILoggerFactory>(),
provider.GetRequiredService<IServiceScopeFactory>(),
provider.GetRequiredService<IHttpContextAccessor>(),
optionsInstance.CacheKeyBuilder
));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using AuthorizationInterceptor.Strategies;
using AuthorizationInterceptor.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace AuthorizationInterceptor.Handlers;
Expand All @@ -15,17 +14,17 @@ internal class AuthorizationInterceptorHandler : DelegatingHandler
private readonly IAuthenticationHandler _authenticationHandler;
private readonly IAuthorizationInterceptorStrategy _strategy;
private readonly ILogger _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly Func<IHttpContextAccessor, string?>? _cacheKeyBuilder;

public AuthorizationInterceptorHandler(string name, Func<HttpResponseMessage, bool> unauthenticatedPredicate, IAuthenticationHandler authenticationHandler, IAuthorizationInterceptorStrategy strategy, ILoggerFactory loggerFactory, IServiceScopeFactory serviceScopeFactory, Func<IHttpContextAccessor, string?>? cacheKeyBuilder = null)
public AuthorizationInterceptorHandler(string name, Func<HttpResponseMessage, bool> unauthenticatedPredicate, IAuthenticationHandler authenticationHandler, IAuthorizationInterceptorStrategy strategy, ILoggerFactory loggerFactory, IHttpContextAccessor httpContextAccessor, Func<IHttpContextAccessor, string?>? cacheKeyBuilder = null)
{
_name = name;
_strategy = strategy;
_authenticationHandler = authenticationHandler;
_unauthenticatedPredicate = unauthenticatedPredicate;
_logger = loggerFactory.CreateLogger("AuthorizationInterceptorHandler");
_serviceScopeFactory = serviceScopeFactory;
_httpContextAccessor = httpContextAccessor;
_cacheKeyBuilder = cacheKeyBuilder;
}

Expand Down Expand Up @@ -74,9 +73,7 @@ private async Task<HttpResponseMessage> SendWithInterceptorAsync(HttpRequestMess
if (_cacheKeyBuilder is null)
return null;

using var scope = _serviceScopeFactory.CreateScope();
var httpContextAccessor = scope.ServiceProvider.GetRequiredService<IHttpContextAccessor>();
return _cacheKeyBuilder.Invoke(httpContextAccessor);
return _cacheKeyBuilder.Invoke(_httpContextAccessor);
}

private HttpRequestMessage AddHeaders(HttpRequestMessage request, AuthorizationHeaders headers, string? cacheKeySuffix)
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>6.0.0</Version>
<Version>6.0.1</Version>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<Title>Authorization Interceptor</Title>
<Authors>Adolfok3</Authors>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using AuthorizationInterceptor.Extensions;
using AuthorizationInterceptor.Tests.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace AuthorizationInterceptor.Tests.Extensions;
Expand Down Expand Up @@ -68,7 +69,7 @@
var services = new ServiceCollection();

// Act
IHttpClientBuilder act() => services.AddHttpClient("Test").AddAuthorizationInterceptorHandler(authHandlerImpl: null);

Check warning on line 72 in tests/AuthorizationInterceptor.Tests/Extensions/HttpClientBuilderExtensionsTests.cs

View workflow job for this annotation

GitHub Actions / code-validation

Cannot convert null literal to non-nullable reference type.

Check warning on line 72 in tests/AuthorizationInterceptor.Tests/Extensions/HttpClientBuilderExtensionsTests.cs

View workflow job for this annotation

GitHub Actions / code-validation

Cannot convert null literal to non-nullable reference type.

// Assert
Assert.Throws<ArgumentNullException>((Func<IHttpClientBuilder>)act);
Expand All @@ -93,4 +94,21 @@
// Assert
Assert.Null(Record.Exception(act));
}

[Fact]
public void AddAuthorizationInterceptorHandler_WithAuthHandler_ShouldRegisterHttpContextAccessor()
{
// Arrange
var services = new ServiceCollection();

// Act
services.AddHttpClient("Test")
.AddAuthorizationInterceptorHandler((provider) => ActivatorUtilities.CreateInstance<MockAuthorizationInterceptorAuthenticationHandler>(provider), options =>
{
options.CacheKeyBuilder = accessor => accessor.HttpContext?.Request.Headers["x-mycustom-header"].ToString();
});

// Assert
Assert.Contains(services, descriptor => descriptor.ServiceType == typeof(IHttpContextAccessor));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,13 @@ public class AuthorizationInterceptorHandlerTests
private readonly IAuthorizationInterceptorStrategy _strategy;
private readonly IAuthenticationHandler _authenticationHandler;
private readonly HttpClient _client;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly Func<IHttpContextAccessor, string?>? _cacheKeyBuilder;
private readonly IHttpContextAccessor _httpContextAccessor;

public AuthorizationInterceptorHandlerTests()
{
_logger = Substitute.For<ILogger>();
_logger.IsEnabled(LogLevel.Debug).Returns(true);
_serviceScopeFactory = Substitute.For<IServiceScopeFactory>();
_cacheKeyBuilder = Substitute.For<Func<IHttpContextAccessor, string?>?>();

var loggerFactory = Substitute.For<ILoggerFactory>();
Expand All @@ -39,19 +37,13 @@ public AuthorizationInterceptorHandlerTests()
});
httpContext.Request.Returns(httpRequest);
_httpContextAccessor.HttpContext.Returns(httpContext);
var serviceScope = Substitute.For<IServiceScope>();
var services = new ServiceCollection();
services.AddSingleton(_httpContextAccessor);
var provider = services.BuildServiceProvider();
serviceScope.ServiceProvider.Returns(provider);
_serviceScopeFactory.CreateScope().Returns(serviceScope);
_cacheKeyBuilder!.Invoke(_httpContextAccessor).Returns((_) => null);

Func<HttpResponseMessage, bool> func = f => f.StatusCode == System.Net.HttpStatusCode.Unauthorized;
_strategy = Substitute.For<IAuthorizationInterceptorStrategy>();
_authenticationHandler = Substitute.For<IAuthenticationHandler>();

var handler = new AuthorizationInterceptorHandler("test", func, _authenticationHandler, _strategy, loggerFactory, _serviceScopeFactory, _cacheKeyBuilder);
var handler = new AuthorizationInterceptorHandler("test", func, _authenticationHandler, _strategy, loggerFactory, _httpContextAccessor, _cacheKeyBuilder);
handler.InnerHandler = new MockAuthorizationInterceptorHandler();
_client = new HttpClient(handler);
}
Expand Down Expand Up @@ -97,7 +89,7 @@ public async Task SendAsync_WithoutCacheKeyBuilder_ShouldUseDefaultCacheKey()
_authenticationHandler,
_strategy,
loggerFactory,
_serviceScopeFactory);
_httpContextAccessor);
handler.InnerHandler = new MockAuthorizationInterceptorHandler();

using var client = new HttpClient(handler);
Expand All @@ -108,7 +100,6 @@ public async Task SendAsync_WithoutCacheKeyBuilder_ShouldUseDefaultCacheKey()

//Assert
Assert.True(response.IsSuccessStatusCode);
_serviceScopeFactory.DidNotReceive().CreateScope();
await _strategy.Received(1).GetHeadersAsync("test", _authenticationHandler, null, Arg.Any<CancellationToken>());
}

Expand Down
Loading