- Logging Out
- Calling an API
- Refresh Tokens
- Online Access (Online Refresh Tokens)
- Data Caching Options
- Organizations
- Native to Web SSO
- Custom Token Exchange (CTE)
- Device-bound tokens with DPoP
- Connect Accounts for using Token Vault
- Accessing SDK Configuration
- Passkeys
- Multi-Factor Authentication (MFA)
- Step-Up Authentication
- MyAccount API
- Session Expiry from Upstream IdP (IPSIE)
<button id="logout">Logout</button>document.getElementById('logout').addEventListener('click', () => {
auth0.logout();
});You can redirect users back to your app after logging out. This URL must appear in the Allowed Logout URLs setting for the app in your Auth0 Dashboard:
auth0.logout({
logoutParams: {
returnTo: 'https://your.custom.url.example.com/'
}
});Retrieve an access token to pass along in the Authorization header using the getTokenSilently API.
<button id="call-api">Call an API</button>//with async/await
document.getElementById('call-api').addEventListener('click', async () => {
const accessToken = await auth0.getTokenSilently();
const result = await fetch('https://myapi.com', {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`
}
});
const data = await result.json();
console.log(data);
});Refresh tokens can be used to request new access tokens. Read more about how our refresh tokens work for browser-based applications to help you decide whether or not you need to use them.
To enable the use of refresh tokens, set the useRefreshTokens option to true:
await createAuth0Client({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',
useRefreshTokens: true,
authorizationParams: {
redirect_uri: '<MY_CALLBACK_URL>'
}
});Using this setting will cause the SDK to automatically send the offline_access scope to the authorization server. Refresh tokens will then be used to exchange for new access tokens instead of using a hidden iframe, and calls the /oauth/token endpoint directly. This means that in most cases the SDK does not rely on third-party cookies when using refresh tokens.
Note This configuration option requires Rotating Refresh Tokens to be enabled for your Auth0 Tenant.
In all cases where a refresh token is not available, the SDK falls back to the standard technique of using a hidden iframe with prompt=none to try and get a new access token and refresh token. This scenario would occur for example if you are using the in-memory cache and you have refreshed the page. In this case, any refresh token that was stored previously would be lost.
If the fallback mechanism fails, a login_required error will be thrown and could be handled in order to put the user back through the authentication process.
Note: This fallback mechanism does still require access to the Auth0 session cookie, so if third-party cookies are being blocked then this fallback will not work and the user must re-authenticate in order to get a new refresh token.
Refresh tokens from one API can be used to request new access tokens for another API. Read more about how MRRT works for browser-based applications to help you decide wether or not you need to use this funcfionality.
To enable the use of MRRT, set the useMrrt option to true, and as well enable the use of refresh tokens:
await createAuth0Client({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',
useRefreshTokens: true,
useMrrt: true,
authorizationParams: {
redirect_uri: '<MY_CALLBACK_URL>'
}
});Using this setting will make the SDK able to reuse the refresh token not only for APIs requested at login, but also for additional APIs allowed in the MRRT policy.
Note: This configuration option requires the refresh token policies of your application to be configured.
When working with multiple APIs, you can define different default scopes for each audience by passing an object instead of a string. This is particularly useful when different APIs require different default scopes:
await createAuth0Client({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',
useRefreshTokens: true,
useMrrt: true,
authorizationParams: {
redirect_uri: '<MY_CALLBACK_URL>',
audience: 'https://api.example.com', // Default audience
scope: {
'https://api.example.com':
'openid profile email offline_access read:products read:orders',
'https://analytics.example.com':
'openid profile email offline_access read:analytics write:analytics',
'https://admin.example.com':
'openid profile email offline_access read:admin write:admin delete:admin'
}
}
});How it works:
- Each key in the
scopeobject is anaudienceidentifier - The corresponding value is the scope string for that audience
- When calling
getAccessToken({ audience: "..." }), the SDK automatically uses the configured scopes for that audience. When scopes are also passed in the method call, they will be merged with the default scopes for that audience.
Note
This new option only works in the initialization of the client, it's not applicable to other runtime methods.
When using scope as an object, and no entry for the default audience is provided, the SDK will use the scopes of the DEFAULT_AUDIENCE. Those will be openid, email, profile and offline_access if useRefreshTokens is enabled.
The revokeRefreshToken() method explicitly revokes a refresh token via the /oauth/revoke endpoint (RFC 7009). This invalidates the refresh token so it can no longer be used to obtain new access tokens.
This method only has an effect when useRefreshTokens is true. If refresh tokens are disabled it returns immediately without doing anything.
Warning
In online access mode (refreshTokenMode: RefreshTokenMode.Online), revokeRefreshToken() behaves differently from offline mode:
- The ORT is revoked at the authorization server via
/oauth/revoke. - Because the ORT is session-bound, the Auth0 session is terminated server-side as part of revocation.
- The entire local cache is cleared immediately — the access token, ID token, and user profile are wiped.
isAuthenticated()returnsfalseandgetUser()returnsundefinedright away.
After calling revokeRefreshToken() in online mode, redirect the user to login — getTokenSilently() will fail because the session is gone. For a redirect-based sign-out, logout() achieves the same result.
// Revoke the refresh token for the default audience
await auth0.revokeRefreshToken();How it affects the cache:
- Offline mode: only the refresh token entry is cleared — the access token remains in cache until it expires. Once it expires,
getTokenSilently()will attempt silent auth (via iframe, ifuseRefreshTokensFallbackis enabled and the Auth0 session is still active) before requiring a new interactive login. - Online mode: the entire local cache is cleared (access token, ID token, user profile).
isAuthenticated()returnsfalseimmediately. The user must log in again.
Difference from logout():
- In offline mode,
revokeRefreshToken()invalidates the rotating refresh token at the server and strips it from the cache, but does not terminate the Auth0 session or clear the rest of the local cache (access token, ID token, user profile remain until they expire). - In online mode,
revokeRefreshToken()terminates the Auth0 session server-side and clears the entire local cache immediately — equivalent to a silentlogout()without a redirect.
In both modes, if you want a redirect-based sign-out, use logout() instead.
revokeRefreshToken() throws a GenericError if the /oauth/revoke endpoint returns an error (for example, if the token has already been revoked or is invalid). Wrap the call in a try/catch:
import { GenericError } from '@auth0/auth0-spa-js';
try {
await auth0.revokeRefreshToken();
} catch (e) {
if (e instanceof GenericError) {
console.error(e.error, e.error_description);
}
}If your application requests tokens for more than one audience, each audience may have its own refresh token. Call revokeRefreshToken() once per audience to revoke them all:
await auth0.revokeRefreshToken({ audience: 'https://api.example.com' });
await auth0.revokeRefreshToken({ audience: 'https://api2.example.com' });Omitting the audience option targets the audience configured in authorizationParams (or the default audience if none is set).
A single audience can accumulate more than one refresh token if different scope combinations were obtained through separate authorization flows. A single revokeRefreshToken() call handles all of them — the SDK collects every distinct refresh token stored for that audience and revokes them sequentially in one call.
If one revocation fails, the error is thrown immediately. Any tokens already revoked in that sequence are stripped from the cache; the remaining ones are left untouched.
When using Multi-Resource Refresh Tokens (MRRT), a single refresh token may cover multiple audiences. Revoking it for any one of those audiences invalidates the shared token and clears all cache entries that reference it:
// With MRRT, this single call revokes the shared token and
// cleans up all cache entries that reference it
await auth0.revokeRefreshToken();Note
Online Access (Online Refresh Tokens) support via SDKs is currently in Early Access. To request access to this feature, contact your Auth0 representative.
Warning
Online Refresh Tokens do not currently support resource servers with Ephemeral Sessions enabled. If a resource server has both allow_online_access and "Allow for Ephemeral Sessions" enabled, the authorization server issues an Online Refresh Token at login that is then rejected with invalid_grant ("Unknown or invalid refresh token") on the very next refresh — this is a known backend limitation, not a client-side defect. Until Ephemeral Sessions support is added for Online Refresh Tokens, disable "Allow for Ephemeral Sessions" on any resource server used with refreshTokenMode: RefreshTokenMode.Online.
Online Refresh Tokens (ORTs) are a refresh token type bound to the lifetime of the user's Auth0 session. See the Auth0 documentation on Online Refresh Tokens for the full conceptual overview. Unlike the rotating offline refresh tokens described above, an ORT is:
- Session-bound — it is valid only while the underlying Auth0 session is active. When the session ends (logout, idle/absolute session expiry, or an admin revoking the session), the ORT stops working.
- Non-rotating — refreshing an access token with an ORT does not issue a new refresh token. The same ORT is reused for the life of the session.
This makes ORTs a good fit for SPAs that want a refresh-token renewal path whose lifetime tracks the SSO session rather than living independently of it.
Important
Online access requires DPoP. Sender-constraining the token via DPoP is mandatory because the ORT is non-rotating — binding it to the browser's key pair is what mitigates token replay if it is exfiltrated. You must set useDpop: true explicitly; the SDK does not enable it for you.
Set refreshTokenMode to RefreshTokenMode.Online together with useRefreshTokens: true and useDpop: true:
import { createAuth0Client, RefreshTokenMode } from '@auth0/auth0-spa-js';
const auth0 = await createAuth0Client({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',
useRefreshTokens: true, // required — online access is a refresh-token grant
refreshTokenMode: RefreshTokenMode.Online,
useDpop: true, // required — DPoP is mandatory for online access
authorizationParams: {
redirect_uri: '<MY_CALLBACK_URL>'
}
});refreshTokenMode is a sub-option of useRefreshTokens. It defaults to RefreshTokenMode.Offline (the rotating offline refresh tokens described above); setting it to RefreshTokenMode.Online opts into Online Refresh Tokens. Always reference the exported RefreshTokenMode enum rather than hard-coding the mode.
Enabling this option causes the SDK to:
- Send the
online_accessscope to the authorization server (instead ofoffline_access). You do not need to add it toauthorizationParams.scopeyourself — the SDK injects it. - Route token renewal through the
refresh_tokengrant against/oauth/token(the same path used by offline refresh tokens), rather than a hidden iframe. - Store the non-rotating ORT in the existing cache and reuse it on every refresh, never replacing it.
Note
Online access is opt-in. When refreshTokenMode is unset or RefreshTokenMode.Offline, the SDK behaves exactly as before.
refreshTokenMode selects which refresh-token type the refresh-token grant uses. It is a sub-option of useRefreshTokens (which must be true for either mode) and defaults to RefreshTokenMode.Offline:
RefreshTokenMode.Offline (default) |
RefreshTokenMode.Online |
|
|---|---|---|
| Requires | useRefreshTokens: true |
useRefreshTokens: true + useDpop: true |
| Scope injected | offline_access |
online_access |
| Token lifetime | Independent of the session (survives logout until revoked/expired) | Bound to the Auth0 session |
| Rotation | Rotating (a new RT is issued on each refresh) | Non-rotating (same RT reused) |
| DPoP | Optional | Required (useDpop: true) |
The two modes inject mutually exclusive scopes (offline_access vs. online_access), so the SDK emits only one — it never sends both. You select between them with refreshTokenMode, not by combining flags.
The SDK enforces the DPoP requirement at two layers:
-
Compile-time (TypeScript). When you call
createAuth0ClientwithrefreshTokenMode: RefreshTokenMode.Online, the compiler requires bothuseRefreshTokens: trueanduseDpop: true:import { createAuth0Client, RefreshTokenMode } from '@auth0/auth0-spa-js'; // ❌ compile error: `useRefreshTokens: true` and `useDpop: true` are required for online mode createAuth0Client({ domain, clientId, refreshTokenMode: RefreshTokenMode.Online }); // ❌ compile error: `useDpop: true` is still required createAuth0Client({ domain, clientId, refreshTokenMode: RefreshTokenMode.Online, useRefreshTokens: true }); // ✅ valid createAuth0Client({ domain, clientId, refreshTokenMode: RefreshTokenMode.Online, useRefreshTokens: true, useDpop: true });
[!NOTE] The compile-time check narrows on the online mode value. A dynamically-typed value (e.g. a
refreshTokenModeread from config at runtime), anas anycast, or plain JavaScript all bypass it — which is why the runtime check below exists too. -
Runtime (all consumers, including plain JS). The
Auth0Clientconstructor throws anInvalidConfigurationErrorwhen online mode is requested butuseRefreshTokensoruseDpopis nottrue. The error'ssuggestiontells you exactly which option to set:import { createAuth0Client, RefreshTokenMode, InvalidConfigurationError } from '@auth0/auth0-spa-js'; try { const auth0 = await createAuth0Client({ domain: '<AUTH0_DOMAIN>', clientId: '<AUTH0_CLIENT_ID>', refreshTokenMode: RefreshTokenMode.Online, useRefreshTokens: true // missing useDpop: true }); } catch (e) { if (e instanceof InvalidConfigurationError) { console.error(e.error_description); // includes the suggested fix console.error(e.suggestion); // 'Set `useDpop: true` (DPoP is mandatory for online access).' } }
Because an ORT is bound to the Auth0 session, the way to invalidate it is to end the session with logout(), which clears the local cache and redirects to /v2/logout:
await auth0.logout({ logoutParams: { returnTo: window.location.origin } });After logout, the ORT is no longer valid; a subsequent getTokenSilently() falls through to the iframe fallback (if useRefreshTokensFallback is enabled) and ultimately to an interactive login.
Warning
In online mode, revokeRefreshToken() revokes the ORT at the authorization server and terminates the Auth0 session. The entire local cache (access token, ID token, user profile) is cleared immediately — isAuthenticated() returns false right away. Redirect the user to login after calling this. Use logout() instead if you want a redirect-based sign-out.
Online access is compatible with MRRT: a single ORT can be exchanged for access tokens across the audiences allowed by your refresh-token policies. The ORT remains non-rotating throughout — the same token is reused for every cross-audience exchange.
import { createAuth0Client, RefreshTokenMode } from '@auth0/auth0-spa-js';
const auth0 = await createAuth0Client({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',
useRefreshTokens: true,
refreshTokenMode: RefreshTokenMode.Online,
useDpop: true,
useMrrt: true,
authorizationParams: {
redirect_uri: '<MY_CALLBACK_URL>',
audience: 'https://api.example.com'
}
});The SDK can be configured to cache ID tokens and access tokens either in memory or in local storage. The default is in memory. This setting can be controlled using the cacheLocation option when creating the Auth0 client.
To use the in-memory mode, no additional options need are required as this is the default setting. To configure the SDK to cache data using local storage, set cacheLocation as follows:
await createAuth0Client({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',,
cacheLocation: 'localstorage' // valid values are: 'memory' or 'localstorage',
authorizationParams: {
redirect_uri: '<MY_CALLBACK_URL>'
}
});Important: This feature will allow the caching of data such as ID and access tokens to be stored in local storage. Exercising this option changes the security characteristics of your application and should not be used lightly. Extra care should be taken to mitigate against XSS attacks and minimize the risk of tokens being stolen from local storage.
The SDK can be configured to use a custom cache store that is implemented by your application. This is useful if you are using this SDK in an environment where more secure token storage is available, such as potentially a hybrid mobile app.
To do this, provide an object to the cache property of the SDK configuration.
The object should implement the following functions. Note that all of these functions can optionally return a Promise or a static value.
| Signature | Return type | Description |
|---|---|---|
get(key) |
Promise or object | Returns the item from the cache with the specified key, or undefined if it was not found |
set(key: string, object: any) |
Promise or void | Sets an item into the cache |
remove(key) |
Promise or void | Removes a single item from the cache at the specified key, or no-op if the item was not found |
allKeys() |
Promise<string[]> or string [] | (optional) Implement this if your cache has the ability to return a list of all keys. Otherwise, the SDK internally records its own key manifest using your cache. Note: if you only want to ensure you only return keys used by this SDK, the keys we use are prefixed with @@auth0spajs@@ |
Here's an example of a custom cache implementation that uses sessionStorage to store tokens and apply it to the Auth0 SPA SDK:
const sessionStorageCache = {
get: function (key) {
return JSON.parse(sessionStorage.getItem(key));
},
set: function (key, value) {
sessionStorage.setItem(key, JSON.stringify(value));
},
remove: function (key) {
sessionStorage.removeItem(key);
},
// Optional
allKeys: function () {
return Object.keys(sessionStorage);
}
};
await createAuth0Client({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',
cache: sessionStorageCache,
authorizationParams: {
redirect_uri: '<MY_CALLBACK_URL>'
}
});Note: The cache property takes precedence over the cacheLocation property if both are set. A warning is displayed in the console if this scenario occurs.
We also export the internal InMemoryCache and LocalStorageCache implementations, so you can wrap your custom cache around these implementations if you wish.
Organizations is a set of features that provide better support for developers who build and maintain SaaS and Business-to-Business (B2B) applications.
Log in to an organization by specifying the organization parameter when setting up the client:
await createAuth0Client({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',
authorizationParams: {
redirect_uri: '<MY_CALLBACK_URL>',
organization: '<MY_ORG_ID_OR_NAME>'
}
});You can also specify the organization when logging in:
// Using a redirect
await client.loginWithRedirect({
authorizationParams: {
organization: '<MY_ORG_ID_OR_NAME>'
}
});
// Using a popup window
await client.loginWithPopup({
authorizationParams: {
organization: '<MY_ORG_ID_OR_NAME>'
}
});When working with multiple organizations, there might be a situation where you want your users to be able to switch between different organizations.
To do this, clear the local logged in state from your application and login to Auth0 again, leveraging any existing Auth0 session to prevent the user from being prompted for their credentials.
async function switchOrganization(newOrganization: string) {
await client.logout({ openUrl: false });
await client.loginWithRedirect({
authorizationParams: {
organization: newOrganization
}
});
}Note: Ensure to pass any additional parameters to loginWithRedirect (or loginWithPopup) just as you might have passed on other occurences of calling login.
Accept a user invitation through the SDK by creating a route within your application that can handle the user invitation URL, and log the user in by passing the organization and invitation parameters from this URL. You can either use loginWithRedirect or loginWithPopup as needed.
const url = new URL(invitationUrl);
const params = new URLSearchParams(url.search);
const organization = params.get('organization');
const invitation = params.get('invitation');
if (organization && invitation) {
await client.loginWithRedirect({
authorizationParams: {
invitation,
organization
}
});
}Native to Web SSO enables seamless single sign-on when users transition from a native mobile application to a web application. The SDK can automatically extract session transfer tokens from URL query parameters and include them in authorization requests.
To enable Native to Web SSO, configure the sessionTransferTokenQueryParamName option with the name of the query parameter that contains the session transfer token:
const auth0 = await createAuth0Client({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',
sessionTransferTokenQueryParamName: 'session_transfer_token', // Enable and configure
authorizationParams: {
redirect_uri: '<MY_CALLBACK_URL>'
}
});
// When your web app is opened with:
// https://yourapp.com?session_transfer_token=xyz123
// The SDK automatically includes the token in authorization:
await auth0.loginWithRedirect();
// The /authorize request will include session_transfer_token=xyz123Default: The feature is disabled by default (undefined). You must explicitly configure a parameter name to enable it.
Important: After extracting the token, the SDK automatically removes it from the URL using window.history.replaceState(). This prevents the token from being accidentally reused on subsequent authentication requests, which is important since session transfer tokens are typically single-use.
You can configure the SDK to extract the session transfer token from any query parameter name. This is useful if your native app uses a custom parameter name:
const auth0 = await createAuth0Client({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',
sessionTransferTokenQueryParamName: 'stt', // Custom parameter name
authorizationParams: {
redirect_uri: '<MY_CALLBACK_URL>'
}
});
// When your web app is opened with:
// https://yourapp.com?stt=xyz123
// The SDK extracts the token and sends it to Auth0 as session_transfer_token:
await auth0.loginWithRedirect();The token is always sent to Auth0's /authorize endpoint as session_transfer_token, regardless of the parameter name you use in your app's URL.
The SDK supports Native to Web SSO with both loginWithRedirect() and loginWithPopup(). The session transfer token is automatically extracted from the URL and cleaned after use in both flows:
const auth0 = await createAuth0Client({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',
sessionTransferTokenQueryParamName: 'session_transfer_token'
});
// When your web app is opened with:
// https://yourapp.com?session_transfer_token=xyz123
// The SDK automatically includes the token in loginWithPopup:
await auth0.loginWithPopup();
// After login completes, the URL is cleaned:
// https://yourapp.comThis is particularly useful for web applications that prefer popup-based authentication flows, as the main page URL persists throughout the login process (unlike redirect flows where the browser navigates away).
You can also manually provide the session transfer token in authorizationParams, which overrides automatic detection:
// Extract token from URL manually
const params = new URLSearchParams(window.location.search);
const sessionTransferToken = params.get('my_custom_param');
if (sessionTransferToken) {
await auth0.loginWithRedirect({
authorizationParams: {
session_transfer_token: sessionTransferToken
}
});
}Note: Manually provided tokens take precedence over automatically detected ones. When you provide a token manually, the URL is not automatically cleaned.
When using Native to Web SSO with Organizations, ensure the organization parameter in your web application matches the organization associated with the session transfer token:
const auth0 = await createAuth0Client({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',
sessionTransferTokenQueryParamName: 'session_transfer_token'
});
// The native app authenticated with org_abc123
// The web app must use the same organization
await auth0.loginWithRedirect({
authorizationParams: {
organization: 'org_abc123'
// session_transfer_token is automatically included from URL
}
});If there is an organization mismatch, authentication will fail and the user will be prompted to log in again.
Enable secure token exchange between external identity providers and Auth0 using RFC 8693 standards.
// Initialize client with custom token exchange configuration
const auth0 = await createAuth0Client({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',
authorizationParams: {
audience: 'https://your-api.example.com'
}
});
// Exchange external token for Auth0 tokens and log user in
async function performTokenExchange() {
try {
// Option 1: Use client's default audience
const tokenResponse = await auth0.loginWithCustomTokenExchange({
subject_token: 'EXTERNAL_PROVIDER_TOKEN',
subject_token_type: 'urn:example:external-token',
scope: 'openid profile email'
// audience will default to audience from client config
});
// Option 2: Specify custom audience for this token exchange
const customTokenResponse = await auth0.loginWithCustomTokenExchange({
subject_token: 'EXTERNAL_PROVIDER_TOKEN',
subject_token_type: 'urn:example:external-token',
audience: 'https://different-api.example.com',
scope: 'openid profile read:records'
});
// Option 3: Exchange token within an organization context
const orgTokenResponse = await auth0.loginWithCustomTokenExchange({
subject_token: 'EXTERNAL_PROVIDER_TOKEN',
subject_token_type: 'urn:example:external-token',
organization: '<MY_ORG_ID_OR_NAME>', // Organization ID or name
scope: 'openid profile email'
});
console.log('Received tokens:', tokenResponse);
// User is now logged in - you can access user info
const user = await auth0.getUser();
console.log('Logged in user:', user);
} catch (error) {
console.error('Exchange failed:', error);
}
}
// Note: exchangeToken() is deprecated - use loginWithCustomTokenExchange() instead
⚠️ Deprecated —exchangeToken()will be removed in the next major version. UseloginWithCustomTokenExchange()instead.
- Create Token Exchange Profile in Auth0 Dashboard:
await managementClient.tokenExchangeProfiles.create({
action_id: 'custom-auth-action',
name: 'External System Exchange',
subject_token_type: 'urn:example:external-token',
type: 'custom_authentication'
});- Add Required Scopes to your API in Auth0:
urn:auth0:oauth2:grant-type:token-exchange
- Validate external tokens in Auth0 Actions using cryptographic verification
- Implement anti-replay mechanisms for subject tokens
- Store refresh tokens securely when using
offline_accessscope
async function safeTokenExchange() {
try {
return await auth0.loginWithCustomTokenExchange(/* ... */);
} catch (error) {
if (error.error === 'invalid_token') {
// Handle token validation errors
await auth0.logout();
window.location.reload();
}
if (error.error === 'insufficient_scope') {
// Request additional scopes
await auth0.loginWithPopup({
authorizationParams: {
scope: 'additional_scope_required'
}
});
}
}
}Use customTokenExchange() when one principal needs to act on behalf of another — for example, an AI agent acting on behalf of a user. Unlike loginWithCustomTokenExchange(), this method has no side effects: it does not update the session or affect isAuthenticated() / getUser().
Pass actor_token and actor_token_type alongside the subject token to identify the acting party per RFC 8693:
const tokenResponse = await auth0.customTokenExchange({
subject_token: '<USER_TOKEN>',
subject_token_type: 'urn:acme:user-token',
actor_token: '<AGENT_TOKEN>',
actor_token_type: 'https://idp.example.com/token-type/agent',
audience: 'https://api.example.com'
});
// Use tokenResponse.access_token to call a downstream API
// The current user session is unchangedToken Exchange Documentation RFC 8693 Spec
Demonstrating Proof-of-Possession —or simply DPoP— is a recent OAuth 2.0 extension defined in RFC9449.
It defines a mechanism for securely binding tokens to a specific device using cryptographic signatures. Without it, a token leak caused by XSS or other vulnerabilities could allow an attacker to impersonate the real user.
To support DPoP in auth0-spa-js, some APIs available in modern browsers are required:
-
Crypto API: allows to create and use cryptographic keys, which are used to generate the proofs (i.e. signatures) required for DPoP.
-
IndexedDB: enables the use of cryptographic keys without exposing the private material.
The following OAuth 2.0 flows are currently supported by auth0-spa-js:
-
Authorization Code Flow (
authorization_code). -
Refresh Token Flow (
refresh_token). -
Custom Token Exchange Flow (
urn:ietf:params:oauth:grant-type:token-exchange).
Important
Currently, only the ES256 algorithm is supported.
DPoP is disabled by default. To enable it, set the useDpop option to true when creating the SDK instance. For example:
const client = await createAuth0Client({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',
useDpop: true,
authorizationParams: {
redirect_uri: '<MY_CALLBACK_URL>'
}
});After enabling DPoP, every new session using a supported OAuth 2.0 flow in Auth0 will begin transparently to use tokens that are cryptographically bound to the current browser.
Important
DPoP will only be used for new user sessions created after enabling it. Any previously existing sessions will continue using non-DPoP tokens until the user logs in again.
You decide how to handle this transition. For example, you might require users to log in again the next time they use your application.
Note
Using DPoP requires storing some temporary data in the user's browser. When you log the user out with client.logout(), this data is deleted.
Tip
If all your clients are already using DPoP, you may want to increase security by making Auth0 reject any non-DPoP interactions. See the docs on Sender Constraining for details.
You use a DPoP token the same way as a "traditional" access token, except it must be sent to the server with an Authorization: DPoP <token> header instead of the usual Authorization: Bearer <token>.
To determine the type of a token, use the detailedResponse option in getTokenSilently() to access the token_type property, which will be either DPoP or Bearer.
For internal requests sent by auth0-spa-js to Auth0, simply enable the useDpop option and every interaction with Auth0 will be protected.
However, to use DPoP with a custom, external API, some additional work is required. The Auth0Client class provides some low-level methods to help with this:
getDpopNonce()setDpopNonce()generateDpopProof()
However, due to the nature of how DPoP works, this is not a trivial task:
- When a nonce is missing or expired, the request may need to be retried.
- Received nonces must be stored and managed.
- DPoP headers must be generated and included in every request, and regenerated for retries.
Because of this, we recommend using the provided fetchWithAuth() method, which handles all of this for you.
The fetchWithAuth() method is a drop-in replacement for the native fetch() function from the Fetch API, so if you're already using it, the change will be minimal.
For example, if you had this code:
await fetch('https://api.example.com/foo', {
method: 'GET',
headers: { 'user-agent': 'My Client 1.0' }
});
console.log(response.status);
console.log(response.headers);
console.log(await response.json());You would change it as follows:
const fetcher = client.createFetcher({
dpopNonceId: 'my_api_request'
});
await fetcher.fetchWithAuth('https://api.example.com/foo', {
method: 'GET',
headers: { 'user-agent': 'My Client 1.0' }
});
console.log(response.status);
console.log(response.headers);
console.log(await response.json());When using fetchWithAuth(), the following will be handled for you automatically:
- Use
getTokenSilently()to get the access token to inject in the headers. - Generate and inject DPoP headers when needed.
- Store and update any DPoP nonces.
- Handle retries caused by a rejected nonce.
Important
If your API requires DPoP, a dpopNonceId must be present in the createFetcher() parameters, since it’s used to keep track of the DPoP nonces for each request.
If you need something more complex than the example above, you can provide a custom implementation in the fetch property.
However, since auth0-spa-js needs to make decisions based on HTTP responses, your implementation must return an object with at least two properties:
status: the response status code as a number.headers: the response headers as a plain object or as a Fetch API’s Headers-like interface.
Whatever it returns, it will be passed as the output of the fetchWithAuth() method.
Your implementation will be called with a standard, ready-to-use Request object, which will contain any headers needed for authorization and DPoP usage (if enabled). Depending on your needs, you can use this object directly or treat it as a container with everything required to make the request your own way.
const fetcher = client.createFetcher({
dpopNonceId: 'my_api_request',
fetch: (request) =>
// The `Request` object has everything you need to do a request in a
// different library. Make sure that your output meets the requirements
// about the `status` and `headers` properties.
axios.request({
url: request.url,
method: request.method,
data: request.body,
headers: Object.fromEntries(request.headers),
timeout: 2000,
// etc.
}),
},
});
const response = await fetcher.fetchWithAuth('https://api.example.com/foo', {
method: 'POST',
body: JSON.stringify({ name: 'John Doe' }),
headers: { 'user-agent': 'My Client 1.0' },
});
console.log(response.status);
console.log(response.headers);
console.log(response.data);The Fetch API doesn’t support passing a timeout value directly; instead, you’re expected to use an AbortSignal. For example:
const fetcher = client.createFetcher();
await fetcher.fetchWithAuth('https://api.example.com/foo', {
signal: AbortSignal.timeout(2000)
});This works, but if you define your request parameters statically when your app starts and then call fetchWithAuth() after an indeterminate amount of time, you'll find that the request will timeout immediately. This happens because the AbortSignal starts counting time as soon as it is created.
To work around this, you can pass a thin wrapper over the native fetch() so that a new AbortSignal is created each time a request is made:
const fetcher = client.createFetcher({
fetch: request => fetch(request, { signal: AbortSignal.timeout(2000) })
});
await fetcher.fetchWithAuth('https://api.example.com/foo');If you need to make requests to different endpoints of the same API, passing a baseUrl to createFetcher() can be useful:
const fetcher = client.createFetcher({
baseUrl: 'https://api.example.com'
});
await fetcher.fetchWithAuth('/foo'); // => https://api.example.com/foo
await fetcher.fetchWithAuth('/bar'); // => https://api.example.com/bar
await fetcher.fetchWithAuth('/xyz'); // => https://api.example.com/xyz
// If the passed URL is absolute, `baseUrl` will be ignored for convenience:
await fetcher.fetchWithAuth('https://other-api.example.com/foo');The fetchWithAuth() method assumes you’re using the SDK to get the access token for the request. This means that by default, it will always call getTokenSilently() internally before making the request.
However, if you already have an access token or need to pass specific parameters to getTokenSilently(), you can override this behavior with a custom access token factory, like so:
client.createFetcher({
getAccessToken: () =>
client.getTokenSilently({
authorizationParams: {
audience: '<SOME_AUDIENCE>',
scope: '<SOME_SCOPE>'
// etc.
}
}),
detailedResponse: true // If you need a mix of DPoP and Bearer tokens per fetcher, it will need to know the token type.
});The Connect Accounts feature uses the Auth0 My Account API to allow users to link multiple third party accounts to a single Auth0 user profile.
When using Connected Accounts, Auth0 acquires tokens from upstream Identity Providers (like Google) and stores them in a secure Token Vault. These tokens can then be used to access third-party APIs (like Google Calendar) on behalf of the user.
The tokens in the Token Vault are then accessible to Resource Servers (APIs) configured in Auth0. The SPA application can then issue requests to the API, which can retrieve the tokens from the Token Vault and use them to access the third-party APIs.
This is particularly useful for applications that require access to different resources on behalf of a user, like AI Agents.
The SDK must be configured with an audience (an API Identifier) - this will be the resource server that uses the tokens from the Token Vault.
The SDK must also be configured to use refresh tokens and MRRT (Multiple Resource Refresh Tokens) since we will use the refresh token grant to get Access Tokens for the My Account API in addition to the API we are calling.
The My Account API requires DPoP tokens, so we also need to enable DPoP.
const auth0 = new Auth0Client({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',
useRefreshTokens: true,
useMrrt: true,
useDpop: true,
authorizationParams: {
redirect_uri: '<MY_CALLBACK_URL>'
}
});Use the login methods to authenticate to the application and get a refresh and access token for the API.
// Login specifying any scopes for the Auth0 API
await auth0.loginWithRedirect({
authorizationParams: {
audience: '<AUTH0 API IDENTIFIER>',
scope: 'openid profile email read:calendar'
}
});
// Handle redirect callback on login.
const query = new URLSearchParams(window.location.search);
if ((query.has('code') || query.has('error')) && query.has('state')) {
await auth0.handleRedirectCallback();
const user = await auth0.getUser();
console.log(user);
}Use the new connectAccountWithRedirect method to redirect the user to the third party Identity Provider to connect their account.
// Start the connect flow by redirecting to the thrid party API's login, defined as an Auth0 connection
await auth0.connectAccountWithRedirect({
connection: '<CONNECTION eg, google-apps-connection>',
scopes: ['<SCOPE eg https://www.googleapis.com/auth/calendar.acls.readonly>'],
authorizationParams: {
// additional authorization params to forward to the authorization server
}
});
// Handle redirect callback on connect. *Note* the `connect_code` param
const query = new URLSearchParams(window.location.search);
if ((query.has('connect_code') || query.has('error')) && query.has('state')) {
const result = await auth0.handleRedirectCallback();
if (result.connection) {
console.log(`You are connected to ${result.connection}!`);
}
}You can now call the API with your access token and the API can use Access Token Exchange with Token Vault to get tokens from the Token Vault to access third party APIs on behalf of the user.
Important
You must enable Offline Access from the Connection Permissions settings to be able to use the connection with Connected Accounts.
After initializing the Auth0Client, you can retrieve the configuration details:
import { createAuth0Client } from '@auth0/auth0-spa-js';
const auth0 = await createAuth0Client({
domain: 'YOUR_DOMAIN',
clientId: 'YOUR_CLIENT_ID'
});
// Get configuration
const config = auth0.getConfiguration();
console.log(config.domain, config.clientId);This is useful when you need to:
- Display the current domain to the user
- Log configuration for debugging
- Pass configuration to other services or analytics
- Verify the SDK is configured correctly
Passkeys provide password-less authentication using platform biometrics (Face ID, Touch ID, Windows Hello) or security keys via the WebAuthn standard. The SDK supports two flows:
- Signup: Register a new user with a passkey
- Login: Authenticate an existing user with a passkey
- Important: Use Refresh Tokens with Passkeys
- Signup with Passkey
- Login with Passkey
- Granular Passkey APIs
- Complete Passkey Flow Example
- Error Handling
Before using passkeys, ensure the following are configured in your Auth0 Dashboard:
- Enable passkey authentication method: Go to Authentication > Database > your connection > Authentication Methods > Passkey.
- Enable the WebAuthn passkey grant: Go to your Application > Advanced Settings > Grant Types and enable the Passkey grant.
- Custom domain required: Passkeys are bound to an origin (domain). A custom domain must be configured — passkeys will not work on the default
*.auth0.comdomain.
Important
When using passkeys, you must configure the SDK with useRefreshTokens: true.
Passkey authentication uses a direct token exchange (/oauth/token with the WebAuthn grant type). It does not create an Auth0 session cookie because there is no redirect to /authorize. This means that when the access token expires, the SDK cannot silently obtain a new one using an iframe (which relies on the Auth0 session cookie via prompt=none).
Without refresh tokens, getTokenSilently() will either:
- Fail with a
login_requirederror (if no Auth0 session exists), or - Return tokens for a different user if a separate Auth0 session cookie exists from a prior redirect-based login, causing an unintended session swap.
To avoid this, always enable refresh tokens:
const auth0 = await createAuth0Client({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',
useRefreshTokens: true, // Required for passkey-based sessions
authorizationParams: {
redirect_uri: '<MY_CALLBACK_URL>'
}
});You must also enable Refresh Token Rotation in your Auth0 Dashboard under Applications > your app > Settings > Refresh Token Rotation.
Register a new user with a passkey. The SDK handles the entire flow internally: requesting a challenge from Auth0, triggering the browser's WebAuthn credential creation ceremony, serializing the result, and exchanging it for tokens.
import { createAuth0Client } from '@auth0/auth0-spa-js';
const auth0 = await createAuth0Client({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',
authorizationParams: {
redirect_uri: '<MY_CALLBACK_URL>'
}
});
// One call handles everything — the user sees the biometric prompt
const tokens = await auth0.passkey.signup({
email: 'user@example.com',
name: 'Jane Doe' // optional display name
});
// User is now logged in — getUser() works immediately
const user = await auth0.getUser();
console.log('Signed up:', user);You can also pass scope and audience to control the access token:
const tokens = await auth0.passkey.signup({
email: 'user@example.com',
scope: 'openid profile email read:products',
audience: 'https://api.example.com'
});To register a user within an organization context:
const tokens = await auth0.passkey.signup({
email: 'user@example.com',
organization: 'org_abc123'
});const tokens = await auth0.passkey.signup({
// At least one identifier is required
email: 'user@example.com',
phoneNumber: '+1234567890', // optional: E.164 format
username: 'janedoe', // optional
// Profile fields (all optional)
name: 'Jane Doe',
givenName: 'Jane',
familyName: 'Doe',
nickname: 'janie',
picture: 'https://example.com/avatar.png',
userMetadata: { plan: 'pro' },
// Connection and org
realm: 'my-db-connection',
organization: 'org_abc123',
// Token options
scope: 'openid profile email',
audience: 'https://api.example.com'
});Note
passkey.signup() and passkey.login() cache tokens and establish a session automatically, just like loginWithRedirect(). After calling them, isAuthenticated(), getUser(), and getTokenSilently() all work as expected.
Remember to configure useRefreshTokens: true. See Important: Use Refresh Tokens with Passkeys.
Authenticate an existing user with their registered passkey. Like signup, a single call handles the entire flow.
const tokens = await auth0.passkey.login();
const user = await auth0.getUser();
console.log('Logged in:', user);If your tenant has multiple database connections with passkeys enabled, specify the realm:
const tokens = await auth0.passkey.login({
realm: 'Username-Password-Authentication'
});To authenticate within an organization context:
const tokens = await auth0.passkey.login({
organization: 'org_abc123'
});For advanced use cases where you need fine-grained control, you can use the individual API methods to handle each step of the passkey flow separately.
Request a passkey signup challenge to start the granular signup flow:
const challenge = await auth0.passkey.getSignupChallenge({
email: 'user@example.com',
name: 'Jane Doe' // optional display name
});
// challenge.authSession — save this to complete signup later
// challenge.publicKey — pass to navigator.credentials.create()Request a passkey login challenge to start the granular login flow:
const challenge = await auth0.passkey.getLoginChallenge({
realm: 'Username-Password-Authentication' // optional
});
// challenge.authSession — save this to complete login later
// challenge.publicKey — pass to navigator.credentials.get()Exchange a signed credential for tokens. This is the final step after running the WebAuthn ceremony:
// After navigator.credentials.create() or navigator.credentials.get()
const credential = await navigator.credentials.create({
publicKey: challenge.publicKey
});
const tokens = await auth0.passkey.getTokenWithPasskey({
authSession: challenge.authSession,
credential: credential, // the raw PublicKeyCredential
scope: 'openid profile email',
audience: 'https://api.example.com'
});async function granularSignup(email, displayName) {
// Step 1: Get challenge
const challenge = await auth0.passkey.getSignupChallenge({
email,
name: displayName
});
// Step 2: Create credential
const credential = await navigator.credentials.create({
publicKey: challenge.publicKey
});
if (!credential) {
throw new Error('Credential creation cancelled');
}
// Step 3: Exchange for tokens
const tokens = await auth0.passkey.getTokenWithPasskey({
authSession: challenge.authSession,
credential
});
return tokens;
}async function granularLogin() {
// Step 1: Get challenge
const challenge = await auth0.passkey.getLoginChallenge();
// Step 2: Get credential
const credential = await navigator.credentials.get({
publicKey: challenge.publicKey
});
if (!credential) {
throw new Error('Credential assertion cancelled');
}
// Step 3: Exchange for tokens
const tokens = await auth0.passkey.getTokenWithPasskey({
authSession: challenge.authSession,
credential
});
return tokens;
}Note
The granular APIs (getSignupChallenge, getLoginChallenge, getTokenWithPasskey) provide the same automatic token caching and session establishment as the simplified signup() and login() methods. After successful completion, isAuthenticated(), getUser(), and getTokenSilently() work as expected.
import { createAuth0Client } from '@auth0/auth0-spa-js';
const auth0 = await createAuth0Client({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',
useRefreshTokens: true,
authorizationParams: {
redirect_uri: '<MY_CALLBACK_URL>'
}
});
// --- Signup (single call) ---
async function signupWithPasskey(email, displayName) {
await auth0.passkey.signup({ email, name: displayName });
return await auth0.getUser();
}
// --- Login (single call) ---
async function loginWithPasskey() {
await auth0.passkey.login();
return await auth0.getUser();
}Tip
Both signup() and login() throw an Error with a descriptive message if the user cancels the biometric prompt (i.e., the WebAuthn API returns null). Wrap calls in try/catch to handle cancellation, network failures, or misconfigured connections.
The MFA API allows you to manage multi-factor authentication for users. The SDK automatically handles MFA context, eliminating the need for manual parsing of error payloads.
Note
Multi Factor Authentication support via SDKs is currently in Early Access. To request access to this feature, contact your Auth0 representative.
- Understanding the MFA Response
- Handling MFA required errors
- Getting Authenticators
- Getting Enrollment Factors
- Enrollment
- Challenge
- Verify
- Complete MFA Flow Example
- Error Handling
Before using the MFA API, configure MFA in your Auth0 Dashboard under Security > Multi-factor Auth. For detailed configuration, see the Auth0 MFA documentation.
When MFA is required, the error payload contains an mfa_requirements object that indicates either a challenge flow (user has enrolled authenticators) or an enroll flow (user needs to set up MFA).
Challenge Flow Response (user has existing authenticators):
{
"error": "mfa_required",
"error_description": "Multifactor authentication required",
"mfa_token": "Fe26.2*...",
"mfa_requirements": {
"challenge": [
{ "type": "otp" },
{ "type": "email" }
...
]
}
}Enroll Flow Response (user needs to enroll an authenticator):
{
"error": "mfa_required",
"error_description": "Multifactor authentication required",
"mfa_token": "Fe26.2*...",
"mfa_requirements": {
"enroll": [
{ "type": "otp" },
{ "type": "phone" },
{ "type": "push-notification" }
...
]
}
}Based on the response:
mfa_requirements.challenge: User has enrolled authenticators → proceed with List Authenticators → Challenge → Verify flowmfa_requirements.enroll: User needs to set up MFA → proceed with Enroll → Verify flow
Note
The SDK handles this logic automatically. When you call getEnrollmentFactors() or getAuthenticators(), the SDK uses the stored context to return the appropriate data.
When MFA is required, the SDK automatically stores the context. You can then call MFA methods with just the token:
try {
await auth0.getTokenSilently();
} catch (error) {
if (error instanceof MfaRequiredError) {
// Check if enrollment is required
const enrollmentFactors = await auth0.mfa.getEnrollmentFactors(error.mfa_token);
if (enrollmentFactors.length > 0) {
// User needs to enroll - show enrollment options
console.log('Available enrollment factors:', enrollmentFactors);
} else {
// User has enrolled authenticators - proceed with challenge
const authenticators = await auth0.mfa.getAuthenticators(error.mfa_token);
console.log('Available authenticators:', authenticators);
}
}
}The SDK automatically filters authenticators based on challenge types from the MFA context:
try {
await auth0.getTokenSilently();
} catch (error) {
if (error instanceof MfaRequiredError) {
const authenticators = await auth0.mfa.getAuthenticators(error.mfa_token);
// SDK automatically filters by challenge types from the error
showAuthenticatorPicker(authenticators);
}
}Check what MFA factors are available for enrollment:
try {
const factors = await auth0.mfa.getEnrollmentFactors(mfaToken);
if (factors.length > 0) {
console.log('Available enrollment options:', factors);
showEnrollmentOptions(factors);
} else {
console.log('User already enrolled');
}
} catch (error) {
if (error instanceof MfaEnrollmentFactorsError) {
console.error('Could not retrieve enrollment factors:', error.error_description);
}
}Enrolling OTP (Authenticator App) auth0-docs
// Enroll OTP authenticator (Google Authenticator, Microsoft Authenticator, etc.)
const enrollment = await auth0.mfa.enroll({
mfaToken: mfaToken,
factorType: 'otp'
});
// Display QR code to user
const qrCodeUri = enrollment.barcodeUri; // otpauth://totp/...
const secret = enrollment.secret; // Base32 secret for manual entry// Enroll SMS authenticator
const smsEnrollment = await auth0.mfa.enroll({
mfaToken: mfaToken,
factorType: 'sms',
phoneNumber: '+12025551234' // E.164 format
});
const oobCode = smsEnrollment.oobCode; // Use this code to complete enrollment verification;// Enroll Voice authenticator
const voiceEnrollment = await auth0.mfa.enroll({
mfaToken: mfaToken,
factorType: 'voice',
phoneNumber: '+12025551234' // E.164 format
});
const oobCode = voiceEnrollment.oobCode; // Use this code to complete enrollment verification// Enroll Email authenticator
const emailEnrollment = await auth0.mfa.enroll({
mfaToken: mfaToken,
factorType: 'email',
email: 'user@example.com'
});
const oobCode = emailEnrollment.oobCode; // Use this code to complete enrollment verification// Enroll Push Notification authenticator (Auth0 Guardian)
const pushEnrollment = await auth0.mfa.enroll({
mfaToken: mfaToken,
factorType: 'push'
});
// Display QR code for Guardian app enrollment
const qrCodeUri = pushEnrollment.barcodeUri; // Scan with Auth0 Guardian app
const oobCode = pushEnrollment.oobCode;
// User scans QR code with Auth0 Guardian mobile app
// Push notifications will be used for future MFA challenges// Initiate SMS challenge - sends code via text message
const challenge = await auth0.mfa.challenge({
mfaToken: mfaToken,
challengeType: 'oob',
authenticatorId: 'sms|dev_xxx'
});
const oobCode = challenge.oobCode; // Save for verification
// User will receive SMS with verification code// Initiate Email challenge - sends code via email
const challenge = await auth0.mfa.challenge({
mfaToken: mfaToken,
challengeType: 'oob',
authenticatorId: 'email|dev_xxx'
});
const oobCode = challenge.oobCode; // Save for verification
// User will receive email with verification code// Initiate Push Notification challenge - sends push to Guardian app
const challenge = await auth0.mfa.challenge({
mfaToken: mfaToken,
challengeType: 'oob',
authenticatorId: 'push|dev_xxx'
});
const oobCode = challenge.oobCode; // Save for verification
// User receives push notification on their Auth0 Guardian mobile app
// They approve/deny the authentication requestNote
Once you have successfully enrolled an OTP factor, you do not need to explicitly call the challenge method to generate a code. The code is generated automatically by your authenticator app—simply open it and provide the displayed code in the verify call.
// Verify MFA challenge and get tokens
const tokens = await auth0.mfa.verify({
mfaToken: mfaToken,
oobCode: challenge.oobCode,
bindingCode: '123456' // Code user received via SMS
});
const accessToken = tokens.access_token; // Use to call your API
const idToken = tokens.id_token; // Contains user identity information// Verify OTP code from authenticator app
const tokens = await auth0.mfa.verify({
mfaToken: mfaToken,
otp: '123456' // 6-digit code from authenticator app
});
const accessToken = tokens.access_token;
const idToken = tokens.id_token;// Challenge the push notification authenticator
const challenge = await auth0.mfa.challenge({
mfaToken: mfaToken,
challengeType: 'oob',
authenticatorId: 'push|dev_xxx' // Push authenticator ID
});
// User receives push notification on their mobile device
// They approve the request in the Auth0 Guardian app
// Poll or wait for user to approve, then verify
const tokens = await auth0.mfa.verify({
mfaToken: mfaToken,
oobCode: challenge.oobCode,
bindingCode: 'APPROVAL_CODE' // Code from Guardian app (if binding required)
});
const accessToken = tokens.access_token;
const idToken = tokens.id_token;Recovery codes can be used to complete MFA verification without initiating a challenge. Each recovery code can only be used once.
const tokens = await auth0.mfa.verify({
mfaToken: mfaToken,
recoveryCode: 'XXXX-XXXX-XXXX' // One of the recovery codes
});
const accessToken = tokens.access_token;
const idToken = tokens.id_token;Here's a complete example showing enrollment and challenge flows:
Tip
See a complete MFA implementation in static/mfa_flow.html that demonstrates enrollment, challenge, and verification flows.
async function handleMfaFlow() {
try {
await auth0.getTokenSilently();
} catch (error) {
if (error instanceof MfaRequiredError) {
const mfaToken = error.mfa_token;
// Check if enrollment is needed
const enrollmentFactors = await auth0.mfa.getEnrollmentFactors(mfaToken);
if (enrollmentFactors.length > 0) {
// User needs to enroll
const selectedFactor = await showEnrollmentUI(enrollmentFactors);
// Enroll based on user selection
if (selectedFactor.type === 'otp') {
const enrollment = await auth0.mfa.enroll({
mfaToken: mfaToken,
factorType: 'otp'
});
await showQRCode(enrollment.barcodeUri);
// User scans QR and enters code to verify enrollment
const verifyCode = await promptUserForCode();
const tokens = await auth0.mfa.verify({
mfaToken: mfaToken,
otp: verifyCode
});
return tokens;
}
} else {
// User has authenticators - proceed with challenge
const authenticators = await auth0.mfa.getAuthenticators(mfaToken);
const selected = await showAuthenticatorPicker(authenticators);
// Initiate challenge
const challenge = await auth0.mfa.challenge({
mfaToken: mfaToken,
challengeType: selected.type === 'otp' ? 'otp' : 'oob',
authenticatorId: selected.id
});
// Get code from user
const code = await promptUserForCode();
// Verify
const tokens = await auth0.mfa.verify({
mfaToken: mfaToken,
otp: selected.type === 'otp' ? code : undefined,
oobCode: selected.type !== 'otp' ? challenge.oobCode : undefined,
bindingCode: selected.type !== 'otp' ? code : undefined
});
return tokens;
}
}
}
}Each MFA operation has its own typed error for precise error handling:
import {
MfaEnrollmentError,
MfaListAuthenticatorsError,
MfaChallengeError,
MfaVerifyError,
MfaEnrollmentFactorsError
} from '@auth0/auth0-spa-js';
// Get authenticators
try {
const authenticators = await auth0.mfa.getAuthenticators(mfaToken);
} catch (error) {
if (error instanceof MfaListAuthenticatorsError) {
console.error('Failed to get authenticators:', error.error_description);
}
}
// Get enrollment factors
try {
const factors = await auth0.mfa.getEnrollmentFactors(mfaToken);
} catch (error) {
if (error instanceof MfaEnrollmentFactorsError) {
console.error('Context not found:', error.error_description);
// MFA token may have expired - restart the flow
}
}
// Enroll authenticator
try {
const enrollment = await auth0.mfa.enroll({
mfaToken,
factorType: 'otp'
});
} catch (error) {
if (error instanceof MfaEnrollmentError) {
console.error('Enrollment failed:', error.error_description);
}
}
// Challenge authenticator
try {
const challenge = await auth0.mfa.challenge({
mfaToken,
challengeType: 'otp',
authenticatorId
});
} catch (error) {
if (error instanceof MfaChallengeError) {
console.error('Challenge failed:', error.error_description);
}
}
// Verify challenge
try {
const tokens = await auth0.mfa.verify({
mfaToken,
otp: '123456'
});
} catch (error) {
if (error instanceof MfaVerifyError) {
if (error.error === 'invalid_otp') {
console.error('Invalid code entered');
} else if (error.error === 'expired_token') {
console.error('MFA token expired - restart flow');
}
}
}Note
You may also encounter an MfaRequiredError if you have multiple challenge factors configured.
Step-up authentication lets you request elevated access for sensitive operations (e.g. a specific audience or scope) and automatically handle MFA challenges via a popup, without manually catching errors or managing the MFA API.
When getTokenSilently() encounters an MFA step-up error and interactiveErrorHandler is configured, the SDK automatically opens a Universal Login popup to complete MFA, then returns the token. This works regardless of whether you use refresh tokens (useRefreshTokens: true) or the default configuration.
Enable the interactive error handler when creating the client. Step-up authentication works with or without refresh tokens — no additional configuration is needed. When using refresh tokens, consider combining with Multi-Resource Refresh Tokens (MRRT), which allow a single refresh token to obtain access tokens for multiple APIs — making step-up requests across different audiences seamless.
const auth0 = await createAuth0Client({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',
interactiveErrorHandler: 'popup',
useRefreshTokens: true, // optional — works with or without refresh tokens
useMrrt: true, // optional — useful when stepping up across multiple APIs
authorizationParams: {
redirect_uri: '<MY_CALLBACK_URL>'
}
});Call getTokenSilently() with the audience and scope that require step-up authentication. If MFA is required, the popup opens automatically and the token is returned once the user completes the challenge — no manual error handling needed.
const accessToken = await auth0.getTokenSilently({
authorizationParams: {
audience: 'https://api.example.com',
scope: 'read:sensitive-data'
}
});
const result = await fetch('https://api.example.com/sensitive', {
headers: { Authorization: `Bearer ${accessToken}` }
});The MFA challenge itself is handled automatically, but popup lifecycle errors can still occur. These are thrown to the caller:
import {
PopupOpenError,
PopupCancelledError,
PopupTimeoutError
} from '@auth0/auth0-spa-js';
try {
const accessToken = await auth0.getTokenSilently({
authorizationParams: {
audience: 'https://api.example.com',
scope: 'read:sensitive-data'
}
});
} catch (error) {
if (error instanceof PopupOpenError) {
// Browser blocked the popup — prompt user to allow popups
}
if (error instanceof PopupCancelledError) {
// User closed the popup before completing MFA
}
if (error instanceof PopupTimeoutError) {
// Popup did not complete within the allowed time
}
}Note
If interactiveErrorHandler is not configured, MFA errors are thrown to the caller as usual. When using refresh tokens, you can handle MfaRequiredError manually using the MFA API.
The MyAccount API lets you manage the current user's authentication methods, factors, and connected accounts directly from the SPA.
Note
The MyAccount API requires refresh tokens and MRRT if your app is configured with a custom API audience. DPoP is supported but optional.
Get the list of MFA factors and their enabled status for the current user.
const factors = await auth0.myAccount.getFactors();
// [{ type: 'totp', usage: ['secondary'] }, { type: 'phone', usage: ['secondary'] }]const methods = await auth0.myAccount.getAuthenticationMethods();const passkeys = await auth0.myAccount.getAuthenticationMethods('passkey');const method = await auth0.myAccount.getAuthenticationMethod('am_abc123');await auth0.myAccount.deleteAuthenticationMethod('am_abc123');// Rename any method
const updated = await auth0.myAccount.updateAuthenticationMethod('am_abc123', {
name: 'My Work Laptop'
});
// Change preferred delivery method for phone
const updated = await auth0.myAccount.updateAuthenticationMethod('am_abc123', {
preferred_authentication_method: 'voice'
});Enrollment is a two-step flow: get a challenge, then verify the credential.
// Step 1: get the WebAuthn creation challenge
const challenge = await auth0.myAccount.enrollmentChallenge({ type: 'passkey' });
// Step 2: trigger the browser ceremony
const credential = await navigator.credentials.create({
publicKey: {
...challenge.authn_params_public_key,
challenge: base64urlToBuffer(challenge.authn_params_public_key.challenge),
user: {
...challenge.authn_params_public_key.user,
id: base64urlToBuffer(challenge.authn_params_public_key.user.id)
}
}
});
// Step 3: verify and complete enrollment
const method = await auth0.myAccount.enrollmentVerify({
type: 'passkey',
location: challenge.location,
auth_session: challenge.auth_session,
authn_response: serializeCredential(credential)
});// Step 1: request OTP to the phone number
const challenge = await auth0.myAccount.enrollmentChallenge({
type: 'phone',
phone_number: '+15551234567',
preferred_authentication_method: 'sms'
});
// Step 2: verify with the OTP the user received
await auth0.myAccount.enrollmentVerify({
type: 'phone',
location: challenge.location,
auth_session: challenge.auth_session,
otp_code: '123456'
});const challenge = await auth0.myAccount.enrollmentChallenge({
type: 'email',
email: 'user@example.com'
});
await auth0.myAccount.enrollmentVerify({
type: 'email',
location: challenge.location,
auth_session: challenge.auth_session,
otp_code: '123456'
});const challenge = await auth0.myAccount.enrollmentChallenge({ type: 'totp' });
// challenge.barcode_uri — show this as a QR code for the user to scan
// challenge.manual_input_code — fallback manual entry code
await auth0.myAccount.enrollmentVerify({
type: 'totp',
location: challenge.location,
auth_session: challenge.auth_session,
otp_code: '123456'
});const challenge = await auth0.myAccount.enrollmentChallenge({ type: 'push-notification' });
// challenge.barcode_uri — show this as a QR code to link the authenticator app
// No OTP needed — user approves on their device
await auth0.myAccount.enrollmentVerify({
type: 'push-notification',
location: challenge.location,
auth_session: challenge.auth_session
});const challenge = await auth0.myAccount.enrollmentChallenge({ type: 'recovery-code' });
// challenge.recovery_code — display this to the user to save securely
// Verify just confirms the user has saved the code
await auth0.myAccount.enrollmentVerify({
type: 'recovery-code',
location: challenge.location,
auth_session: challenge.auth_session
});const challenge = await auth0.myAccount.enrollmentChallenge({ type: 'password' });
await auth0.myAccount.enrollmentVerify({
type: 'password',
location: challenge.location,
auth_session: challenge.auth_session,
new_password: 'newSecurePassword123!'
});All MyAccount API errors throw MyAccountApiError with RFC 7807 fields.
import { MyAccountApiError } from '@auth0/auth0-spa-js';
try {
await auth0.myAccount.enrollmentChallenge({ type: 'passkey' });
} catch (err) {
if (err instanceof MyAccountApiError) {
console.error(err.status, err.title, err.detail);
if (err.validation_errors) {
err.validation_errors.forEach(e => console.error(e.field, e.detail));
}
}
}
try {
await auth0.myAccount.deleteAuthenticationMethod('am_abc123');
} catch (err) {
if (err instanceof MyAccountApiError) {
console.error(err.status, err.title, err.detail);
}
}Important
interactiveErrorHandler only affects getTokenSilently(). Other methods like loginWithPopup() and loginWithRedirect() are not affected.
When using Okta or OIDC enterprise connections configured with id_token_session_expiry_supported: true, Auth0 includes a session_expiry claim in the ID token. This represents the latest moment the upstream identity provider considers the session valid — an absolute Unix timestamp in seconds.
You can also emit this claim via a Post-Login Action:
exports.onExecutePostLogin = async (event, api) => {
// IMPORTANT: value must be Unix seconds, not milliseconds.
// Wrong: Date.now() + 7200000 — milliseconds, throws invalid_token at login
// Correct: Math.floor(Date.now() / 1000) + 7200 — 2-hour ceiling
api.idToken.setCustomClaim('session_expiry', Math.floor(Date.now() / 1000) + 7200);
};- Okta or OIDC enterprise connection with
id_token_session_expiry_supported: true, or - A Post-Login Action that sets
session_expiryas a Unix timestamp in seconds
Once the ceiling is reached (with a 30-second clock-skew tolerance), the SDK tears down the local session. No network call is made past the ceiling.
// Before ceiling is reached
const user = await auth0.getUser(); // returns the user object
const token = await auth0.getTokenSilently(); // returns the access token
// After ceiling is reached
const user = await auth0.getUser(); // returns undefined
const token = await auth0.getTokenSilently(); // returns undefined — no network call made
const claims = await auth0.getIdTokenClaims(); // returns undefined
const isAuth = await auth0.isAuthenticated(); // returns falseThe ceiling is pinned to the value set at initial login. Silent token refreshes cannot extend it.
If your app assumes getUser() or getTokenSilently() always return a value for a logged-in user, add null checks to handle the breach gracefully:
const token = await auth0.getTokenSilently();
if (!token) {
await auth0.loginWithRedirect();
return;
}
fetch('/api/data', {
headers: { Authorization: `Bearer ${token}` }
});const user = await auth0.getUser();
if (!user) {
await auth0.loginWithRedirect();
return;
}
console.log(`Hello, ${user.name}`);