Customizing User Token Management
The most common way to use access token management is for interactive web applications - however, you may want to customize certain aspects of it. Here’s what you can do.
General Options
Section titled “General Options”You can pass in some global options when registering token management in the ASP.NET Core service provider.
ChallengeScheme- by default the OIDC configuration is inferred from the default challenge scheme. This is recommended approach. If for some reason your OIDC handler is not the default challenge scheme, you can set the scheme name on the optionsUseChallengeSchemeScopedTokens- the general assumption is that you only have one OIDC handler configured. If that is not the case, token management needs to maintain multiple sets of token artefacts simultaneously. You can opt in to that feature using this setting.RefreshBeforeExpiration- specifies how long before expiration the token should be refreshed (defaults to 1 minute)ClientCredentialsScope- when requesting client credentials tokens from the OIDC provider, the scope parameter will not be set since its value cannot be inferred from the OIDC configuration. With this setting you can set the value of the scope parameter.ClientCredentialsResource- same as previous, but for the resource parameterClientCredentialStyle- specifies how client credentials are transmitted to the OIDC provider
Access Token Management before 4.0
builder.Services.AddOpenIdConnectAccessTokenManagement(options =>{ options.ChallengeScheme = "schemeName"; options.UseChallengeSchemeScopedTokens = false; options.RefreshBeforeExpiration = TimeSpan.FromMinutes(2);
options.ClientCredentialsScope = "api1 api2"; options.ClientCredentialsResource = "urn:resource"; options.ClientCredentialStyle = ClientCredentialStyle.PostBody;});Access Token Management 4.0 to before 4.3
builder.Services.AddOpenIdConnectAccessTokenManagement(options =>{ options.ChallengeScheme = Scheme.Parse("schemeName"); options.UseChallengeSchemeScopedTokens = false; options.RefreshBeforeExpiration = TimeSpan.FromMinutes(2);
options.ClientCredentialsScope = Scope.Parse("api1 api2"); options.ClientCredentialsResource = Resource.Parse("urn:resource"); options.ClientCredentialStyle = ClientCredentialStyle.PostBody;});Access Token Management 4.3 and later
MaxAccessTokenLength- the maximum length of a user access token (defaults toAccessToken.MaxLength, 32768 characters)MaxIdentityTokenLength- the maximum length of an identity token (defaults toIdentityToken.MaxLength, 32768 characters)MaxRefreshTokenLength- the maximum length of a refresh token (defaults toRefreshToken.MaxLength, 4096 characters)
builder.Services.AddOpenIdConnectAccessTokenManagement(options =>{ options.ChallengeScheme = Scheme.Parse("schemeName"); options.UseChallengeSchemeScopedTokens = false; options.RefreshBeforeExpiration = TimeSpan.FromMinutes(2);
options.ClientCredentialsScope = Scope.Parse("api1 api2"); options.ClientCredentialsResource = Resource.Parse("urn:resource"); options.ClientCredentialStyle = ClientCredentialStyle.PostBody;
options.MaxAccessTokenLength = 64 * 1024;});The maximum length options apply to tokens in a refresh response from the token endpoint, and to tokens read back from
the authentication session. Raise them when your identity provider issues larger tokens, for example access tokens with
many claims. A token over the limit makes token management throw an InvalidOperationException that names the option
to increase. Each value must be greater than zero, or reading the options throws an OptionsValidationException.
Larger tokens make the authentication cookie larger, because the default token store keeps tokens in the authentication
session. Server or proxy header size limits can then reject requests. To keep the cookie small, store the session on the
server with the cookie handler’s TicketStore (see Token Storage).
Per Request Parameters
Section titled “Per Request Parameters”You can also modify token management parameters on a per-request basis.
The UserTokenRequestParameters class can be used for that:
SignInScheme- allows specifying a sign-in scheme. This is used by the default token storeChallengeScheme- allows specifying a challenge scheme. This is used to infer token service configurationForceTokenRenewal- forces token retrieval even if a cached token would be availableScope- overrides the globally configured scope parameterResource- override the globally configured resource parameterAssertion- allows setting a client assertion for the request
The request parameters can be passed via the manual API:
var token = await _tokenManagementService .GetAccessTokenAsync(User, new UserTokenRequestParameters { // ... });…the extension methods
var token = await HttpContext.GetUserAccessTokenAsync( new UserTokenRequestParameters { // ... });…or the HTTP client factory
// registers HTTP client that uses the managed user access tokenbuilder.Services.AddUserAccessTokenHttpClient("invoices", parameters: new UserTokenRequestParameters { // ... }, configureClient: client => { client.BaseAddress = new Uri("https://api.company.com/invoices/"); });
// registers a typed HTTP client with token management supportbuilder.Services.AddHttpClient<InvoiceClient>(client => { client.BaseAddress = new Uri("https://api.company.com/invoices/"); }) .AddUserAccessTokenHandler(new UserTokenRequestParameters { // ... });Token Storage
Section titled “Token Storage”By default, the user’s access and refresh token will be stored in the ASP.NET Core authentication session (implemented by the cookie handler).
You can modify this in two ways:
- the cookie handler itself has an extensible storage mechanism via the
TicketStoremechanism - replace the store altogether by providing an
IUserTokenStoreimplementation and registering it in the service provider at application startup
IUserTokenStore Interface
Section titled “IUserTokenStore Interface”The IUserTokenStore interface is the primary abstraction for token storage. Implement this interface to store tokens in a database, distributed cache, or other backing store.
public interface IUserTokenStore{ /// <summary> /// Stores a token for a user /// </summary> Task StoreTokenAsync( ClaimsPrincipal user, UserToken token, UserTokenRequestParameters parameters = default, CancellationToken ct = default);
/// <summary> /// Retrieves a token for a user. Returns a TokenForParameters which contains /// either the cached token or just the refresh token if no cached token exists. /// </summary> Task<TokenResult<TokenForParameters>> GetTokenAsync( ClaimsPrincipal user, UserTokenRequestParameters parameters = default, CancellationToken ct = default);
/// <summary> /// Clears/removes the stored token for a user /// </summary> Task ClearTokenAsync( ClaimsPrincipal user, UserTokenRequestParameters parameters = default, CancellationToken ct = default);}Custom Token Store Example
Section titled “Custom Token Store Example”The following example shows a custom token store that persists tokens to a database:
public class DatabaseUserTokenStore : IUserTokenStore{ private readonly ITokenRepository _repository;
public DatabaseUserTokenStore(ITokenRepository repository) { _repository = repository; }
public async Task StoreTokenAsync( ClaimsPrincipal user, UserToken token, UserTokenRequestParameters parameters = default, CancellationToken ct = default) { var userId = user.FindFirstValue("sub") ?? throw new InvalidOperationException("No sub claim found");
await _repository.SaveTokenAsync(userId, new StoredToken { AccessToken = token.AccessToken.ToString(), RefreshToken = token.RefreshToken?.ToString(), Expiration = token.Expiration, Scope = token.Scope?.ToString() }, ct); }
public async Task<TokenResult<TokenForParameters>> GetTokenAsync( ClaimsPrincipal user, UserTokenRequestParameters parameters = default, CancellationToken ct = default) { var userId = user.FindFirstValue("sub"); if (userId == null) { return TokenResult.Failure<TokenForParameters>( new TokenResultFailure("No sub claim found")); }
var stored = await _repository.GetTokenAsync(userId, ct); if (stored == null) { return TokenResult.Failure<TokenForParameters>( new TokenResultFailure("No token found")); }
var userToken = new UserToken { AccessToken = AccessToken.Parse(stored.AccessToken), RefreshToken = stored.RefreshToken != null ? RefreshToken.Parse(stored.RefreshToken) : null, Expiration = stored.Expiration };
var refreshToken = stored.RefreshToken != null ? new UserRefreshToken(RefreshToken.Parse(stored.RefreshToken), null) : null;
return TokenResult.Success(new TokenForParameters(userToken, refreshToken)); }
public async Task ClearTokenAsync( ClaimsPrincipal user, UserTokenRequestParameters parameters = default, CancellationToken ct = default) { var userId = user.FindFirstValue("sub"); if (userId != null) { await _repository.DeleteTokenAsync(userId, ct); } }}Access Token Management 4.3 and later
AccessToken.Parse and RefreshToken.Parse reject tokens over the default maximum length. If you raised one of the
maximum length options, pass the same limit to the Parse(string value, int maxLength) overload, for example
AccessToken.Parse(stored.AccessToken, options.MaxAccessTokenLength). AccessToken, IdentityToken, and RefreshToken
also have a matching TryParse overload, and IdentityToken has ParseOrDefault(string? value, int maxLength).
Register your custom store in the service provider:
builder.Services.AddOpenIdConnectAccessTokenManagement();builder.Services.AddSingleton<IUserTokenStore, DatabaseUserTokenStore>();IStoreTokensInAuthenticationProperties Interface
Section titled “IStoreTokensInAuthenticationProperties Interface”For more granular control over how tokens are stored within the ASP.NET Core AuthenticationProperties, implement IStoreTokensInAuthenticationProperties. This is a lower-level interface used by the default IUserTokenStore implementation.
public interface IStoreTokensInAuthenticationProperties{ /// <summary> /// Gets a user token from the authentication properties /// </summary> TokenResult<TokenForParameters> GetUserToken( AuthenticationProperties authenticationProperties, UserTokenRequestParameters parameters = default);
/// <summary> /// Sets a user token in the authentication properties /// </summary> Task SetUserTokenAsync( UserToken token, AuthenticationProperties authenticationProperties, UserTokenRequestParameters parameters = default, CancellationToken ct = default);
/// <summary> /// Removes the user token from the authentication properties /// </summary> void RemoveUserToken( AuthenticationProperties authenticationProperties, UserTokenRequestParameters parameters = default);
/// <summary> /// Gets the authentication scheme to use /// </summary> Task<Scheme> GetSchemeAsync( UserTokenRequestParameters parameters = default, CancellationToken ct = default);}This interface is useful when you need to customize how tokens are serialized or structured within the authentication ticket, while still using the cookie-based storage mechanism