Examples
April 30, 2026 · View on GitHub
- Get a token on behalf of a user
- Get an access token for a connection
- Multiple Custom Domains (MCD)
- Discovery Cache
- DPoP Authentication
Get a token on behalf of a user
Use getTokenOnBehalfOf() when your API receives an Auth0 access token for itself and needs to
exchange it for another Auth0 access token targeting a downstream API while preserving the same
user identity. This is especially useful for MCP servers and other intermediary APIs that need to
call downstream APIs on behalf of the user.
The following example verifies the incoming access token for your API, exchanges it for a token for the downstream API, and then calls the downstream API with the exchanged token.
import { apiClient } from './auth0.js';
function getBearerToken(authorizationHeader: string | null): string {
if (!authorizationHeader?.toLowerCase().startsWith('bearer ')) {
throw new Error('Missing Bearer access token');
}
return authorizationHeader.slice('Bearer '.length).trim();
}
export async function handleCalendarRequest(request: Request) {
const incomingAccessToken = getBearerToken(request.headers.get('authorization'));
// Verify the incoming token for your API before exchanging it.
const claims = await apiClient.verifyAccessToken({
accessToken: incomingAccessToken,
});
const obo = await apiClient.getTokenOnBehalfOf(incomingAccessToken, {
audience: 'https://calendar-api.example.com',
scope: 'calendar:read calendar:write',
});
const downstreamResponse = await fetch('https://calendar-api.example.com/events', {
headers: {
authorization: `Bearer ${obo.accessToken}`,
},
});
if (!downstreamResponse.ok) {
throw new Error(`Calendar API request failed with ${downstreamResponse.status}`);
}
return {
user: claims.sub,
data: await downstreamResponse.json(),
};
}
The parameters for the getTokenOnBehalfOf method are as follows:
accessToken: The incoming Auth0 access token used as thesubject_token.audience: The identifier of the downstream API.scope(optional): The requested scopes for the downstream API.
If the exchange is successful, the method returns an OnBehalfOfTokenResult object containing:
accessToken: The exchanged access token issued for the downstream API.expiresAt: The access token expiration time, represented in seconds since the Unix epoch.scope: The scope granted for the exchanged token, if returned.tokenType: The returned token type, if returned.issuedTokenType: The returned RFC 8693 issued token type, if returned.
[!TIP] Production notes:
- Pass the raw access token to
getTokenOnBehalfOf(). Do not pass the fullAuthorizationheader or include theBearerprefix.- Verify the incoming token for your API before exchanging it so your application rejects invalid or mis-targeted tokens early.
- The downstream
audiencemust match an API identifier configured in your Auth0 tenant.getTokenOnBehalfOf()only returns access-token-oriented fields. It does not exposeidTokenorrefreshToken.
When a downstream API receives an exchanged token, it can verify the token, confirm that the current actor is the expected MCP server, and record the full delegation chain for audit logging. The following example uses the verifier helpers for that flow:
import { getCurrentActor, getDelegationChain } from '@auth0/auth0-api-js';
const claims = await apiClient.verifyAccessToken({ accessToken });
const currentActor = getCurrentActor(claims);
const delegationChain = getDelegationChain(claims);
// Authorize only the current actor.
if (currentActor && currentActor !== 'mcp_server_client_id') {
throw new Error('Unexpected actor');
}
// Use the full chain for logging or audit only.
auditLogger.info('delegated_request', {
user: claims.sub,
currentActor,
delegationChain,
});
Only the outermost act.sub should be used for authorization decisions. Use delegationChain for logging, audit, or attribution.
In the current implementation, getTokenOnBehalfOf() forwards the incoming access token as the
RFC 8693 subject_token and relies on Auth0 to handle any DPoP-specific behavior for that token.
Get an access token for a connection
The getAccessTokenForConnection method allows you to exchange an access token for an access token for a specific connection. To use this method, you will need to instantiate the ApiClient with the client credentials:
import { ApiClient } from '@auth0/auth0-api-js';
const apiClient = new ApiClient({
domain: '<AUTH0_DOMAIN>',
audience: '<AUTH0_AUDIENCE>',
clientId: '<AUTH0_CLIENT_ID>',
clientSecret: '<AUTH0_CLIENT_SECRET>',
});
const tokenSet = await apiClient.getAccessTokenForConnection({
connection: 'my-connection',
accessToken: 'my-access-token',
loginHint: 'login-hint', // Optional
});
The parameters for the getAccessTokenForConnection method are as follows:
connection: The name of the connection to get the token for.accessToken: The access token used as the subject token to be exchanged.loginHint(optional): An optional login hint to pass to the connection.
If the exchange is successful, the method will return a ConnectionTokenSet object containing the following properties:
accessToken: The access token issued by the connection.scope: The scope granted by the connection.expiresAt: The access token expiration time, represented in seconds since the Unix epoch.connection: The name of the connection the token was requested for.loginHint: An optional login hint that was passed during the exchange.
For additional details, please refer to the Token Vault documentation.
Multiple Custom Domains (MCD)
Multiple Custom Domains (MCD) support enables a single API application to accept access tokens issued by multiple domains associated with the same Auth0 tenant, including the canonical domain and its custom domains.
This is commonly required in scenarios such as:
- Multi-brand applications (B2C) where each brand uses a different custom domain but they all share the same API.
- A single API serves multiple frontend applications that use different custom domains.
- A gradual migration from the canonical domain to a custom domain, where both domains need to be supported during the transition period.
In these cases, your API must trust and validate tokens from multiple issuers instead of a single domain.
The SDK supports two approaches for configuring multiple allowed issuer domains:
Static domains
Use a static allowlist when the set of trusted issuer domains is known in advance and remains the same for all requests. This approach also works well for domain migration scenarios, where multiple domains, such as the canonical domain and one or more custom domains, need to be accepted during a transition period. The SDK validates incoming tokens against this predefined list of allowed issuer domains.
import { ApiClient } from '@auth0/auth0-api-js';
const apiClient = new ApiClient({
audience: 'https://api.example.com',
domains: [
'brand1.auth.example.com',
'brand2.auth.example.com',
],
});
const payload = await apiClient.verifyAccessToken({
accessToken,
});
Dynamic resolver
Use a dynamic resolver when the set of allowed issuer domains needs to be determined at runtime based on the incoming request.
The SDK provides a DomainsResolverContext containing request and token-derived information (url, headers, and unverifiedIss). You can use any combination of these inputs to determine the allowed issuer domains for the request.
In the following example, a single API application is accessed through two domains:
https://api.brand1.com/https://api.brand2.com/
Each domain should only accept tokens issued by its corresponding Auth0 custom domains.
-
https://api.brand1.com/should accept tokens issued by:brand1-en.auth.example.combrand1-jp.auth.example.com
-
https://api.brand2.com/should accept tokens issued by:brand2-en.auth.example.combrand2-jp.auth.example.com
To enforce this behavior, you can configure a dynamic domain resolver that determines the allowed issuer domains based on the incoming request.
import { ApiClient, type DomainsResolver, type DomainsResolverContext } from '@auth0/auth0-api-js';
const domainsResolver: DomainsResolver = async (context: DomainsResolverContext) => {
const host = context.url ? new URL(context.url).hostname : undefined;
if (host === 'api.brand1.com') {
return ['brand1-en.auth.example.com', 'brand1-jp.auth.example.com'];
}
if (host === 'api.brand2.com') {
return ['brand2-en.auth.example.com', 'brand2-jp.auth.example.com'];
}
// Fallback to the default custom domain.
return ['default.auth.example.com'];
};
const apiClient = new ApiClient({
audience: 'https://api.example.com',
domains: domainsResolver, // provide the resolver function
algorithms: ['RS256'], // optional, defaults to RS256
});
const payload = await apiClient.verifyAccessToken({
accessToken,
httpUrl: '<REQUEST_URL>', // Get it from the incoming request in your framework.
headers: '<REQUEST_HEADERS>', // Get it from the incoming request in your framework.
});
It is the application's responsibility to decide how to use this information to return the allowed issuer domains. This allows the application to control which issuers the SDK can verify tokens from on a per-request basis. The resolver must return a non-empty array of domain strings.
In MCD, httpUrl is optional for bearer token verification. When provided, the SDK passes it to the domains resolver as context.url. If it is omitted, context.url will be undefined. So if your resolver needs the request URL, make sure you pass httpUrl.
Security Requirements
When configuring domains or a domain resolver for Multiple Custom Domains (MCD), you are responsible for ensuring that only trusted issuer domains are returned.
Mis-configuring the domain resolver is a critical security risk. It can cause the SDK to:
- accept access tokens from unintended issuers
- make discovery or JWKS requests to unintended domains
Single Tenant Limitation:
The domains configuration is intended only for multiple custom domains that belong to the same Auth0 tenant. It is not a supported mechanism for connecting multiple Auth0 tenants to a single API.
Request-Derived Input Warning:
If your resolver uses request-derived values such as context.url, context.headers, or context.unverifiedIss, do not trust those values directly. Use them only to map known and expected request values to a fixed allowlist of issuer domains that you control.
In particular:
context.urlandcontext.headersmay be influenced by clients, proxies, or load balancers, depending on your framework and deployment setupcontext.unverifiedIsscomes from the token before signature verification and must not be trusted by itself
If your deployment relies on reverse proxies or load balancers, ensure that host-related request information is treated as trusted only when it comes from trusted infrastructure. Misconfigured proxy handling can cause the SDK to trust unintended issuer domains.
Discovery Cache
By default, the SDK caches OIDC discovery metadata and JWKS fetchers in memory using LRU caches with a TTL of 600 seconds and a maximum of 100 entries.
Most applications can keep the defaults, but you may want to adjust discoveryCache in the following cases:
- Increase
maxEntriesif one process may verify tokens for more than100distinct domains or JWKS URIs during theTTLwindow. This is most common in Multiple Custom Domains (MCD) deployments that work with many custom domains. - Decrease
maxEntriesif memory usage matters more than avoiding repeated discovery and JWKS setup. - Increase
ttlif the same domains are reused frequently and you want to reduce repeated discovery and JWKS setup after cache entries expire. - Decrease
ttlif you want the SDK to recreate discovery and JWKS fetchers sooner. - Set
ttlto0if you want to effectively disable discovery cache.
Rule of thumb:
Set maxEntries to cover the number of distinct domains or JWKS URIs a single process is expected to use during the TTL window, with some headroom.
If you need different cache behavior, configure discoveryCache:
import { ApiClient } from '@auth0/auth0-api-js';
const cachedApiClient = new ApiClient({
domain: 'your-tenant.auth0.com',
audience: 'https://api.example.com',
discoveryCache: { ttl: 900, maxEntries: 200 },
});
DPoP Authentication
DPoP (Demonstrating Proof of Possession) is an application-level mechanism for sender-constraining OAuth 2.0 access and refresh tokens by proving that the client application is in possession of a certain private key.
By default, DPoP is enabled but not required. This means that the auth0-api-js will accept both Bearer and DPoP tokens.
Access token verifier options
If the request uses DPoP authentication, you must provide all required DPoP parameters when calling verifyAccessToken(), missing any of them will result in an error.
The following parameters are required for DPoP validation:
accessToken: The JWT access token to be verified.scheme: The authentication scheme used in theAuthorizationheader. Eitherbearerordpop.dpopProof: The value of theDPoPheader from the incoming HTTP request.httpMethod: The HTTP method of the incoming request (e.g.,GET,POST).httpUrl: The full URL of the incoming request.
Accept both Bearer and DPoP tokens (default)
import { ApiClient } from '@auth0/auth0-api-js';
const apiClient = new ApiClient({
domain: 'your-tenant.auth0.com',
audience: 'https://api.example.com',
dpop: { mode: 'allowed' }, // accept bearer or `DPoP`
});
// 1. `Bearer` token verification (no `DPoP` proof)
const bearerPayload = await apiClient.verifyAccessToken({
accessToken: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…', // JWT access token
});
// 2. `DPoP-bound` token verification
const dpopPayload = await apiClient.verifyAccessToken({
accessToken: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…', // JWT with cnf.jkt
scheme: 'dpop',
dpopProof: 'eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3A...',
httpMethod: 'GET',
httpUrl: 'https://api.example.com/resource/123',
});
Requests using DPoP must include both Authorization and DPoP headers:
Authorization: DPoP eyJhbGciOiJFUzI1NiIsInR5cCI6...
DPoP: eyJhbGciOiJkcG9wIiwidHlwIjoi...
Require only DPoP tokens
To enforce stronger protection and reject non-DPoP tokens:
import { ApiClient } from '@auth0/auth0-api-js';
const apiClient = new ApiClient({
domain: 'your-tenant.auth0.com',
audience: 'https://api.example.com',
dpop: { mode: 'required' }, // `DPoP` enforced
});
// for each request, validate the `DPoP-bound` token + proof
const payload = await apiClient.verifyAccessToken({
accessToken: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…',
scheme: 'dpop',
dpopProof: 'eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3A…',
httpMethod: 'POST',
httpUrl: 'https://api.example.com/resource/123',
});
// use `payload` claims downstream
Require only Bearer tokens
If you want to reject all DPoP tokens and only accept standard Bearer access tokens, you can disable DPoP support explicitly:
import { ApiClient } from '@auth0/auth0-api-js';
const apiClient = new ApiClient({
domain: 'your-tenant.auth0.com',
audience: 'https://api.example.com',
dpop: { mode: 'disabled' }, // `DPoP` disabled; `Bearer` only
});
// verify a bearer token (`DPoP` proof is ignored even if present)
const payload = await apiClient.verifyAccessToken({
accessToken: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…',
// `scheme` defaults to bearer.
// other `DPoP` params are not required.
});
// use `payload` claims downstream
Customize DPoP validation behavior
import { ApiClient } from '@auth0/auth0-api-js';
const apiClient = new ApiClient({
domain: 'your-tenant.auth0.com',
audience: 'https://api.example.com',
dpop: {
mode: 'allowed',
iatOffset: 120, // accept proofs up to 2 minutes old
iatLeeway: 10, // allow up to 10 seconds of clock skew into the future
},
});
// later, verify a DPoP-bound request
const payload = await apiClient.verifyAccessToken({
accessToken: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…',
scheme: 'dpop',
dpopProof: 'eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3A…',
httpMethod: 'POST',
httpUrl: 'https://api.example.com/resource/123',
});
// `payload` contains the verified JWT claims
DPoP Behavior Matrix
| Mode | Behavior |
|---|---|
allowed | Default behavior. Both Bearer and DPoP tokens are accepted. Proofs are validated if present. |
disabled | Legacy mode: only Bearer scheme is accepted; DPoP scheme is rejected. Warning: DPoP-bound tokens (with cnf.jkt) are still accepted as Bearer without proof validation, which downgrades token binding. Use only for migration or legacy compatibility. |
required | Invalid configuration. DPoP is ignored, so required: true has no effect. DPoP is ignored entirely. |
Proof Timing Options
When DPoP is enabled, you can control the accepted timing of DPoP proofs using the following options:
-
iatOffset: The maximum age (in seconds) of a DPoP proof. Proofs withiatolder than this offset (relative to now) will be rejected. Default:300 seconds(5 minutes) -
iatLeeway: Clock skew tolerance (in seconds) when comparing a proof'siatwith the current server time. Default:30 seconds