AuthorizationInterceptor 6.1.0

dotnet add package AuthorizationInterceptor --version 6.1.0
                    
NuGet\Install-Package AuthorizationInterceptor -Version 6.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AuthorizationInterceptor" Version="6.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AuthorizationInterceptor" Version="6.1.0" />
                    
Directory.Packages.props
<PackageReference Include="AuthorizationInterceptor" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AuthorizationInterceptor --version 6.1.0
                    
#r "nuget: AuthorizationInterceptor, 6.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AuthorizationInterceptor@6.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AuthorizationInterceptor&version=6.1.0
                    
Install as a Cake Addin
#tool nuget:?package=AuthorizationInterceptor&version=6.1.0
                    
Install as a Cake Tool

AuthorizationInterceptor Icon

Authorization Interceptor

A lightweight .NET library that automatically manages HTTP authentication headers for HttpClient. When a request receives a 401 response, the interceptor re-authenticates and retries with fresh headers — no manual token management required.

GitHub Actions License: MIT Codecov NuGet Version .NET Support

Features

  • Automatic retry on auth failure — intercepts 401 responses and retries with fresh headers
  • OAuth2 refresh token support — reuse existing tokens via RefreshToken flow
  • Custom header support — return any key-value authorization headers
  • Multiple cache backends — in-memory, distributed (Redis/NCache), or hybrid caching
  • Deduplicated authentication — concurrent requests share a single authentication call per cache key
  • Extensible interceptor chain — compose your own caching and logging strategies
  • Multi-target framework support — .NET 8+

Quick Start

Install the core package:

dotnet add package AuthorizationInterceptor

Step 1: Implement authentication logic

Create a class that implements IAuthenticationHandler:

public class TargetApiAuth : IAuthenticationHandler
{
    private readonly HttpClient _client;

    public TargetApiAuth(HttpClient client)
    {
        _client = client;
    }

    public async ValueTask<AuthorizationHeaders?> AuthenticateAsync(
        AuthorizationHeaders? expiredHeaders, CancellationToken ct)
    {
        if (expiredHeaders == null)
        {
            // First login — request a fresh token
            var response = await _client.PostAsync("auth", content: null, ct);
        }
        else
        {
            // Token expired — refresh it using the existing refresh token
            // This step is only applicable to APIs integrating with OAuth refresh tokens
            var refreshToken = expiredHeaders.OAuthHeaders!.RefreshToken;
            var response = await _client.PostAsync($"refresh?refresh={refreshToken}", content: null, ct);
        }

        var json = await response.Content.ReadAsStringAsync(ct);
        var tokens = JsonSerializer.Deserialize<UserTokens>(json)!;

        return new OAuthHeaders(
            accessToken: tokens.AccessToken,
            tokenType: tokens.TokenType,
            expiresIn: tokens.ExpiresIn,
            refreshToken: tokens.RefreshToken,
            refreshTokenExpiresIn: tokens.RefreshAccessTokenExpiresIn);
    }
}

public record UserTokens(string AccessToken, string TokenType, int ExpiresIn, string RefreshToken, int RefreshAccessTokenExpiresIn);

Step 2: Register the handler

builder.Services.AddHttpClient("TargetApi")
    .AddAuthorizationInterceptorHandler<TargetApiAuth>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://targetapi.com"));

That's it. Calls to this HttpClient will automatically retry with fresh authorization headers when a 401 is received.

Caching & Interceptors

By default, without any cache interceptor, a new access token is generated on every expiration. For production deployments, use one of the cache interceptors below to avoid redundant authentication calls.

Available Packages

Package Use case
AuthorizationInterceptor.Extensions.MemoryCache Local in-memory caching — good for single-instance apps
AuthorizationInterceptor.Extensions.DistributedCache Distributed caching (Redis, NCache, etc.) — for multi-instance deployments
AuthorizationInterceptor.Extensions.HybridCache Memory + distributed cache combined — recommended for production
builder.Services.AddHttpClient("TargetApi")
    .AddAuthorizationInterceptorHandler<TargetApiAuth>(options =>
    {
        options.UseHybridCacheInterceptor(); // memory → distributed → auth handler
    })
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://targetapi.com"));

This uses an in-memory cache first (fastest), falls back to distributed cache, then calls the authentication handler only when no cached token exists. Instances share the same token through the distributed layer, which avoids redundant login calls.

Concurrency

When several requests need headers at the same time and none are cached, only one of them calls the authentication handler. The others wait, then reuse whatever it stored in the interceptors. The same applies after an unauthenticated response: a request only refreshes the token if no one else has already replaced the one that was rejected.

This deduplication is scoped to the process, and to the HttpClient name plus the CacheKeyBuilder suffix — different cache keys never block each other. Across instances it is the shared cache, not a lock, that keeps authentication calls down: with a cold distributed cache, two instances can still authenticate at the same time. If your provider invalidates the previous token on every issuance, keep that in mind when scaling out.

Deduplication requires at least one cache interceptor. Without one there is nothing to share, so every request authenticates on its own.

