Another.OAuth2.ClientUtility.ClientCredentialsFlow 1.0.0

dotnet add package Another.OAuth2.ClientUtility.ClientCredentialsFlow --version 1.0.0
                    
NuGet\Install-Package Another.OAuth2.ClientUtility.ClientCredentialsFlow -Version 1.0.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="Another.OAuth2.ClientUtility.ClientCredentialsFlow" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Another.OAuth2.ClientUtility.ClientCredentialsFlow" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Another.OAuth2.ClientUtility.ClientCredentialsFlow" />
                    
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 Another.OAuth2.ClientUtility.ClientCredentialsFlow --version 1.0.0
                    
#r "nuget: Another.OAuth2.ClientUtility.ClientCredentialsFlow, 1.0.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 Another.OAuth2.ClientUtility.ClientCredentialsFlow@1.0.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=Another.OAuth2.ClientUtility.ClientCredentialsFlow&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Another.OAuth2.ClientUtility.ClientCredentialsFlow&version=1.0.0
                    
Install as a Cake Tool

Another OAuth2 Client Credentials Utility

A modular .NET 8 library that provide an OAuth2 client credentials token acquisition, in-memory caching and HttpClient bearer injection

  • Automatic access token retrieval and refresh.
  • Caching of tokens (in-memory by default).
  • Plug-in architecture for token storage (e.g. future Redis) implementing the IAccessTokenCache interface.
  • Simple service registration via extension methods.
  • Seamless bearer injection using a delegating handler.

You can reuse a named HttpClient across your application, the first outbound request triggers token acquisition, subsequent requests reuse the cached token until refresh is required.

Why this library exists

This project was born from a practical limitation encountered when integrating with an OAuth 2.0 authorization server that did not expose a valid OpenID Connect Discovery document (the .well-known/openid-configuration endpoint).

Because of this missing endpoint, it wasn’t possible to use MSAL.NET - Microsoft’s official library - even though the latest versions of MSAL now support the Client Credentials flow for non-Azure OAuth 2.0 providers. This library therefore provides a lightweight and fully configurable alternative, allowing you to:

  • Work with any OAuth 2.0 server that supports the Client Credentials flow, even without OpenID Connect metadata
  • Plug in your own endpoints for token acquisition
  • Keep a clean separation between token management, caching, and HTTP request logic
  • Is possible to extend the OAuth 2.0

Use it when your identity provider doesn’t fully implement OpenID Connect discovery or when you simply prefer a minimal, dependency-free solution focused on server-to-server authentication.

Extensibility

Although this library currently focuses on the Client Credentials flow, its architecture was designed with extensibility in mind.
You can easily extend it to support additional OAuth 2.0 grant types or custom authentication mechanisms by:

  • Implementing your own IAccessTokenProvider to handle alternative flows (e.g. Resource Owner Password, JWT Bearer, or Device Code)
  • Reusing the same caching and injection infrastructure already in place
  • Registering your provider through the existing dependency injection extensions

This makes the library a flexible foundation for building modular authentication clients that can adapt to different OAuth 2.0 scenarios.

Contents

NuGet Package

Install the package via NuGet Package Manager Console:

dotnet add package Another.OAuth2.ClientUtility.ClientCredentialsFlow.NuGet

The package includes:

  • In-memory token caching by default.
  • Automatic token refresh before expiration.
  • HttpClient integration via delegating handler.
  • Extensible architecture for custom token providers and cache implementations.

Projects in this repository

Project Purpose
Common Shared abstractions, option models, token models.
BusinessLogic Token provider + delegating handler + DI extensions.
DataAccess Token manager (internal) + cache abstraction + in-memory cache.
ConsoleAppSample Example usage showing a protected API call.
ClientCredentialsFlow.NuGet The NuGet package project that bundles the above.

Quick Start

  1. Prepare configuration (appsettings.json):
{
"OAuthClients":{
   "SampleApi":{
      "TokenEndpoint":"https://demo.duendesoftware.com/connect/token",
      "ClientId":"m2m",
      "ClientSecret":"secret",
      "Scope":"api",
      "RefreshBeforeExpiration":"00:00:05"
   }
},
"ProtectedApis":{
   "SampleApi":{
      "BaseUrl":"https://demo.duendesoftware.com/"
   }
}
}
  1. Register services in Program.cs:
services.AddClientCredentialsHttpClient( "SampleApi", configuration.GetSection("OAuthClients:SampleApi")) .ConfigureHttpClient((sp, client) => { var baseUrl = configuration.GetValue<string>("ProtectedApis:SampleApi:BaseUrl"); client.BaseAddress = new Uri(baseUrl); });
  1. Inject and use HttpClient:
public class MyService { 

private readonly HttpClient _httpClient; 

public MyService(HttpClient httpClient) { _httpClient = httpClient; } 

public async Task CallApiAsync() { 

    var response = await _httpClient.GetAsync("api/data"); 
    response.EnsureSuccessStatusCode(); 

    var content = await response.Content.ReadAsStringAsync(); 
    Console.WriteLine(content); 
 } 
}
  1. Run your application, the first API call will automatically handle token acquisition and injection.

Configuration

