Skip to content
Introducing the next era of Duende IdentityServer. Read our CEO’s announcement

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.


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:

MethodDefault ParameterBehavior
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");
options.TokenRetriever = TokenRetrieval.FromQueryString();
// or with a custom parameter name:
options.TokenRetriever = TokenRetrieval.FromQueryString("token");

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 header
options.TokenRetriever = request =>
request.Headers["X-Api-Token"].FirstOrDefault();

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.

using Duende.AspNetCore.Authentication.OAuth2Introspection.Infrastructure;
// This is the default — you do not need to set it explicitly
options.CacheKeyGenerator = CacheUtils.CacheKeyFromToken();
// Using a prefix to namespace keys in a shared cache
options.CacheKeyPrefix = "introspection:";

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.

FlagEffect
HybridCacheEntryFlags.None (default)Write to both L1 and L2 caches
HybridCacheEntryFlags.DisableLocalCacheSkip the in-process L1 cache
HybridCacheEntryFlags.DisableDistributedCacheSkip the distributed L2 cache
HybridCacheEntryFlags.DisableLocalCache | HybridCacheEntryFlags.DisableDistributedCacheDisable caching entirely
using Microsoft.Extensions.Caching.Hybrid;
// Disable the in-process cache, use only the distributed cache
options.SetCacheEntryFlags = HybridCacheEntryFlags.DisableLocalCache;
// Disable caching entirely (every request will call the introspection endpoint)
options.SetCacheEntryFlags =
HybridCacheEntryFlags.DisableLocalCache |
HybridCacheEntryFlags.DisableDistributedCache;

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 handler
builder.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.


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" scheme
app.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");