Table of Contents

Token providers

A token provider gets the token that a call to another API carries. This page lists the providers Arc4u ships, explains how a handler picks one, and shows how to configure the scenarios of an ASP.NET Core back end: forward the user's token, exchange it on behalf of the user, or use an application identity. The concept is summarized in the glossary; how the token is attached to the call is in HttpClient and gRPC clients.

How a provider is selected

Three pieces take part in every call:

  • Named settings: a SimpleKeyValueSettings registered as a named option (read it with IOptionsMonitor<SimpleKeyValueSettings>.Get(name)). The key ProviderId names the provider, AuthenticationType says for which caller the settings apply, and the other keys (ClientId, Scope, ...) are read by the provider. The key names are the constants of TokenKeys.
  • A token provider: an ITokenProvider registered as a keyed service, whose key is the ProviderId value.
  • A handler: an HttpClient DelegatingHandler or a gRPC interceptor built with the settings. For each call it resolves the keyed provider, calls GetTokenAsync with the settings, and writes the header.
flowchart LR
    Config["appsettings.json (Authentication:...)"] -->|"AddClientTokens, AddOnBehalfOf, ..."| Settings["Named SimpleKeyValueSettings"]
    Settings --> Handler["JwtHttpHandler / OAuth2Interceptor"]
    Handler -->|"keyed by ProviderId"| Provider["ITokenProvider"]
    Provider -->|"TokenInfo"| Handler
    Handler -->|"Authorization header"| Api["Downstream API"]

AuthenticationType decides whose token is sent:

  • Inject: the provider's token is sent whoever the caller is. The application identity scenarios (client credentials, user name and password, remote secret) use it by default. The token type returned by the provider becomes the scheme: Bearer and Basic go in the Authorization header, any other value is used as the name of a custom header.
  • Any other value: no token is sent when the current user was authenticated with another authentication type, and the scheme is always Bearer. The settings that Arc4u registers for the user's own token use OAuth2 (bearer token of an API call) and Cookies (user signed in with OpenID Connect). The HttpClient handler checks that the settings value contains the identity's authentication type; the gRPC interceptor checks that they are equal. See HttpClient and gRPC clients.

Built-in token providers

Each provider exposes its key as a ProviderName constant (TokenProviderName for OidcClientIdentityModelTokenProvider). Register the ones you use as keyed services under that key, as shown in the scenarios below.

Key Type Package Token it returns
Bootstrap BootstrapContextTokenProvider Arc4u.OAuth2.AspNetCore.Authentication The access token of the incoming request, kept in the BootstrapContext of the current identity. Fails when it is expired.
Oidc OidcTokenProvider Arc4u.OAuth2.AspNetCore.Authentication The access token of a user signed in with OpenID Connect and a cookie, refreshed with the refresh token when it is about to expire.
Obo AzureADOboTokenProvider Arc4u.OAuth2.AspNetCore.Authentication A token for another API, exchanged for the user's token with the on-behalf-of grant.
ClientCredentials ClientCredentialsTokenProvider Arc4u.OAuth2.AspNetCore An application token (client credentials grant).
Credential CredentialTokenCacheTokenProvider Arc4u.OAuth2.AspNetCore A token for a fixed user name and password (password grant), requested through the CredentialDirect CredentialTokenProvider.
RemoteSecret RemoteClientSecretTokenProvider Arc4u.OAuth2.AspNetCore No call to an identity provider: the configured secret, sent in the configured header.
Client ClientTokenProvider Arc4u.OAuth2.Blazor The user's token, fetched by a Blazor WebAssembly app from its server. See Blazor.
blazor BlazorTokenProvider, BlazorMsalTokenProvider Arc4u.OAuth2.Blazor The user's token in a Blazor WebAssembly app. See Blazor.
OidcClientIdentityModel OidcClientIdentityModelTokenProvider Arc4u.OAuth2.Client.Authentication The user's token in a desktop or mobile app. See Desktop and mobile clients.
usernamePassword UsernamePasswordTokenProvider Arc4u.OAuth2.Client A token for credentials that the user of a client app enters. See Desktop and mobile clients.

