Customizing Client Credentials Token Management
The most common way to use access token management is for machine-to-machine communication. However, you may want to customize certain aspects of it.
Client Options
Section titled “Client Options”You can add token client definitions to your host while configuring the DotNet service provider, e.g.:
Access Token Management 4.0 and later
services.AddClientCredentialsTokenManagement() .AddClient("invoices", client => { client.TokenEndpoint = new Uri("https://sts.company.com/connect/token");
client.ClientId = ClientId.Parse("4a632e2e-0466-4e5a-a094-0455c6105f57"); client.ClientSecret = ClientSecret.Parse("e8ae294a-d5f3-4907-88fa-c83b3546b70c"); client.ClientCredentialStyle = ClientCredentialStyle.AuthorizationHeader;
client.Scope = Scope.Parse("list"); client.Resource = Resource.Parse("urn:invoices"); });Access Token Management before 4.0
services.AddClientCredentialsTokenManagement() .AddClient("invoices", client => { client.TokenEndpoint = "https://sts.company.com/connect/token";
client.ClientId = "4a632e2e-0466-4e5a-a094-0455c6105f57"; client.ClientSecret = "e8ae294a-d5f3-4907-88fa-c83b3546b70c"; client.ClientCredentialStyle = ClientCredentialStyle.AuthorizationHeader;
client.Scope = "list"; client.Resource = "urn:invoices"; });You can set the following options:
TokenEndpoint- URL of the OAuth token endpoint where this token client requests tokens fromClientId- client IDClientSecret- client secret (if a shared secret is used)ClientCredentialStyle- Specifies how the client ID / secret is sent to the token endpoint. Options are using the authorization header, or POST body values (defaults to header)Scope- the requested scope of access (if any)Resource- the resource indicator (if any)
Internally the standard .NET options system is used to register the configuration. This means you can also register clients like this:
Access Token Management 4.0 and later
services.Configure<ClientCredentialsClient>("invoices", client =>{ client.TokenEndpoint = new Uri("https://sts.company.com/connect/token");
client.ClientId = ClientId.Parse("4a632e2e-0466-4e5a-a094-0455c6105f57"); client.ClientSecret = ClientSecret.Parse("e8ae294a-d5f3-4907-88fa-c83b3546b70c");
client.Scope = Scope.Parse("list"); client.Resource = Resource.Parse("urn:invoices");});Access Token Management before 4.0
services.Configure<ClientCredentialsClient>("invoices", client =>{ client.TokenEndpoint = "https://sts.company.com/connect/token";
client.ClientId = "4a632e2e-0466-4e5a-a094-0455c6105f57"; client.ClientSecret = "e8ae294a-d5f3-4907-88fa-c83b3546b70c";
client.Scope = "list"; client.Resource = "urn:invoices";});Or use the IConfigureNamedOptions if you need access to the ASP.NET Core service provider during registration, e.g.:
Access Token Management 4.0 and later
using Duende.AccessTokenManagement;using Duende.IdentityModel.Client;using Microsoft.Extensions.Options;
public class ClientCredentialsClientConfigureOptions(DiscoveryCache cache) : IConfigureNamedOptions<ClientCredentialsClient>{ public void Configure(string? name, ClientCredentialsClient options) { if (name == "invoices") { var disco = cache.GetAsync().GetAwaiter().GetResult();
options.TokenEndpoint = new Uri(disco.TokenEndpoint);
options.ClientId = ClientId.Parse("4a632e2e-0466-4e5a-a094-0455c6105f57"); options.ClientSecret = ClientSecret.Parse("e8ae294a-d5f3-4907-88fa-c83b3546b70c");
options.Scope = Scope.Parse("list"); options.Resource = Resource.Parse("urn:invoices"); } }
public void Configure(ClientCredentialsClient options) { // implement default configure Configure("", options); }}Access Token Management before 4.0
using Duende.AccessTokenManagement;using Duende.IdentityModel.Client;using Microsoft.Extensions.Options;
public class ClientCredentialsClientConfigureOptions(DiscoveryCache cache) : IConfigureNamedOptions<ClientCredentialsClient>{ public void Configure(string? name, ClientCredentialsClient options) { if (name == "invoices") { var disco = cache.GetAsync().GetAwaiter().GetResult();
options.TokenEndpoint = disco.TokenEndpoint;
options.ClientId = "4a632e2e-0466-4e5a-a094-0455c6105f57"; options.ClientSecret = "e8ae294a-d5f3-4907-88fa-c83b3546b70c";
options.Scope = "list"; options.Resource = "urn:invoices"; } }
public void Configure(ClientCredentialsClient options) { // implement default configure Configure("", options); }}You will also need to register the config options, for example:
services.AddClientCredentialsTokenManagement();
services.AddSingleton(new DiscoveryCache("https://sts.company.com"));services.AddSingleton<IConfigureOptions<ClientCredentialsClient>, ClientCredentialsClientConfigureOptions>();Backchannel Communication
Section titled “Backchannel Communication”By default, all backchannel communication will be done using a named client from the HTTP client factory. The name is Duende.AccessTokenManagement.BackChannelHttpClient which is also a constant called ClientCredentialsTokenManagementDefaults.BackChannelHttpClientName.
You can register your own HTTP client with the factory using the above name and thus provide your own custom HTTP client.
The client registration object has two additional properties to customize the HTTP client:
HttpClientName- if set, this HTTP client name from the factory will be used instead of the default oneHttpClient- allows setting an instance ofHttpClientto use. Will take precedence over a client name
Token caching
Section titled “Token caching”Access Token Management 4.0 and later
In V4, access tokens are cached using HybridCache.
Using remote caches
Section titled “Using remote caches”Hybrid cache is a 2 tier cache, with in-memory and remote capabilities. Hybrid cache automatically picks up any IDistributedCache implementation as it’s remote cache. See Distributed Caching in Asp.Net on more information on topic.
Injecting a custom cache
Section titled “Injecting a custom cache”By default, we use the default HybridCache implementation. You may want to inject a custom hybrid cache implementation, such as FusionCache.
You can do this either for the entire system:
services.AddSingleton<HybridCache>(new MyCustomCacheImplementation());Or only for Duende.AccessTokenManagement, by using Service Keys:
services.AddKeyedSingleton<HybridCache>(ServiceProviderKeys.ClientCredentialsTokenCache, new MyCustomCacheImplementation());Customizing cache keys
Section titled “Customizing cache keys”By default, cache keys are built up as follows:
{options.CacheKeyPrefix}::{client_name}::hashed({scope})::hashed({resource})
options.CacheKeyPrefixcan be configured using theClientCredentialsTokenManagementOptionsclient_nameis the name of the clientscopeis the scope parameter (if any) that’s used to request the access token.resourceis the resource parameter (if any) that’s used to request the access token.
The values of both the scope and resource hashed (MD5) to ensure that the cache key length is not unbounded.
You can implement your own cache key generator by implementing a custom IClientCredentialsCacheKeyGenerator and registering this to your service container. This is needed if you’re adding custom parameters to your TokenRequestParameters
Encrypting cache entries
Section titled “Encrypting cache entries”You may want to share a remote cache with other parts of the application or even with other applications. In that case, it may be wise to encrypt the access tokens in the remote cache. You can achieve this with a custom serializer.
// Explicitly register a serializer for the client credentials tokens with the hybrid cacheservices.AddHybridCache() .AddSerializer<ClientCredentialsToken, EncryptedHybridCacheSerializer>();
// This example uses data protection api. You'll want to configure this to suit your needsservices.AddDataProtection();
/// <summary>/// Example on how to implement a serializer that encrypts data using ASP.NET Core Data Protection./// </summary>public class EncryptedHybridCacheSerializer : IHybridCacheSerializer<ClientCredentialsToken>{ private readonly IDataProtector _protector;
public EncryptedHybridCacheSerializer(IDataProtectionProvider provider) { _protector = provider.CreateProtector("ClientCredentialsToken"); }
public ClientCredentialsToken Deserialize(ReadOnlySequence<byte> source) { // Convert the sequence to a byte array var buffer = source.ToArray(); // Unprotect (decrypt) the data var unprotected = _protector.Unprotect(buffer); // Deserialize the JSON payload return JsonSerializer.Deserialize<ClientCredentialsToken>(unprotected)!; }
public void Serialize(ClientCredentialsToken value, IBufferWriter<byte> target) { // Serialize the value to JSON var json = JsonSerializer.SerializeToUtf8Bytes(value); // Protect (encrypt) the data var protectedBytes = _protector.Protect(json); // Write to the buffer target.Write(protectedBytes); }}Access Token Management before 4.0
By default, tokens will be cached using the IDistributedCache abstraction in ASP.NET Core. You can either use the in-memory cache version, or a real distributed cache like Redis.
For development purposes, you can use the MemoryDistributedCache:
services.AddDistributedMemoryCache();Note that MemoryDistributedCache will be cleared whenever the process is restarted. It won’t be shared between multiple instances of your application in a load-balanced environment.
As a result, a new token will have to be obtained when you restart your application, and each instance will obtain a different token.
For production deployments, we recommend using a distributed cache.
The built-in cache in Duende.AccessTokenManagment uses two settings from the options, which apply with any IDistributedCache:
services.AddClientCredentialsTokenManagement(options => { options.CacheLifetimeBuffer = 60; options.CacheKeyPrefix = "Duende.AccessTokenManagement.Cache::"; });CacheLifetimeBuffer is a value in seconds that will be subtracted from the token lifetime, e.g. if a token is valid for one hour, it will be cached for 59 minutes only. The cache key prefix is used to construct the unique key for the cache item based on client name, requested scopes and resource.
Additional V4 Caching Options
Section titled “Additional V4 Caching Options”In V4, the ClientCredentialsTokenManagementOptions class has additional properties for fine-tuning cache behavior:
DefaultCacheLifetime- The default cache lifetime for the first token request when the actual expiration is not yet known. Defaults to 5 minutes.UseCacheAutoTuning- When enabled, the cache uses the actual token expiration time to set the cache entry lifetime. The first request usesDefaultCacheLifetimesince the expiration isn’t known yet. Defaults tofalse.LocalCacheExpiration- How long local (in-memory) cache entries are valid before being refreshed from the remote cache. Defaults to 1 minute. Set tonullto use the same expiration as the remote cache.NonceStoreKeyPrefix- Prefix used for storing DPoP nonces in the cache.
services.AddClientCredentialsTokenManagement(options => { options.CacheLifetimeBuffer = 60; options.CacheKeyPrefix = "Duende.AccessTokenManagement.Cache::";
// V4-specific options options.DefaultCacheLifetime = TimeSpan.FromMinutes(5); options.UseCacheAutoTuning = false; options.LocalCacheExpiration = TimeSpan.FromMinutes(1); });Access Token Management 4.3 and later
Per-Client Cache Settings
Section titled “Per-Client Cache Settings”The options on ClientCredentialsTokenManagementOptions apply to every client. When your clients request tokens from different authorization servers, you may need different cache settings per client. For example, you may want cache auto-tuning for one server but not for another.
Starting with v4.3, you can override the cache settings for a single client with ConfigureClientCache. Pass the name of the client and configure a ClientCredentialsClientCacheOptions:
services.AddClientCredentialsTokenManagement(options =>{ options.UseCacheAutoTuning = true;}).AddClient("partner-api", client =>{ client.TokenEndpoint = new Uri("https://sts.partner.com/connect/token"); client.ClientId = ClientId.Parse("partner-client"); client.ClientSecret = ClientSecret.Parse("secret");}).ConfigureClientCache("partner-api", cache =>{ cache.UseCacheAutoTuning = false; cache.DefaultCacheLifetime = TimeSpan.FromMinutes(2);});Each property overrides the ClientCredentialsTokenManagementOptions property with the same name, for this client only:
UseCacheAutoTuning- Overrides the globalUseCacheAutoTuning.DefaultCacheLifetime- Overrides the globalDefaultCacheLifetime.CacheLifetimeBuffer- Overrides the globalCacheLifetimeBuffer. The value is in seconds, the same as the global option.LocalCacheExpiration- Overrides the globalLocalCacheExpiration.
All properties are nullable and default to null. A property you leave null uses the global value, so a client without per-client settings behaves as before. The CacheLifetimeBuffer also decides when a cached token for this client counts as expired and gets renewed.
ClientCredentialsClientCacheOptions uses the .NET options system, with the client name as the options name. This means you can also bind the settings from configuration:
services.Configure<ClientCredentialsClientCacheOptions>( "partner-api", builder.Configuration.GetSection("TokenCache:PartnerApi"));The name must match the client name exactly. Nothing checks that a client with that name exists: settings for a name you never request a token for have no effect. The settings also apply to clients you don’t register with AddClient, for example when you use a custom IClientCredentialsTokenEndpoint.
Finally, you can also replace the caching implementation altogether by registering your own IClientCredentialsTokenCache.
Access Token Management 4.3 and later
Maximum Token Length
Section titled “Maximum Token Length”Token management rejects access tokens longer than ClientCredentialsTokenManagementOptions.MaxAccessTokenLength.
The default is AccessToken.MaxLength, 32768 characters. Raise it when your identity provider issues larger tokens, for
example access tokens with many claims:
services.AddClientCredentialsTokenManagement(options => { options.MaxAccessTokenLength = 64 * 1024; });The limit applies to the token endpoint response, before the token goes into the cache. A token over the limit makes
token management throw an InvalidOperationException that names the option, and the token is not cached. Tokens read
back from the cache are not checked again. The value must be greater than zero, or reading the options throws an
OptionsValidationException.
A higher limit increases memory use and the size of each cache entry.