Extensibility
The introspection handler exposes several extension points that let you change how tokens are retrieved from requests, how introspection results are cached, and how the handler is registered when you need multiple schemes.
Token Retrieval
Section titled “Token Retrieval”By default, the handler reads the bearer token from the Authorization header using TokenRetrieval.FromAuthorizationHeader().
You can change this by assigning a different strategy to OAuth2IntrospectionOptions.TokenRetriever.
The TokenRetrieval helper class provides two built-in strategies:
| Method | Default Parameter | Behavior |
|---|---|---|
TokenRetrieval.FromAuthorizationHeader(scheme) | scheme = "Bearer" | Reads the token from the Authorization header |
TokenRetrieval.FromQueryString(name) | name = "access_token" | Reads the token from the query string |
Reading from the Authorization Header (default)
Section titled “Reading from the Authorization Header (default)”using Duende.AspNetCore.Authentication.OAuth2Introspection.Infrastructure;
options.TokenRetriever = TokenRetrieval.FromAuthorizationHeader();// or with a custom scheme:options.TokenRetriever = TokenRetrieval.FromAuthorizationHeader("DPoP");Reading from the Query String
Section titled “Reading from the Query String”options.TokenRetriever = TokenRetrieval.FromQueryString();// or with a custom parameter name:options.TokenRetriever = TokenRetrieval.FromQueryString("token");Custom Token Retrieval
Section titled “Custom Token Retrieval”For scenarios where neither built-in strategy applies, for example, when reading a token from a cookie or a custom header,
you can assign any Func<HttpRequest, string?> delegate:
options.TokenRetriever = request =>{ // Try Authorization header first, fall back to query string var fromHeader = TokenRetrieval.FromAuthorizationHeader()(request); if (!string.IsNullOrEmpty(fromHeader)) { return fromHeader; }
return TokenRetrieval.FromQueryString()(request);};// Reading from a custom headeroptions.TokenRetriever = request => request.Headers["X-Api-Token"].FirstOrDefault();Custom Cache Key Generation
Section titled “Custom Cache Key Generation”Introspection results are cached by token. The default cache key is the configured CacheKeyPrefix
concatenated with the SHA-256 hash of the raw token, generated by CacheUtils.CacheKeyFromToken().
You can override this with the CacheKeyGenerator option to apply your own key format.
Default Key Behavior
Section titled “Default Key Behavior”using Duende.AspNetCore.Authentication.OAuth2Introspection.Infrastructure;
// This is the default — you do not need to set it explicitlyoptions.CacheKeyGenerator = CacheUtils.CacheKeyFromToken();
// Using a prefix to namespace keys in a shared cacheoptions.CacheKeyPrefix = "introspection:";Custom Key Generator
Section titled “Custom Key Generator”The delegate receives the OAuth2IntrospectionOptions instance and the raw token string, and must return a
string cache key:
options.CacheKeyGenerator = (opts, token) =>{ // Prefix with a tenant identifier from options var hash = Convert.ToHexString( System.Security.Cryptography.SHA256.HashData( System.Text.Encoding.UTF8.GetBytes(token)));
return $"{opts.CacheKeyPrefix}tenant-a:{hash}";};Controlling Cache Behavior with SetCacheEntryFlags
Section titled “Controlling Cache Behavior with SetCacheEntryFlags”The handler uses ASP.NET Core’s HybridCache, which maintains both an in-process (L1) cache and an optional
distributed (L2) cache. The SetCacheEntryFlags option controls which layers are written to.
| Flag | Effect |
|---|---|
HybridCacheEntryFlags.None (default) | Write to both L1 and L2 caches |
HybridCacheEntryFlags.DisableLocalCache | Skip the in-process L1 cache |
HybridCacheEntryFlags.DisableDistributedCache | Skip the distributed L2 cache |
HybridCacheEntryFlags.DisableLocalCache | HybridCacheEntryFlags.DisableDistributedCache | Disable caching entirely |
using Microsoft.Extensions.Caching.Hybrid;
// Disable the in-process cache, use only the distributed cacheoptions.SetCacheEntryFlags = HybridCacheEntryFlags.DisableLocalCache;
// Disable caching entirely (every request will call the introspection endpoint)options.SetCacheEntryFlags = HybridCacheEntryFlags.DisableLocalCache | HybridCacheEntryFlags.DisableDistributedCache;Custom Cache Implementation
Section titled “Custom Cache Implementation”By default the handler resolves a HybridCache from the DI container. You can replace the cache instance
used by the introspection handler without replacing the application-wide cache by registering a keyed service
using ServiceProviderKeys.IntrospectionCache.
using Duende.AspNetCore.Authentication.OAuth2Introspection;using Microsoft.Extensions.Caching.Hybrid;
// Register a custom HybridCache keyed to the introspection handlerbuilder.Services.AddKeyedSingleton<HybridCache>( ServiceProviderKeys.IntrospectionCache, (sp, key) => { // Configure a dedicated cache — e.g., with a custom serializer or backing store return new CustomIntrospectionCache(); });The ServiceProviderKeys.IntrospectionCache constant ("Duende.AspNetCore.Authentication.OAuth2Introspection.Cache")
is the DI key the handler uses when resolving its cache. Any keyed HybridCache registered under this key
takes precedence over the default.
Multiple Introspection Schemes
Section titled “Multiple Introspection Schemes”Use the AddOAuth2Introspection(string scheme, Action<OAuth2IntrospectionOptions>) overload when you need to
validate tokens from two different authorization servers in the same application.
builder.Services.AddAuthentication() // Primary introspection scheme for internal APIs .AddOAuth2Introspection("internal", options => { options.Authority = "https://internal.idp.example.com"; options.ClientId = "internal-api"; options.ClientSecret = "internal-secret"; }) // Secondary scheme for partner tokens .AddOAuth2Introspection("partner", options => { options.Authority = "https://partner.idp.example.com"; options.ClientId = "partner-api"; options.ClientSecret = "partner-secret"; });Use the [Authorize(AuthenticationSchemes = "internal")] attribute or .RequireAuthorization() with a
policy to specify which scheme an endpoint should use:
// Minimal API — require the "partner" schemeapp.MapGet("/partner-data", () => Results.Ok("data")) .RequireAuthorization(policy => policy.AddAuthenticationSchemes("partner").RequireAuthenticatedUser());
// MVC controller action — require the "internal" scheme[Authorize(AuthenticationSchemes = "internal")]public IActionResult InternalData() => Ok("data");