The table below shows which registration method produces the settings each provider reads.

Scenario Registration method Section Settings name ProviderId AuthenticationType
Forward the user's token (API) ConfigureOAuth2Settings, called by AddJwtAuthentication and AddHybridAuthentication Authentication:OAuth2.Settings OAuth2 Bootstrap OAuth2
Forward the user's token (web app) ConfigureOpenIdSettings, called by AddOidcAuthentication and AddHybridAuthentication Authentication:OpenId.Settings Cookies Oidc Cookies
On-behalf-of AddOnBehalfOf Authentication:OnBehalfOf the entry name Obo Inject
Client credentials AddClientTokens Authentication:ClientTokens the entry name ClientCredentials Inject
User name and password AddClientTokens Authentication:ClientTokens the entry name Credential Inject
Remote secret AddRemoteSecretsAuthentication Authentication:RemoteSecrets the entry name RemoteSecret Inject

The ProviderId and AuthenticationType columns are the defaults; most sections let you override them. The server-side registration methods and their sections are described in Server authentication.

Forward the user's token

Use it when an API calls another API that accepts the same token (same audience), or when a web app calls its own API with the token of the signed-in user.

AddJwtAuthentication registers the settings named OAuth2 and AddOidcAuthentication registers the settings named Cookies; AddHybridAuthentication registers both. Register the matching provider, then build the handler with those settings:

// Program.cs
using Arc4u.OAuth2.Token;
using Arc4u.OAuth2.TokenProviders;

var builder = WebApplication.CreateBuilder(args);

// ... authentication: see the Server authentication guide.

builder.Services.AddKeyedScoped<ITokenProvider, BootstrapContextTokenProvider>(BootstrapContextTokenProvider.ProviderName);
builder.Services.AddKeyedScoped<ITokenProvider, OidcTokenProvider>(OidcTokenProvider.ProviderName);

var app = builder.Build();
app.Run();

OidcTokenProvider also needs an ITokenRefreshProvider: RefreshTokenProvider redeems the refresh token with the client ID, client secret and authority of the OpenID Connect scheme.

Pass the settings named OAuth2 to one handler and the settings named Cookies to another, and chain them (see Chain several handlers): each one sends the token only when the caller used its authentication type.

Call an API on behalf of the user

The on-behalf-of flow exchanges the user's token for a token whose audience is the downstream API. Use it when the downstream API does not accept the token your API received. AzureADOboTokenProvider posts the exchange (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer, requested_token_use=on_behalf_of) to the token endpoint of the Default authority, and caches the result until it expires. The user's token is the access token kept by the cookie authentication or, when there is none, the BootstrapContext of the current identity (the bearer token of the request). The cookie token comes first because UseOpenIdBearerInjector() replaces the BootstrapContext with the on-behalf-of token it injects.

{
  "Authentication": {
    "DefaultAuthority": {
      "Url": "https://login.microsoftonline.com/<tenant-id>/v2.0"
    },
    "OnBehalfOf": {
      "Inventory": {
        "ClientId": "<client-id>",
        "ClientSecret": "<client-secret>",
        "Scopes": [ "api://inventory/.default" ],
        "AuthenticationType": "OAuth2"
      }
    }
  }
}

AddOnBehalfOf(builder.Configuration) reads Authentication:OnBehalfOf (pass sectionName to use another section) and registers one settings entry per child, named after the child (Inventory here). The configuration based AddOidcAuthentication and AddHybridAuthentication already call it with the default section: do not call it a second time, see Troubleshooting. AddOnBehalfOfSettings(options, optionKey) registers one entry from code.

Key Type Default Description
Authentication:OnBehalfOf:<name>:ClientId string none (required) Client ID of your application.
Authentication:OnBehalfOf:<name>:ClientSecret string none (required) Client secret of your application.
Authentication:OnBehalfOf:<name>:Scopes string[] none (required) Scopes requested for the downstream API.
Authentication:OnBehalfOf:<name>:AuthenticationType string Inject See How a provider is selected.
Authentication:OnBehalfOf:<name>:ProviderId string Obo Key of the token provider.
Note