Options & Customization

Retry on 403 as well as 401

Some APIs return 403 instead of 401 when tokens expire:

builder.Services.AddHttpClient("TargetApi")
    .AddAuthorizationInterceptorHandler<TargetApiAuth>(options =>
    {
        options.UnauthenticatedPredicate = response =>
            response.StatusCode is HttpStatusCode.Forbidden or HttpStatusCode.Unauthorized;
    });

Multiple HttpClient instances with different auth data

When you need to pass extra dependencies into your authentication handler:

builder.Services.AddHttpClient("TargetApi")
    .AddAuthorizationInterceptorHandler((sp) =>
        ActivatorUtilities.CreateInstance<TargetApiAuth>(sp, someOtherDependency));

Per-request cache keys

When the same HttpClient is used for the same target API, but authorization headers must be cached separately by a request value, configure CacheKeyBuilder.

This is useful when a single integration can authenticate on behalf of different users, tenants, stores, organizations, or any other request-scoped identifier. The value returned by CacheKeyBuilder is appended to the cache key used by the configured cache interceptor.

Example using the authenticated user:

builder.Services.AddHttpClient("TargetApi")
    .AddAuthorizationInterceptorHandler<TargetApiAuth>(options =>
    {
        options.UseHybridCacheInterceptor();
        options.CacheKeyBuilder = accessor =>
            accessor.HttpContext?.User.FindFirst("sub")?.Value;
    });

Example using a route or query value:

builder.Services.AddHttpClient("TargetApi")
    .AddAuthorizationInterceptorHandler<TargetApiAuth>(options =>
    {
        options.UseDistributedCacheInterceptor();
        options.CacheKeyBuilder = accessor =>
        {
            var httpContext = accessor.HttpContext;
            var storeId = httpContext?.Request.RouteValues["storeId"]?.ToString()
                ?? httpContext?.Request.Query["storeId"].ToString();

            return string.IsNullOrWhiteSpace(storeId) ? null : storeId;
        };
    });

With this configuration, requests using the same HttpClient but different HttpContext.Request values will not share the same cached authorization headers.

If CacheKeyBuilder returns null or an empty value, the interceptor uses the default cache key for the HttpClient name.

Interceptors receive the two halves already combined, as a single key parameter: the HttpClient name on its own when no suffix applies, or {name}_{suffix} when one does.

Custom interceptors

Add custom logic steps to the interceptor chain:

builder.Services.AddHttpClient("TargetApi")
    .AddAuthorizationInterceptorHandler<TargetApiAuth>(options =>
    {
        options.UseMemoryCacheInterceptor();
        options.UseCustomInterceptor<MyLoggingInterceptor>();
    });

Implement IAuthorizationInterceptor:

public class MyLoggingInterceptor : IAuthorizationInterceptor
{
    public ValueTask<AuthorizationHeaders?> GetHeadersAsync(
        string key, CancellationToken ct)
        => new(new AuthorizationHeaders());

    public ValueTask UpdateHeadersAsync(
        string key, AuthorizationHeaders? expiredHeaders,
        AuthorizationHeaders? newHeaders, CancellationToken ct)
    {
        // Log or transform headers between cache and auth handler
        return default;
    }
}

The interceptor chain becomes: MemoryCache → MyLoggingInterceptor → AuthHandler → MyLoggingInterceptor → MemoryCache. Build your own cache backend by targeting AuthorizationInterceptor.Extensions.Abstractions.

Sample Applications

Run a working demo with a mock API endpoint:

cd samples
dotnet run --project TargetApi   # starts mock auth server on :5001
# in another terminal:
dotnet run --project SourceApi   # calls the mock API with interceptor enabled

Source: Samples

License

This project is licensed under the MIT License. See LICENSE.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
6.1.0 58 8/10/2026
6.0.1 256 6/16/2026
6.0.0 113 6/15/2026
5.4.1 16,961 2/12/2026
5.4.0 136 1/19/2026
5.2.0 15,969 9/18/2025
5.0.1 231 9/12/2025
5.0.0 256 3/18/2025
5.0.0-preview-1 225 3/17/2025
5.0.0-beta 226 3/17/2025
4.0.0 191 2/28/2025
2.2.0 234 11/12/2024
2.1.2 170 11/6/2024
2.1.0 460 4/27/2024
2.0.0 260 4/24/2024 2.0.0 is deprecated because it is no longer maintained.
2.0.0-beta1 255 4/24/2024 2.0.0-beta1 is deprecated because it is no longer maintained.
1.2.1-beta1 348 4/8/2024 1.2.1-beta1 is deprecated because it is no longer maintained.
1.1.2-beta1 233 4/4/2024 1.1.2-beta1 is deprecated because it is no longer maintained.
1.1.1-beta1 213 4/4/2024 1.1.1-beta1 is deprecated because it is no longer maintained.
1.0.0 248 4/9/2024 1.0.0 is deprecated because it is no longer maintained.
Loading failed