ClientCredentialsOptions (named options per client):

  • TokenEndpoint (required) – full token endpoint URL (e.g. https://demo.duendesoftware.com/connect/token)
  • ClientId, ClientSecret (required)
  • Scope (optional) – space-delimited or single scope
  • Audience (optional)
  • AdditionalBodyParameters / AdditionalHeaders (optional dictionaries)
  • RefreshBeforeExpiration – subtract time from token expiry to refresh early
  • Timeout – per token request

Protected API settings (SampleClientSettings in sample) supply base URL only.

Sample Console App

Entry point: SampleConsoleRunner:

var client = _httpClientFactory.CreateClient(SampleConsoleRunner.SampleClientName); 
var json = await client.GetStringAsync("api/test"); 

_logger.LogInformation("Protected API response: {Response}", json);

How Automatic Token Injection Works

  1. You register a named HttpClient via AddClientCredentialsHttpClient.
  2. The extension:
    • Adds the token provider (for contacting the OAuth token endpoint).
    • Registers a keyed internal token manager (one per client name).
    • Attaches ClientCredentialsDelegatingHandler.
  3. On first outbound request:
    • Handler sees no Authorization header.
    • Requests a token via the manager & provider.
    • Injects Authorization: Bearer <token>.
  4. Subsequent requests reuse cached token until near expiry.
  5. On expiry or pre-refresh moment, a new token is fetched (single-threaded via semaphore).

Multiple Clients

Register multiple:

services.AddClientCredentialsHttpClient("CatalogApi", config.GetSection("OAuthClients:CatalogApi")) .ConfigureHttpClient((_, c) => c.BaseAddress = new Uri(config["ProtectedApis:CatalogApi:BaseUrl"]));
services.AddClientCredentialsHttpClient("ReportsApi", config.GetSection("OAuthClients:ReportsApi")) .ConfigureHttpClient((_, c) => c.BaseAddress = new Uri(config["ProtectedApis:ReportsApi:BaseUrl"]));

Use via named HttpClient:

var catalogClient = _httpClientFactory.CreateClient("CatalogApi");
var reportsClient = _httpClientFactory.CreateClient("ReportsApi");

Each client has isolated options, cache, and refresh lifecycle.

Redis / Distributed Cache Extension (Not implemented, this is an example on how to use third parties caching systems)

Introduce an abstraction already defined:

public interface IAccessTokenCache { Task<AccessToken?> GetAsync(string clientName, CancellationToken ct = default); Task SetAsync(string clientName, AccessToken token, CancellationToken ct = default); }

Create a Redis implementation (example):

public sealed class RedisAccessTokenCache : IAccessTokenCache { private readonly IConnectionMultiplexer _redis; public RedisAccessTokenCache(IConnectionMultiplexer redis) => _redis = redis;

public async Task<AccessToken?> GetAsync(string clientName, CancellationToken ct)
{
    var db = _redis.GetDatabase();
    var raw = await db.StringGetAsync($"oauth:token:{clientName}");
    if (raw.IsNullOrEmpty) return null;
    var parts = raw.ToString().Split('|', 2);
    return parts.Length == 2 && DateTimeOffset.TryParse(parts[1], out var exp)
        ? new AccessToken(parts[0], exp)
        : null;
}

public async Task SetAsync(string clientName, AccessToken token, CancellationToken ct)
{
    var db = _redis.GetDatabase();
    var value = $"{token.Value}|{token.ExpiresAt:O}";
    var ttl = token.ExpiresAt - DateTimeOffset.UtcNow;
    await db.StringSetAsync($"oauth:token:{clientName}", value, ttl > TimeSpan.Zero ? ttl : TimeSpan.FromMinutes(5));
}
}

Wire it in place of the memory cache:

services.AddSingleton<IAccessTokenCache, RedisAccessTokenCache>();

Done, whitout any change to handlers or consumers.

Extensibility Points

Component Interface Replace When
Token retrieval IClientCredentialsTokenProvider Custom auth server logic / mTLS
Caching IAccessTokenCache Distributed (Redis) / encryption requirements
Delegating handler ClientCredentialsDelegatingHandler Advanced header logic / tracing
Options ClientCredentialsOptions Additional OAuth parameters

You can also add a proactive refresh background service if you need zero latency on first post-expiration call (optional).

Diagnostics & Logging

Log levels:

  • Trace: token reuse / header injection decisions.
  • Debug: token request boundaries.
  • Error: token endpoint failures.

Security Considerations

  • Store ClientSecret securely (user secrets, environment variables, Azure Key Vault).
  • Avoid logging the full token; truncate if needed.
  • Prefer HTTPS for both token and resource endpoints (enforced by Uri usage).
  • Rotate client secrets periodically.
  • If using distributed cache, consider encrypting access tokens at rest.

Troubleshooting

Issue Cause Resolution
404 calling /api/test Wrong BaseAddress or path Verify BaseUrl ends with / and call relative "api/test"
401 Unauthorized Invalid client credentials / scope mismatch Check ClientId, ClientSecret, Scope in config
Token not refreshed RefreshBeforeExpiration zero or handler skipped Do not set your own Authorization header; rely on handler
Extension method missing Missing project reference or using Add reference & using ...BusinessLogic.Extensions
Multiple token requests close together Manager not shared Ensure keyed registration was used, or factory exists

Roadmap / Ideas / Contributions needed

  • Adding Unit Test coverage.
  • Proactive token refresh service that refreshes before expiry in background.
  • Metrics (events: token requested / reused / expired).
  • Polly integration for transient token endpoint retry logic, because now it fails immediately on error responses, which may be too harsh for some scenarios, maybe is better to retry a few times.
  • Redis / Memory hybrid fallback cache (try Redis, fallback to memory).
  • Find a way that makes third party cache easier to integrate with minimal code (e.g. via NuGet).

License

See LICENSE file.

Disclaimer

The sample uses the public Duende demo (https://demo.duendesoftware.com), intended for development only. Do not rely on demo credentials or endpoints in production.


Feedback or contributions are super-welcome, feel free to open issues or pull requests. Happy coding 😊

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 was computed.  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 was computed.  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
1.0.0 194 10/29/2025