The provider returns the token type Bearer: with the default Inject, the token is sent in the Authorization: Bearer header. Set AuthenticationType to OAuth2 (in an API) or Cookies (in a web app) to send the token only when the caller was authenticated that way.

Register the provider and the services it uses: the scoped TokenRefreshInfo (already registered by AddOidcAuthentication, not by AddJwtAuthentication), an ICacheHelper that gives the cache for the exchanged tokens (see Caching) and an IActivitySourceFactory.

builder.Services.AddOnBehalfOf(builder.Configuration);
builder.Services.AddScoped<TokenRefreshInfo>();
builder.Services.AddKeyedScoped<ITokenProvider, AzureADOboTokenProvider>(AzureADOboTokenProvider.ProviderName);

Call an API with the application identity

Use the client credentials grant for calls that do not depend on a user: background jobs, calls to a technical API, or when the downstream API authorizes your application rather than the user. Declare one entry per downstream API in Authentication:ClientTokens:

{
  "Authentication": {
    "DefaultAuthority": {
      "Url": "https://login.microsoftonline.com/<tenant-id>/v2.0"
    },
    "ClientTokens": {
      "Inventory": {
        "Scenario": "ClientCredentials",
        "Scopes": [ "api://inventory/.default" ],
        "Settings": {
          "ClientId": "<client-id>",
          "ClientSecret": "<client-secret>"
        }
      }
    }
  }
}
builder.Services.AddDefaultAuthority(builder.Configuration);
builder.Services.AddClientTokens(builder.Configuration);
builder.Services.AddKeyedSingleton<ITokenProvider, ClientCredentialsTokenProvider>(ClientCredentialsTokenProvider.ProviderName);

AddClientTokens reads Authentication:ClientTokens (pass sectionName to use another section), validates every entry and throws a ConfigurationException at registration when one is invalid. Each entry becomes the settings named after it (Inventory here). The configuration based AddJwtAuthentication already calls it; calling it again is harmless, an entry is registered only once.

ClientCredentialsTokenProvider sends the client ID and secret in an Authorization: Basic header (client_secret_basic), and the scope and any extra parameter in the form body. It caches the token in the registered ITokenCache (see the shortest setup for its registration) and requests a new one when the cached token expires in less than one minute.

Key Type Default Description
Authentication:ClientTokens:<name>:Scenario string none (required) ClientCredentials, UserPassword or Basic.
Authentication:ClientTokens:<name>:Scopes string[] openid Scopes requested, joined with spaces.
Authentication:ClientTokens:<name>:AuthenticationType string Inject See How a provider is selected.
Authentication:ClientTokens:<name>:Authority object the Default authority An authority for this entry only (Url, TokenEndpoint, Issuer, MetaDataAddress), registered under the entry name.
Authentication:ClientTokens:<name>:Settings:ClientId string none (required) Client ID of your application.
Authentication:ClientTokens:<name>:Settings:ClientSecret string none (required) Client secret of your application.
Authentication:ClientTokens:<name>:Settings:<other> string none Any other key is sent to the token endpoint as an extra form parameter, for example resource. See Cache keys.

The Default authority comes from Authentication:DefaultAuthority, read by AddDefaultAuthority. When TokenEndpoint is not set, the token endpoint is read from the OpenID Connect metadata of the authority (<Url>/.well-known/openid-configuration, or MetaDataAddress).

Call an API with a user name and password

Some identity providers issue a token for a technical account with the password grant. The UserPassword scenario takes the user name and the password as two keys, the Basic scenario takes them as one user:password value:

{
  "Authentication": {
    "DefaultAuthority": {
      "Url": "https://sts.example.com/realms/example"
    },
    "ClientTokens": {
      "Reporting": {
        "Scenario": "UserPassword",
        "Scopes": [ "openid" ],
        "Settings": {
          "ClientId": "<client-id>",
          "User": "<user>",
          "Password": "<password>"
        }
      },
      "Archive": {
        "Scenario": "Basic",
        "Settings": {
          "ClientId": "<client-id>",
          "Credential": "<user>:<password>"
        }
      }
    }
  }
}

Both scenarios use the Credential provider, which caches the token and asks the CredentialDirect provider for a new one when it expires in less than one minute. ClientSecret is optional in the Settings of both scenarios, for identity providers that require it with the password grant. Unlike the client credentials scenario, other Settings keys are not sent to the token endpoint.

builder.Services.AddDefaultAuthority(builder.Configuration);
builder.Services.AddClientTokens(builder.Configuration);
builder.Services.AddKeyedTransient<ITokenProvider, CredentialTokenCacheTokenProvider>(CredentialTokenCacheTokenProvider.ProviderName);
builder.Services.AddKeyedSingleton<ICredentialTokenProvider, CredentialTokenProvider>(CredentialTokenProvider.ProviderName);

CredentialTokenCacheTokenProvider also needs the ITokenCache of the shortest setup. Without the Default authority (registered by AddDefaultAuthority or by the server authentication), every call throws NotSupportedException: The 'about' scheme is not supported.

Caution

Keep passwords and client secrets out of appsettings.json in source control: use user secrets, environment variables, a vault, or the encrypted values of the Configuration guide.

Send a remote secret

A remote secret is not a token: it is a value (typically an encrypted user:password pair) that your service sends in a header, and that the receiving service turns into a token with its Basic authentication middleware (see Server authentication). RemoteClientSecretTokenProvider calls no identity provider.

{
  "Authentication": {
    "RemoteSecrets": {
      "Billing": {
        "ClientSecret": "<encrypted-secret>",
        "HeaderKey": "SecretKey"
      }
    }
  }
}
builder.Services.AddRemoteSecretsAuthentication(builder.Configuration);
builder.Services.AddKeyedSingleton<ITokenProvider, RemoteClientSecretTokenProvider>(RemoteClientSecretTokenProvider.ProviderName);

AddRemoteSecretsAuthentication reads Authentication:RemoteSecrets (pass sectionName to use another section). The configuration based AddJwtAuthentication already calls it.

Key Type Default Description
Authentication:RemoteSecrets:<name>:ClientSecret string none (required) The value to send.
Authentication:RemoteSecrets:<name>:HeaderKey string SecretKey The header that carries the value. Basic sends Authorization: Basic <value>: a clear user:password value is Base64 encoded first, a value already Base64 encoded is sent as is.
Authentication:RemoteSecrets:<name>:AuthenticationType string Inject Keep Inject: the header name is taken from the token type only in that mode.
Authentication:RemoteSecrets:<name>:ProviderId string RemoteSecret Key of the token provider.

Cache keys

The providers cache the tokens they obtain. A cache key contains every value that changes the token, and the secret parts (user token, password, client secret) are hashed with SHA-256 through TokenCacheKey. The hash is the same in every process, so with a distributed cache (Redis, SQL Server) the entries are shared between instances and survive a restart:

Provider Key
ClientCredentials ClientCredentials_<client-id>_<hash>, the hash covers the authority URL, the scope, the client secret and the extra parameters (for example resource, in any order).
Credential Credential_<user>_<hash>, the hash covers the authority URL, the scope, the user and the password.
Obo _Obo_<client-id>_<hash>, the hash covers the authority URL, the user's token and the scope.

Two ClientTokens entries that differ only by an extra parameter such as resource get their own cached token.

Write your own token provider

Implement ITokenProvider, register it as a keyed service, and put its key in the ProviderId of the settings. GetTokenAsync returns a ValueTask<Result<TokenInfo>>, so a provider that answers from a cache or from the settings completes without allocating a Task. Return a failed result rather than throwing when no token is available, the handlers then send the call without a token.

using Arc4u;
using Arc4u.OAuth2.Token;
using FluentResults;

public sealed class StaticTokenProvider : ITokenProvider
{
    public const string ProviderName = "Static";

    public ValueTask<Result<TokenInfo>> GetTokenAsync(IKeyValueSettings? settings, object? platformParameters)
    {
        if (settings is null || !settings.Values.TryGetValue("Token", out var token))
        {
            return ValueTask.FromResult(Result.Fail<TokenInfo>("No token configured."));
        }

        return ValueTask.FromResult(Result.Ok(new TokenInfo("Bearer", token, DateTime.UtcNow.AddMinutes(5))));
    }
}
builder.Services.AddKeyedSingleton<ITokenProvider, StaticTokenProvider>(StaticTokenProvider.ProviderName);

When your provider keeps tokens for a user (in a cache, in memory, in a store), implement ISignOutTokenProvider instead and remove them in SignOutAsync. SignOutUserAsync of AppPrincipalFactory (client) and of AppServicePrincipalFactory (server) signs the user out of every keyed provider that implements it (with SignOutTokenProvidersAsync), once per instance, and logs the exceptions so one provider cannot block the others. Every provider gets the same settings, those given to SignOutUserAsync: ignore them when they are not yours, and do not throw. A provider that only implements ITokenProvider (a machine-to-machine token, a secret, the token of the incoming request) is skipped. Keep registering the provider as an ITokenProvider.

AddOidcAuthentication, AddHybridAuthentication, AddOnBehalfOf, AddOidcClientAuthentication and AddAuthenticationCookie call services.AddTokenProvidersSignOut(): the providers are then resolved one by one, so a provider that cannot be resolved (a missing dependency, a scoped provider resolved from a singleton factory) is logged and the others are signed out. Call AddTokenProvidersSignOut() yourself when your application uses none of these methods; otherwise all the keyed providers are resolved together, and one that cannot be resolved prevents the sign-out of all of them.

Register AppPrincipalFactory as transient (or scoped), not as a singleton: it resolves the providers from the scope it is created in, so a singleton would sign out root instances of the scoped services instead of the ones the application uses.

Provider Sign-out
AzureADOboTokenProvider Removes from the cache the on-behalf-of tokens of every API obtained for the current user, also with an access token that was refreshed since. Their keys are kept in an OboTokenCacheIndex per user (the registered IUserObjectIdentifier, as for the claims cache, otherwise the oid or sub claim) and application (aud claim, otherwise azp or appid), both read from the user token, so two applications sharing a cache do not remove each other's tokens: add this type to your JsonSerializerContext if the cache serializer uses one
ClientTokenProvider (Blazor) Forgets the token kept in memory, and does not keep a token requested before the sign-out. Sign out of the backend cookie in your application
BlazorMsalTokenProvider Clears the principal. Sign out with NavigateToLogout
OidcClientIdentityModelTokenProvider Cancels a login in progress, clears the tokens in memory and in the secure cache, then logs the user out of the authority (in the browser, bounded by LoginTimeout) when the user had a session. A refresh or a login in progress is not kept; concurrent calls refresh the token once
UsernamePasswordTokenProvider Removes the user name, the password and the token from the secure cache. Does nothing with settings without Authority and ServiceApplicationId
BlazorTokenProvider Removes the token from the local storage
OidcTokenProvider None: the tokens are in the authentication cookie. Sign out of the cookie and OpenID Connect schemes with HttpContext.SignOutAsync
CredentialTokenCacheTokenProvider None: the token is the one of a configured account, shared by all the users
RemoteClientSecretTokenProvider, ClientCredentialsTokenProvider, BootstrapContextTokenProvider, NullTokenProvider None: they hold no user session

To add a scenario to Authentication:ClientTokens, implement IClientTokenScenario (its validation rules and the settings it writes) and register it before AddClientTokens runs, keeping the built-in ones. Here MyScenario is your implementation:

ClientTokensExtension.SetScenarioResolver(discriminator => discriminator switch
{
    "MyScenario" => new MyScenario(),
    _ => ClientTokensExtension.DefaultScenario(discriminator)
});

See also