Examples using react-native-auth0

August 6, 2026 · View on GitHub

Authentication API

Unlike web authentication, we do not provide a hook for integrating with the Authentication API.

Instantiate the Auth0 class to get access to the methods that call Auth0's Authentication API endpoints:

import Auth0 from 'react-native-auth0';

const auth0 = new Auth0({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
});

Login with Password Realm Grant

auth0.auth
  .passwordRealm({
    username: 'info@auth0.com',
    password: 'password',
    realm: 'myconnection',
  })
  .then(console.log)
  .catch(console.error);

Get user information using user's access_token

auth0.auth
  .userInfo({ token: 'the user access_token' })
  .then(console.log)
  .catch(console.error);

This endpoint requires an access token that was granted the /userinfo audience. Check that the authentication request that returned the access token included an audience value of https://{YOUR_AUTH0_DOMAIN}.auth0.com/userinfo.

Parse user profile from an ID token locally

If you already have credentials (e.g. from webAuth.authorize() or credentialsManager.getCredentials()), you can extract the user profile from the ID token without a network request:

import Auth0, { parseIdToken } from 'react-native-auth0';

const auth0 = new Auth0({ domain, clientId });
const credentials = await auth0.webAuth.authorize({
  scope: 'openid profile email',
});
const user = parseIdToken(credentials.idToken);
// user.sub, user.name, user.email, etc.

This is the same parsing that Auth0Provider performs internally. It's useful when you manage auth state yourself via the Auth0 class and want to avoid the network round-trip of auth.userInfo().

Getting new access token with refresh token

auth0.auth
  .refreshToken({ refreshToken: 'the user refresh_token' })
  .then(console.log)
  .catch(console.error);

Using custom scheme for web authentication redirection

Custom Schemes can be used for redirecting to the React Native application after web authentication:

authorize({}, { customScheme: 'YOUR_AUTH0_DOMAIN' })
  .then(console.log)
  .catch(console.error);

Login using MFA with One Time Password code

Deprecated — will be removed in v6. The MFA methods on the auth client (auth0.auth.loginWithOTP, auth0.auth.loginWithOOB, auth0.auth.loginWithRecoveryCode, auth0.auth.multifactorChallenge) are superseded by the mfa client, which also lets you list and enrol authenticators. See the mapping table in the Migration Guide.

This call requires the client to have the MFA Client Grant Type enabled. Check this article to learn how to enable it.

When you sign in to a multifactor authentication enabled connection using the passwordRealm method, you receive an error stating that MFA is required for that user along with an mfa_token value. Use this value to complete the MFA flow, passing the One Time Password from the enrolled MFA code generator app.

// Recommended: the `mfa` client
const credentials = await auth0.mfa.verify({
  mfaToken: error.json.mfa_token,
  otp: '{user entered OTP}',
});
// Deprecated: removed in v6
auth0.auth
  .loginWithOTP({
    mfaToken: error.json.mfa_token,
    otp: '{user entered OTP}',
  })
  .then(console.log)
  .catch(console.error);

Login with Passwordless

Passwordless is a two-step authentication flow that makes use of this type of connection. The Passwordless OTP grant is required to be enabled in your Auth0 application beforehand. Check our guide to learn how to enable it.

The send option controls how the user receives the challenge, and the two options complete differently. Make sure you use the one that matches how you started the flow:

  • send: 'code' — the user receives a 6-digit one-time code that you collect in your app and exchange for tokens with loginWithEmail / loginWithSMS. This is fully handled by the SDK and is the recommended option for React Native / Expo.
  • send: 'link' — the user receives a magic link (typically by email). Tapping it completes authentication server-side and redirects to your app's callback URL with the tokens already in the URL fragment (e.g. myapp://callback#access_token=...). There is no code to pass back to loginWithEmail in this case; instead your app must listen for the deep link and parse the tokens from the fragment yourself (see Handling the magic link callback below).

Note

This SDK accepts only 'code' or 'link' for send. The underlying native SDKs (Auth0.swift / Auth0.Android) additionally expose platform-specific link types such as link_ios / link_android, but those are not surfaced through react-native-auth0.

To start the flow, request a code to be sent to the user's email or phone number:

auth0.auth
  .passwordlessWithEmail({
    email: 'info@auth0.com',
    send: 'code',
  })
  .then(console.log)
  .catch(console.error);

or

auth0.auth
  .passwordlessWithSMS({
    phoneNumber: '+5491159991000',
  })
  .then(console.log)
  .catch(console.error);

Then, to complete the authentication, send back the received code along with the email or phone number used:

auth0.auth
  .loginWithEmail({
    email: 'info@auth0.com',
    code: '123456',
  })
  .then(console.log)
  .catch(console.error);

or

auth0.auth
  .loginWithSMS({
    phoneNumber: '+5491159991000',
    code: '123456',
  })
  .then(console.log)
  .catch(console.error);

To start the flow, request a link to be sent to the user's email:

auth0.auth
  .passwordlessWithEmail({
    email: 'info@auth0.com',
    send: 'link',
  })
  .then(console.log)
  .catch(console.error);

With send: 'link', tapping the link in the email completes authentication on the Auth0 server and redirects to your registered callback URL with the tokens in the URL fragment:

myapp://callback#access_token=...&scope=openid%20profile%20email%20offline_access&expires_in=7200&token_type=Bearer

The SDK does not intercept this redirect, so do not call loginWithEmail here — there is no code to send back. Instead, listen for the incoming deep link in your app using React Native's Linking API (or expo-linking on Expo) and parse the tokens from the URL fragment yourself:

import { Linking } from 'react-native';

function parseTokensFromUrl(url) {
  const fragment = url.split('#')[1] ?? '';
  const params = new URLSearchParams(fragment);
  return {
    idToken: params.get('id_token'),
    accessToken: params.get('access_token'),
    expiresIn: params.get('expires_in'),
    scope: params.get('scope'),
    tokenType: params.get('token_type'),
  };
}

// Handle the case where the app is opened from a cold start by the link
Linking.getInitialURL().then((url) => {
  if (url) {
    const tokens = parseTokensFromUrl(url);
    // store / use the tokens
  }
});

// Handle the case where the app is already running
const subscription = Linking.addEventListener('url', ({ url }) => {
  const tokens = parseTokensFromUrl(url);
  // store / use the tokens
});

Note

For native and Expo apps, the code flow is recommended because the SDK handles the full exchange for you. Use the magic link flow only if your use case specifically requires magic links, and be aware that you are responsible for handling the deep link and securely storing the returned tokens.

Login with Passwordless OTP (Database Connections)

Note

This feature is currently in Early Access. Reach out to Auth0 support to have it enabled for your tenant.

Native only (iOS, Android). Calling auth0.passwordless.* on the web platform rejects with an UnsupportedOperation error.

This flow lets a user authenticate with a one-time code sent to their email or phone against a standard database connection (auth0 strategy) that has email_otp or phone_otp enabled. It is distinct from Login with Passwordless, which targets dedicated email / sms connections.

It is a two-step, challenge-response flow: request a challenge (which delivers the OTP and returns an opaque auth_session), then exchange the challenge and the user-entered code for credentials.

Email challenge

// 1. Request a challenge — Auth0 emails the OTP and returns the challenge.
const challenge = await auth0.passwordless.challengeWithEmail({
  email: 'info@auth0.com',
  connection: 'Username-Password-Authentication', // required; must have email_otp enabled
  // allowSignup defaults to false
});

// 2. Exchange the challenge and the code the user received for credentials.
const credentials = await auth0.passwordless.loginWithOTP({
  challenge,
  otp: '123456',
});

Phone challenge

// 1. Request a challenge — delivered by SMS ('text') or voice call ('voice').
const challenge = await auth0.passwordless.challengeWithPhoneNumber({
  phoneNumber: '+15555550123',
  connection: 'Username-Password-Authentication', // required; must have phone_otp enabled
  deliveryMethod: 'text', // defaults to 'text'
});

// 2. Exchange the challenge and the code for credentials.
const credentials = await auth0.passwordless.loginWithOTP({
  challenge,
  otp: '123456',
});

Both challenge methods accept an optional allowSignup parameter (defaults to false). When set to true, a new user is created for the given email or phone number if one does not already exist on the connection; when false, the challenge is rejected for unknown identifiers.

The challenge object returned from a challenge call is opaque — pass it as-is to loginWithOTP. You can optionally provide audience and scope to loginWithOTP.

Create user in database connection

auth0.auth
  .createUser({
    email: 'info@auth0.com',
    username: 'username',
    password: 'password',
    connection: 'myconnection',
  })
  .then(console.log)
  .catch(console.error);

Using HTTPS callback URLs

HTTPS callback URLs provide enhanced security compared to custom URL schemes. They work with Android App Links and iOS Universal Links to prevent URL scheme hijacking:

auth0.webAuth
  .authorize({ scope: 'openid profile email' }, { customScheme: 'https' })
  .then((credentials) => console.log(credentials))
  .catch((error) => console.log(error));

Using Custom Headers

You can set custom headers to be included in all requests to the Auth0 API. This can be useful for implementing custom security requirements, logging, or tracking.

Set global headers during initialization

Global headers are included in all requests made by the SDK:

// Set global headers during Auth0 initialization
const auth0 = new Auth0({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
  headers: {
    'Accept-Language': 'fr-CA',
    'X-Tracking-Id': 'user-tracking-id-123',
  },
});

Using custom headers with Auth0Provider component

If you're using the hooks-based approach with Auth0Provider, you can provide headers during initialization:

import { Auth0Provider } from 'react-native-auth0';

// In your app component
<Auth0Provider
  domain={'YOUR_AUTH0_DOMAIN'}
  clientId={'YOUR_CLIENT_ID'}
  headers={{
    'Accept-Language': 'fr-CA',
    'X-App-Version': '1.2.3',
  }}
>
  <App />
</Auth0Provider>;

Set request-specific headers

You can also provide headers for specific API calls, which will override global headers with the same name:

// For specific authentication requests
auth0.auth
  .passwordRealm({
    username: 'info@auth0.com',
    password: 'password',
    realm: 'myconnection',
    headers: {
      'X-Custom-Header': 'request-specific-value',
      'X-Request-ID': 'unique-request-id-456',
    },
  })
  .then(console.log)
  .catch(console.error);

Credential Renewal Retry

Platform Support: iOS only.

Automatic retry mechanism for credential renewal to improve reliability in unstable network conditions, particularly important for mobile applications with refresh token rotation enabled.

Overview

When your application operates on unstable mobile networks, credential renewal requests may fail due to transient network issues. The maxRetries configuration option enables automatic retry with exponential backoff for the following error scenarios:

  • Network errors: Connection timeouts, DNS failures, unreachable hosts
  • Rate limiting: HTTP 429 (Too Many Requests)
  • Server errors: HTTP 5xx responses

Important: While the retry mechanism is particularly valuable for refresh token rotation (RRT) scenarios, it can be used to improve credential renewal reliability in any configuration, including non-RRT deployments. The retry logic helps handle transient network failures regardless of your token rotation strategy.

Example scenario with Refresh Token Rotation:

  1. Request A calls getCredentials() and starts a token refresh
  2. Request A successfully hits the server and gets new credentials
  3. Request A fails on the way back (network issue), never reaching the client
  4. The retry mechanism automatically retries the failed request using the same (old) refresh token
  5. The retry succeeds within the refresh token rotation overlap window

Critical for RRT: If you have refresh token rotation enabled, you must configure a token overlap period of at least 180 seconds (3 minutes) in your Auth0 tenant. This overlap window allows retries to succeed using the old refresh token before it expires, preventing users from being locked out due to network failures.

Prerequisites

To use the retry mechanism:

  1. SDK Version: Requires react-native-auth0 v5.4.0 or later
  2. Scope: Ensure your authentication requests include the offline_access scope to receive refresh tokens

Additional requirements for Refresh Token Rotation:

If you have refresh token rotation enabled in your Auth0 tenant:

  1. Token Overlap Period: Configure an overlap period of at least 180 seconds (3 minutes) in your Auth0 tenant settings. This is critical to ensure retries can succeed using the old refresh token before it expires.

Using Retry with Hooks

import React from 'react';
import { View, Button, Alert } from 'react-native';
import { Auth0Provider, useAuth0 } from 'react-native-auth0';

function App() {
  return (
    <Auth0Provider
      domain="YOUR_AUTH0_DOMAIN"
      clientId="YOUR_AUTH0_CLIENT_ID"
      maxRetries={2} // Configure retry mechanism at initialization (iOS only)
    >
      <MyComponent />
    </Auth0Provider>
  );
}

function MyComponent() {
  const { getCredentials } = useAuth0();

  const fetchCredentialsWithRetry = async () => {
    try {
      // The retry mechanism is automatically applied to all credential renewal attempts
      const credentials = await getCredentials();

      console.log('Authenticated successfully');
      // Use credentials for API calls...
    } catch (error) {
      console.error('Failed to get credentials after retries:', error);
      Alert.alert(
        'Error',
        'Unable to refresh credentials. Please log in again.'
      );
    }
  };

  return (
    <View>
      <Button title="Get Credentials" onPress={fetchCredentialsWithRetry} />
    </View>
  );
}

Using Retry with Auth0 Class

import Auth0 from 'react-native-auth0';

// Configure retry mechanism at initialization (iOS only)
const auth0 = new Auth0({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
  maxRetries: 2, // Recommended maximum of 2 retries
});

// Get credentials - retry mechanism is automatically applied
try {
  const credentials = await auth0.credentialsManager.getCredentials();

  console.log('Credentials retrieved successfully');
} catch (error) {
  console.error('Credential renewal failed after retries:', error);
}

Platform Support

PlatformSupportBehavior
iOS✅ Full SupportUses exponential backoff retry with Auth0.swift v2.14+
Android⚠️ Parameter IgnoredAuth0.Android SDK does not currently support retry configuration
Web⚠️ Parameter Ignored@auth0/auth0-spa-js SDK does not currently support retry configuration

Default Behavior:

  • maxRetries defaults to 0 (no retries) to maintain backward compatibility
  • Recommended maximum: 2 retries
  • Each retry uses exponential backoff to avoid overwhelming the server

Error Handling

The retry mechanism only retries on transient, recoverable errors. The following errors will not trigger a retry:

  • Invalid refresh token
  • Refresh token expired
  • Refresh token revoked
  • Client authentication failures
  • Authorization errors (insufficient permissions)

Example with comprehensive error handling:

import { useAuth0 } from 'react-native-auth0';

function MyComponent() {
  const { getCredentials, authorize } = useAuth0();

  const fetchCredentials = async () => {
    try {
      const credentials = await getCredentials(
        undefined,
        undefined,
        undefined,
        false,
        2
      );
      return credentials;
    } catch (error) {
      // Check if it's a non-retryable error that requires re-authentication
      if (
        error.code === 'NO_REFRESH_TOKEN' ||
        error.code === 'RENEW_FAILED' ||
        error.message?.includes('refresh token')
      ) {
        console.log('Refresh token invalid, re-authenticating...');
        // Trigger a new login flow
        await authorize({ scope: 'openid profile offline_access' });
      } else {
        console.error('Transient error after retries:', error);
        throw error;
      }
    }
  };

  // ...
}

Best Practices:

  1. Use moderate retry counts: Recommended maximum of 2 retries to balance reliability with performance
  2. Configure adequate overlap period: Ensure your Auth0 tenant has at least 180 seconds token overlap configured
  3. Test on real devices: Simulate network instability during testing to validate retry behavior

IPSIE Session Expiry

Platform Support: iOS, Android, and Web.

Auth0 supports the IPSIE SL1 session_expiry claim, which lets an upstream identity provider (e.g. Okta) set a hard ceiling on how long an Auth0-issued session may live. When an okta or oidc enterprise connection has the "Use ID Token for Session Expiry" toggle enabled (in the Dashboard, or id_token_session_expiry_supported: true via the Management API), and the app uses the Authorization Code flow, Auth0 includes a session_expiry Unix timestamp in the ID token returned to your app after login.

This ceiling is layered on top of your tenant's existing idle and absolute session timeouts — it does not replace them. The session ends at whichever limit is reached first.

Warning

session_expiry is interpreted as seconds since the Unix epoch (per RFC 7519 NumericDate). If the Post-Login Action that sets it emits milliseconds (e.g. Date.now() without / 1000), the value reads as tens of thousands of years out; the platform SDKs reject implausibly large values (≥ 10_000_000_000) as malformed and treat them as no ceiling, silently disabling enforcement. Always emit seconds.

The underlying platform SDKs enforce this ceiling on every credential retrieval. Once the ceiling has passed, getCredentials() clears the stored credentials and rejects instead of attempting a token renewal — the user must re-authenticate. No opt-in code is required; enforcement is transparent once the connection option is active on your tenant.

react-native-auth0 surfaces this as a single, cross-platform error type: CredentialsManagerError with type === 'SESSION_EXPIRED'. Your existing "no credentials" re-login path already handles it, or you can match it explicitly:

import { useAuth0, CredentialsManagerError } from 'react-native-auth0';

function MyComponent() {
  const { getCredentials, authorize } = useAuth0();

  const fetchCredentials = async () => {
    try {
      const credentials = await getCredentials();
      return credentials;
    } catch (error) {
      if (
        error instanceof CredentialsManagerError &&
        error.type === 'SESSION_EXPIRED'
      ) {
        // Upstream IdP session has ended — send the user back to login.
        await authorize({ scope: 'openid profile offline_access' });
      } else {
        throw error;
      }
    }
  };

  // ...
}

If you need to read the ceiling directly — for example to warn the user before their session ends — it is exposed as sessionExpiresAt (an absolute Unix timestamp, in seconds) on the returned Credentials. It is undefined when the connection does not emit the claim:

const credentials = await getCredentials();
if (credentials.sessionExpiresAt) {
  const endsAt = new Date(credentials.sessionExpiresAt * 1000);
  console.log(`Upstream IdP session ends at: ${endsAt.toISOString()}`);
}

Note

Enforcement applies a small negative leeway (about 30 seconds) to account for clock skew, so the session is treated as expired slightly before this exact timestamp. Build any countdown UI with that margin in mind.

This value is decoded from the current ID token's session_expiry claim, except on Android where the credentials manager reports the ceiling pinned at the initial login (the value it actually enforces) when one is stored. It is also readable directly from the raw session_expiry claim on the decoded ID token — see Parse user profile from an ID token locally.

Note

On Android, the session_expiry ceiling is pinned at the initial login and is not raised by subsequent refresh-token grants. On iOS and Web, sessionExpiresAt is derived from the current ID token. Sessions from connections without the claim behave exactly as before.

Biometric Authentication

Platform Support: Native only (iOS/Android)

Configure biometric authentication to protect credential access. The SDK supports four biometric policies that control when biometric prompts are shown.

Biometric Policy Types

  • BiometricPolicy.default: System-managed behavior. Reuses the same LAContext on iOS, allowing the system to optimize prompt frequency. May skip the biometric prompt if authentication was recently successful.

  • BiometricPolicy.always: Always requires biometric authentication on every credential access. Creates a fresh LAContext on iOS and uses the "Always" policy on Android to ensure a new prompt is shown.

  • BiometricPolicy.session: Requires biometric authentication only once per session. After successful authentication, credentials can be accessed without prompting for the specified timeout duration.

  • BiometricPolicy.appLifecycle: Similar to session policy, but persists for the app's lifecycle. Session remains valid until the app restarts or clearCredentials() is called. Default timeout is 1 hour (3600 seconds).

Using with Auth0Provider (Hooks)

import {
  Auth0Provider,
  BiometricPolicy,
  LocalAuthenticationStrategy,
  LocalAuthenticationLevel,
} from 'react-native-auth0';

function App() {
  return (
    <Auth0Provider
      domain="YOUR_AUTH0_DOMAIN"
      clientId="YOUR_CLIENT_ID"
      localAuthenticationOptions={{
        title: 'Authenticate to access credentials',
        subtitle: 'Please authenticate to continue',
        description: 'We need to authenticate you to retrieve your credentials',
        cancelTitle: 'Cancel',
        evaluationPolicy: LocalAuthenticationStrategy.deviceOwnerWithBiometrics,
        fallbackTitle: 'Use Passcode',
        authenticationLevel: LocalAuthenticationLevel.strong,
        deviceCredentialFallback: true,
        // Option 1: Default policy (system-managed, backward compatible)
        biometricPolicy: BiometricPolicy.default,

        // Option 2: Always require biometric authentication
        // biometricPolicy: BiometricPolicy.always,

        // Option 3: Session-based (5 minutes)
        // biometricPolicy: BiometricPolicy.session,
        // biometricTimeout: 300,

        // Option 4: App lifecycle (1 hour)
        // biometricPolicy: BiometricPolicy.appLifecycle,
        // biometricTimeout: 3600,
      }}
    >
      <YourApp />
    </Auth0Provider>
  );
}

Using with Auth0 Class

import Auth0, {
  BiometricPolicy,
  LocalAuthenticationStrategy,
  LocalAuthenticationLevel,
} from 'react-native-auth0';

const auth0 = new Auth0({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
  localAuthenticationOptions: {
    title: 'Authenticate to access credentials',
    subtitle: 'Please authenticate to continue',
    description: 'We need to authenticate you to retrieve your credentials',
    cancelTitle: 'Cancel',
    evaluationPolicy: LocalAuthenticationStrategy.deviceOwnerWithBiometrics,
    fallbackTitle: 'Use Passcode',
    authenticationLevel: LocalAuthenticationLevel.strong,
    deviceCredentialFallback: true,
    biometricPolicy: BiometricPolicy.session,
    biometricTimeout: 300, // 5 minutes
  },
});

// Get credentials - will prompt for biometric authentication based on policy
const credentials = await auth0.credentialsManager.getCredentials();

Platform-Specific Behavior

Android

  • BiometricPolicy.default and BiometricPolicy.always both map to the Android SDK's "Always" policy
  • Uses BiometricPrompt for authentication
  • Session state is stored in memory and cleared on app restart

iOS

  • BiometricPolicy.default reuses the same LAContext, allowing the system to manage prompt frequency
  • BiometricPolicy.always, session, and appLifecycle create a fresh LAContext to ensure reliable prompts
  • Uses Face ID or Touch ID based on device capabilities
  • Session state is thread-safe and managed in memory

Migration from Previous Behavior

If you were not explicitly configuring biometric authentication before, the new BiometricPolicy.default maintains backward-compatible behavior. To enforce stricter biometric requirements, switch to BiometricPolicy.always.

Management API (Users)

Deprecated — will be removed in v6. Calling the Management API from a client requires an access token with over-privileged scopes (read:current_user, update:current_user_metadata) that cannot be kept secret in a mobile app or a browser. Move these operations to a backend you control (a BFF): your app sends its own access token, the backend validates it and calls the Management API with its own credentials. Both native SDKs have already dropped their Management clients.

Reading the current user's profile does not need the Management API — use auth0.auth.userInfo({ token }), or the user object from useAuth0(), which is decoded from the ID token.

Patch user with user_metadata

Recommended — update metadata through your own backend, which holds the Management API credentials:

const credentials = await auth0.credentialsManager.getCredentials();

await fetch('https://your-api.example.com/me/metadata', {
  method: 'PATCH',
  headers: {
    'Authorization': `Bearer ${credentials.accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ first_name: 'John', last_name: 'Doe' }),
});

Legacy (v5 only) — deprecated, and removed in v6:

auth0
  .users('the user access_token')
  .patchUser({
    id: 'user_id',
    metadata: { first_name: 'John', last_name: 'Doe' },
  })
  .then(console.log)
  .catch(console.error);

Get full user profile

Recommended — reading the signed-in user's profile needs no Management API and no backend:

const credentials = await auth0.credentialsManager.getCredentials();
const profile = await auth0.auth.userInfo({ token: credentials.accessToken });

// Or, inside a component, the `user` object decoded from the ID token:
const { user } = useAuth0();

Legacy (v5 only) — deprecated, and removed in v6:

auth0
  .users('{ACCESS_TOKEN}')
  .getUser({ id: 'user_id' })
  .then(console.log)
  .catch(console.error);

For more info please check our generated documentation

Organizations

Organizations is a set of features that provide better support for developers who build and maintain SaaS and Business-to-Business (B2B) applications.

Using Organizations, you can: Note that Organizations is currently only available to customers on our Enterprise and Startup subscription plans.

Log in to an organization

auth0.webAuth
  .authorize({ organization: 'organization-id' })
  .then((credentials) => console.log(credentials))
  .catch((error) => console.log(error));

Accept user invitations

Users can be invited to your organization via a link. Tapping on the invitation link should open your app. Since invitations links are https only, is recommended that your Android app supports Android App Links. In the case of iOS, your app must support Universal Links.

In Enable Android App Links Support and Enable Universal Links Support, you will find how to make the Auth0 server publish the Digital Asset Links file required by your applications.

When your app gets opened by an invitation link, grab the invitation URL and pass it as a parameter to the webauth call. Use the Linking Module method called getInitialUrl() to obtain the URL that launched your application.

auth0.webAuth
  .authorize({
    invitationUrl:
      'https://myapp.com/login?invitation=inv123&organization=org123',
  })
  .then((credentials) => console.log(credentials))
  .catch((error) => console.log(error));

If the URL doesn't contain the expected values, an error will be raised through the provided callback.

Multi-Resource Refresh Tokens (MRRT)

MRRT Overview

Multi-Resource Refresh Tokens (MRRT) allow your application to obtain access tokens for multiple APIs using a single refresh token. This is useful when your application needs to access multiple backend services, each identified by a different audience.

MRRT Prerequisites

Before using MRRT, ensure:

  1. MRRT is enabled on your Auth0 tenant - Contact Auth0 support or enable it through the Auth0 Dashboard
  2. Request offline_access scope during login - This ensures a refresh token is issued
  3. Configure your APIs in Auth0 Dashboard - Each API you want to access should be registered with its own audience identifier

Using MRRT with Hooks

import { useAuth0 } from 'react-native-auth0';

function MyComponent() {
  const { authorize, getApiCredentials, clearApiCredentials } = useAuth0();

  const login = async () => {
    // Login with offline_access to get a refresh token
    await authorize({
      scope: 'openid profile email offline_access',
      audience: 'https://primary-api.example.com',
    });
  };

  const getFirstApiToken = async () => {
    try {
      // Get credentials for the first API
      const credentials = await getApiCredentials(
        'https://first-api.example.com',
        'read:data write:data'
      );
      console.log('First API authenticated successfully');
      console.log('Expires At:', new Date(credentials.expiresAt * 1000));
    } catch (error) {
      console.error('Error:', error);
    }
  };

  const getSecondApiToken = async () => {
    try {
      // Get credentials for a different API using the same refresh token
      const credentials = await getApiCredentials(
        'https://second-api.example.com',
        'read:reports'
      );
      console.log('Second API authenticated successfully');
    } catch (error) {
      console.error('Error:', error);
    }
  };

  const clearFirstApiCache = async () => {
    // Clear cached credentials for a specific API
    await clearApiCredentials('https://first-api.example.com');

    // Or clear with specific scope
    await clearApiCredentials('https://first-api.example.com', 'read:data');
  };

  return (
    // Your UI components
  );
}

Using MRRT with Auth0 Class

import Auth0 from 'react-native-auth0';

const auth0 = new Auth0({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
});

// Login with offline_access scope
await auth0.webAuth.authorize({
  scope: 'openid profile email offline_access',
  audience: 'https://primary-api.example.com',
});

// Get credentials for a specific API
const apiCredentials = await auth0.credentialsManager.getApiCredentials(
  'https://first-api.example.com',
  'read:data write:data'
);

console.log('Access Token:', apiCredentials.accessToken);
console.log('Token Type:', apiCredentials.tokenType);
console.log('Expires At:', apiCredentials.expiresAt);
console.log('Scope:', apiCredentials.scope);

// Clear cached credentials for a specific API
await auth0.credentialsManager.clearApiCredentials(
  'https://first-api.example.com'
);

// Clear with specific scope
await auth0.credentialsManager.clearApiCredentials(
  'https://first-api.example.com',
  'read:data write:data'
);

Web Platform Configuration

On the web platform, you must explicitly enable MRRT support in the Auth0Provider:

import { Auth0Provider } from 'react-native-auth0';

function App() {
  return (
    <Auth0Provider
      domain="your-domain.auth0.com"
      clientId="your-client-id"
      useMrrt={true}
      cacheLocation="localstorage"
    >
      <YourApp />
    </Auth0Provider>
  );
}

Custom Token Exchange (RFC 8693)

Custom Token Exchange allows you to exchange external identity provider tokens for Auth0 tokens using the RFC 8693 OAuth 2.0 Token Exchange specification. This enables scenarios where users authenticate with an external system and that token needs to be exchanged for Auth0 tokens.

⚠️ Important: The external token must be validated in Auth0 Actions using cryptographic verification. See the Auth0 Custom Token Exchange documentation for setup instructions.

Using Custom Token Exchange with Hooks

import React from 'react';
import { Button, Alert } from 'react-native';
import {
  useAuth0,
  AuthenticationException,
  AuthenticationErrorCodes,
} from 'react-native-auth0';

function TokenExchangeScreen() {
  const { customTokenExchange, user, error } = useAuth0();

  const handleExchange = async () => {
    try {
      // Exchange an external token for Auth0 tokens
      const credentials = await customTokenExchange({
        subjectToken: 'token-from-external-provider',
        subjectTokenType: 'urn:acme:legacy-system-token',
        scope: 'openid profile email',
        audience: 'https://api.example.com',
      });

      Alert.alert('Success', `Logged in as ${user?.name}`);
    } catch (e) {
      if (e instanceof AuthenticationException) {
        switch (e.type) {
          case AuthenticationErrorCodes.INVALID_SUBJECT_TOKEN:
            Alert.alert('Error', 'The external token is invalid or expired');
            break;
          case AuthenticationErrorCodes.UNSUPPORTED_TOKEN_TYPE:
            Alert.alert('Error', 'The token type is not supported');
            break;
          case AuthenticationErrorCodes.TOKEN_EXCHANGE_NOT_CONFIGURED:
            Alert.alert(
              'Error',
              'Custom Token Exchange is not configured for this tenant'
            );
            break;
          case AuthenticationErrorCodes.TOKEN_VALIDATION_FAILED:
            Alert.alert('Error', 'Token validation failed in Auth0 Action');
            break;
          case AuthenticationErrorCodes.NETWORK_ERROR:
            Alert.alert('Error', 'Network error. Please check your connection.');
            break;
          default:
            Alert.alert('Error', e.message);
        }
      } else {
        console.error('Token exchange failed:', e);
      }
    }
  };

  return <Button onPress={handleExchange} title="Exchange Token" />;
}

Using Custom Token Exchange with Auth0 Class

import Auth0, {
  AuthenticationException,
  AuthenticationErrorCodes,
} from 'react-native-auth0';

const auth0 = new Auth0({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_CLIENT_ID',
});

async function exchangeExternalToken(externalToken: string) {
  try {
    const credentials = await auth0.customTokenExchange({
      subjectToken: externalToken,
      subjectTokenType: 'urn:acme:legacy-system-token',
      audience: 'https://api.example.com',
      scope: 'openid profile email',
    });

    console.log('Exchange successful:', credentials);
    return credentials;
  } catch (error) {
    if (error instanceof AuthenticationException) {
      // Access the underlying error details
      console.error('Error type:', error.type);
      console.error('Error message:', error.message);
      console.error('Underlying error code:', error.underlyingError.code);

      // Handle specific error types
      if (error.type === AuthenticationErrorCodes.INVALID_SUBJECT_TOKEN) {
        // Token is invalid or expired - prompt user to re-authenticate
        throw new Error('Please authenticate again with the external provider');
      }
    }
    throw error;
  }
}

With Organization Context

Exchange tokens within a specific organization context:

const credentials = await customTokenExchange({
  subjectToken: 'external-provider-token',
  subjectTokenType: 'urn:acme:legacy-system-token',
  organization: 'org_123', // or organization name
  scope: 'openid profile email',
});

Delegation & Impersonation (Actor Token)

For delegation and impersonation scenarios — where an actor (such as an AI agent, support representative, or service) acts on behalf of the subject — pass an actor token alongside the subject token. This follows RFC 8693 Section 2.1.

const credentials = await customTokenExchange({
  subjectToken: 'subject-provider-token',
  subjectTokenType: 'urn:acme:legacy-system-token',
  actorToken: 'actor-id-token',
  actorTokenType: 'http://corporate-idp/id-token',
});

actorTokenType follows the same URI validation rules as subjectTokenType and accepts any developer-defined URI.

ℹ️ Prerequisites: This flow requires actor token support enabled on your tenant (contact Auth0 support) and an Auth0 Action that calls api.authentication.setActor() to set the act claim on the issued tokens. The actorToken itself must be a valid Auth0 ID token (JWT) — Auth0 validates its format, signature, expiry, and client_id, and rejects the exchange otherwise.

Accessing the act claim

When an Action sets the actor, the issued ID token carries an act claim describing the acting party (which may be nested to represent a delegation chain). Auth0 writes the same act claim onto the access token too, but the access token can be opaque, so the ID token is the reliable place to read it. It is available on the parsed user profile:

import { parseIdToken } from 'react-native-auth0';

const credentials = await customTokenExchange({
  subjectToken: 'subject-provider-token',
  subjectTokenType: 'urn:acme:legacy-system-token',
  actorToken: 'actor-id-token',
  actorTokenType: 'http://corporate-idp/id-token',
});

const user = parseIdToken(credentials.idToken);
console.log(user.act); // The acting party claim

⚠️ Refresh token suppression: When an actor token is present, Auth0 will not issue a refresh token, regardless of whether offline_access is in the requested scope. credentials.refreshToken will be undefined in this case. Because there is no refresh token, the act claim cannot be re-emitted via a later refresh-token grant — the acting party is fixed at exchange time. To act again, perform a new token exchange.

⚠️ Paired parameters: actorToken and actorTokenType must be provided together. Supplying only one throws an AuthError with code invalid_actor_token_parameters before any network request is made.

Subject Token Type Requirements

The subjectTokenType parameter must be a unique profile token type URI starting with https:// or urn:.

Valid Token Type Patterns

You control the token type namespace. Use one of these patterns:

URN Format (Recommended):

  • urn:yourcompany:token-type - Company-specific token type
  • urn:acme:legacy-system-token - Legacy system tokens
  • urn:example:external-idp - External IdP tokens

HTTPS URL Format:

  • https://yourcompany.com/tokens/legacy - Using your organization's domain
  • https://example.com/custom-token - Custom token identifier

Reserved Namespaces (Forbidden)

The following namespaces are reserved and you CANNOT use them:

  • ❌ http://auth0.com/*
  • ❌ https://auth0.com/*
  • ❌ http://okta.com/*
  • ❌ https://okta.com/*
  • ❌ urn:ietf:*
  • ❌ urn:auth0:*
  • ❌ urn:okta:*

Common Use Cases

  1. Seamless Migration from Legacy IdP: Exchange legacy refresh tokens

    await customTokenExchange({
      subjectToken: legacyRefreshToken,
      subjectTokenType: 'urn:acme:legacy-system-token',
      scope: 'openid profile email offline_access',
    });
    
  2. External Authentication Provider: Exchange tokens from partner IdP

    await customTokenExchange({
      subjectToken: externalProviderToken,
      subjectTokenType: 'urn:partner:auth-token',
      scope: 'openid profile email',
    });
    
  3. Custom JWT Tokens: Exchange JWTs from your own system

    await customTokenExchange({
      subjectToken: customJwt,
      subjectTokenType: 'urn:yourcompany:jwt-token',
      audience: 'https://api.example.com',
    });
    

Error Codes Reference

Custom Token Exchange throws AuthError with specific error codes for different failure scenarios. Use the code property for programmatic error handling:

try {
  await auth0.customTokenExchange({...});
} catch (error) {
  console.error('Error code:', error.code);
  console.error('Error message:', error.message);

  // Handle specific errors
  if (error.code === 'invalid_grant') {
    // Handle invalid token
  }
}
Error CodeDescription
custom_token_exchange_failedGeneral token exchange failure
invalid_grantThe external token is invalid, malformed, or expired
invalid_requestThe request is missing required parameters or is malformed
unsupported_token_typeThe token type is not supported or recognized
unauthorized_clientCustom Token Exchange is not enabled for this client
invalid_targetThe requested audience is invalid or not allowed
invalid_scopeThe requested scope is invalid or not allowed
access_deniedToken exchange was denied by the authorization server
server_errorThe authorization server encountered an internal error
temporarily_unavailableThe server is temporarily unable to handle the request
network_errorNetwork connectivity issue occurred
a0.token_exchange_failedAuth0-specific token exchange failure
a0.action_failedThe token validation in Auth0 Action failed
a0.invalid_subject_tokenSubject token validation failed
a0.unsupported_subject_token_typeSubject token type is not supported

These error codes follow:

  • RFC 8693 standard: invalid_grant, invalid_request, unsupported_token_type, access_denied, etc.
  • Auth0-specific codes: a0.token_exchange_failed, a0.action_failed, etc.

Auth0 Actions Validation

Custom Token Exchange requires validation of the subject token in Auth0 Actions. The Action must:

  1. Validate the subject token cryptographically (verify signature, expiration, issuer, etc.)
  2. Apply authorization policy to determine if the exchange is allowed
  3. Set the user using one of the api.authentication.setUser*() methods

For detailed examples of validating different token types in Actions, see:

Security Best Practices:

  • Use asymmetric algorithms (RS256, ES256) whenever possible
  • Store secrets in Actions Secrets, never hardcode them
  • Cache JWKS keys using api.cache.set() to improve performance
  • Validate token expiration, issuer, and audience claims
  • Implement rate limiting for failed validations using api.access.rejectInvalidSubjectToken()

Passkeys

Overview

Passkeys provide a passwordless authentication experience using platform biometrics (Face ID, Touch ID, fingerprint) backed by public-key cryptography. The same three functions — passkeySignupChallenge, passkeyLoginChallenge, and getTokenByPasskey — are used on native and web; no web-specific methods were added. Only the WebAuthn ceremony step differs: on native you call your own native module or a library like react-native-passkey; on web you call the browser's built-in navigator.credentials.create()/.get() API directly, and pass the resulting PublicKeyCredential straight to getTokenByPasskey — the SDK does not perform this step for you on either platform.

The passkey flow has three steps:

  1. Challenge — Request a WebAuthn challenge from Auth0 (passkeySignupChallenge or passkeyLoginChallenge)
  2. WebAuthn Ceremony — Create or assert the passkey yourself, using whatever mechanism your platform provides. On native, use your own native module or a library (e.g. react-native-passkey, which may in turn use Android's CredentialManager API or iOS's ASAuthorizationController); on web, call navigator.credentials.create()/.get() directly.
  3. Exchange — Send the credential response back to Auth0 to get tokens (getTokenByPasskey)

Platform Support: iOS 16.6+, Android, and Web (modern browsers with WebAuthn support).

Prerequisites

Before using passkeys:

  1. Enable the Passkey Grant Type for your Auth0 application in the Auth0 Dashboard
  2. Configure a custom domain on your Auth0 tenant (required for passkeys)
  3. iOS: Requires iOS 16.6 or later. Add an Associated Domain with the webcredentials service pointing to your Auth0 custom domain
  4. Android: Requires Android API 28+. Configure your app's Digital Asset Links for the Auth0 custom domain
  5. Web: Requires a browser with WebAuthn support (all modern browsers). Passkeys must be triggered from a user gesture (e.g. a button click) due to browser security restrictions.

Important: passkeySignupChallenge is for creating new user accounts with a passkey. It will fail if the email already exists in the database connection. Use passkeyLoginChallenge for existing users who have already registered a passkey.

Signup with Passkey

The signup flow requests a registration challenge from Auth0, then you use the platform credential manager (via a native module or library like react-native-passkey) to create a new passkey, and finally exchange the credential for Auth0 tokens.

import { useAuth0, PasskeyError } from 'react-native-auth0';

function PasskeySignupScreen() {
  const { passkeySignupChallenge, getTokenByPasskey } = useAuth0();

  const handleSignup = async () => {
    try {
      // Step 1: Get the signup challenge from Auth0
      const challenge = await passkeySignupChallenge({
        email: 'user@example.com',
        name: 'John Doe',
        realm: 'Username-Password-Authentication',
      });

      // Step 2: Use the platform credential manager to create a passkey
      // challenge.authParamsPublicKey contains the WebAuthn PublicKeyCredentialCreationOptions
      // Use your preferred library (e.g., react-native-passkey) or native module
      const credentialJson = await yourCredentialManagerCreate(
        challenge.authParamsPublicKey
      );

      // Step 3: Exchange the credential response for Auth0 tokens
      const credentials = await getTokenByPasskey({
        authSession: challenge.authSession,
        authResponse: credentialJson,
        realm: 'Username-Password-Authentication',
        audience: 'https://api.example.com',
        scope: 'openid profile email offline_access',
      });

      console.log('Signed up with passkey');
    } catch (error) {
      if (error instanceof PasskeyError) {
        console.error('Passkey signup failed:', error.type, error.message);
      }
    }
  };

  return <Button title="Sign Up with Passkey" onPress={handleSignup} />;
}

Signin with Passkey

The login flow requests an assertion challenge from Auth0, then you use the platform credential manager to assert an existing passkey, and finally exchange the credential for Auth0 tokens.

import { useAuth0, PasskeyError } from 'react-native-auth0';

function PasskeySigninScreen() {
  const { passkeyLoginChallenge, getTokenByPasskey } = useAuth0();

  const handleSignin = async () => {
    try {
      // Step 1: Get the login challenge from Auth0
      const challenge = await passkeyLoginChallenge({
        realm: 'Username-Password-Authentication',
      });

      // Step 2: Use the platform credential manager to assert an existing passkey
      // challenge.authParamsPublicKey contains the WebAuthn PublicKeyCredentialRequestOptions
      // Use your preferred library (e.g., react-native-passkey) or native module
      const credentialJson = await yourCredentialManagerGet(
        challenge.authParamsPublicKey
      );

      // Step 3: Exchange the credential response for Auth0 tokens
      const credentials = await getTokenByPasskey({
        authSession: challenge.authSession,
        authResponse: credentialJson,
        realm: 'Username-Password-Authentication',
        audience: 'https://api.example.com',
        scope: 'openid profile email offline_access',
      });

      console.log('Signed in with passkey');
    } catch (error) {
      if (error instanceof PasskeyError) {
        console.error('Passkey signin failed:', error.type, error.message);
      }
    }
  };

  return <Button title="Sign In with Passkey" onPress={handleSignin} />;
}

Signup with Passkey (Web)

Web uses the exact same passkeySignupChallenge / passkeyLoginChallenge / getTokenByPasskey functions as native — no web-specific methods were added. Only step 2 (the WebAuthn ceremony) differs: the app calls the browser's built-in WebAuthn API — navigator.credentials.create() for signup and navigator.credentials.get() for login — instead of a native module or third-party library. Unlike native, authResponse on web accepts the raw PublicKeyCredential object returned directly by navigator.credentials — no manual serialization needed. (See Auth Response Format for the native, JSON-string form.)

import { useAuth0, PasskeyError } from 'react-native-auth0';

function PasskeySignupScreenWeb() {
  const { passkeySignupChallenge, getTokenByPasskey } = useAuth0();

  // Must be called from a user gesture (e.g. an onClick handler).
  const handleSignup = async () => {
    try {
      const challenge = await passkeySignupChallenge({
        email: 'user@example.com',
        name: 'John Doe',
        realm: 'Username-Password-Authentication',
      });

      // navigator.credentials isn't wrapped by the SDK — normalize a
      // cancelled/failed WebAuthn ceremony (e.g. the user dismissed the
      // prompt) into a PasskeyError so it's handled the same way as any
      // other passkey error below.
      let credential: PublicKeyCredential;
      try {
        credential = (await navigator.credentials.create({
          publicKey:
            challenge.authParamsPublicKey as PublicKeyCredentialCreationOptions,
        })) as PublicKeyCredential;
      } catch (e) {
        throw new PasskeyError(e as Error);
      }

      const credentials = await getTokenByPasskey({
        authSession: challenge.authSession,
        authResponse: credential,
        realm: 'Username-Password-Authentication',
      });

      console.log('Signed up with passkey');
    } catch (error) {
      if (error instanceof PasskeyError) {
        console.error('Passkey signup failed:', error.type, error.message);
      }
    }
  };

  return <button onClick={handleSignup}>Sign Up with Passkey</button>;
}

Signin with Passkey (Web)

Same idea for login: passkeyLoginChallenge and getTokenByPasskey are unchanged from native — only the credential-manager step (navigator.credentials.get() instead of a native module) is web-specific.

import { useAuth0, PasskeyError } from 'react-native-auth0';

function PasskeySigninScreenWeb() {
  const { passkeyLoginChallenge, getTokenByPasskey } = useAuth0();

  // Must be called from a user gesture (e.g. an onClick handler).
  const handleSignin = async () => {
    try {
      const challenge = await passkeyLoginChallenge({
        realm: 'Username-Password-Authentication',
      });

      let credential: PublicKeyCredential;
      try {
        credential = (await navigator.credentials.get({
          publicKey:
            challenge.authParamsPublicKey as PublicKeyCredentialRequestOptions,
        })) as PublicKeyCredential;
      } catch (e) {
        throw new PasskeyError(e as Error);
      }

      const credentials = await getTokenByPasskey({
        authSession: challenge.authSession,
        authResponse: credential,
        realm: 'Username-Password-Authentication',
      });

      console.log('Signed in with passkey');
    } catch (error) {
      if (error instanceof PasskeyError) {
        console.error('Passkey signin failed:', error.type, error.message);
      }
    }
  };

  return <button onClick={handleSignin}>Sign In with Passkey</button>;
}

Auth Response Format

On iOS and Android, the authResponse parameter passed to getTokenByPasskey must be a JSON string representing the PublicKeyCredential response from the platform credential manager. On web, pass the raw PublicKeyCredential object returned by navigator.credentials.create()/.get() directly — the SDK serializes it internally.

For registration (signup):

{
  "id": "<base64url-encoded credential ID>",
  "rawId": "<base64url-encoded credential ID>",
  "type": "public-key",
  "response": {
    "clientDataJSON": "<base64url-encoded>",
    "attestationObject": "<base64url-encoded>"
  },
  "authenticatorAttachment": "platform"
}

For assertion (login):

{
  "id": "<base64url-encoded credential ID>",
  "rawId": "<base64url-encoded credential ID>",
  "type": "public-key",
  "response": {
    "clientDataJSON": "<base64url-encoded>",
    "authenticatorData": "<base64url-encoded>",
    "signature": "<base64url-encoded>",
    "userHandle": "<base64url-encoded>"
  },
  "authenticatorAttachment": "platform"
}

Using Passkeys with Auth0 Class

import Auth0, { PasskeyError } from 'react-native-auth0';

const auth0 = new Auth0({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
});

// Signup flow
const signupChallenge = await auth0.passkeySignupChallenge({
  email: 'user@example.com',
  name: 'John Doe',
  realm: 'Username-Password-Authentication',
});

// Use your credential manager library to create the passkey
// signupChallenge.authParamsPublicKey has the WebAuthn creation options
const registrationJson = await yourCredentialManagerCreate(
  signupChallenge.authParamsPublicKey
);

const signupCredentials = await auth0.getTokenByPasskey({
  authSession: signupChallenge.authSession,
  authResponse: registrationJson,
  realm: 'Username-Password-Authentication',
});

// Login flow
const loginChallenge = await auth0.passkeyLoginChallenge({
  realm: 'Username-Password-Authentication',
});

// Use your credential manager library to assert the passkey
// loginChallenge.authParamsPublicKey has the WebAuthn request options
const assertionJson = await yourCredentialManagerGet(
  loginChallenge.authParamsPublicKey
);

const loginCredentials = await auth0.getTokenByPasskey({
  authSession: loginChallenge.authSession,
  authResponse: assertionJson,
  realm: 'Username-Password-Authentication',
});

Signup Challenge Parameters

The passkeySignupChallenge method accepts the following parameters to create a user profile along with the passkey. At least one of email, phoneNumber, or username is required — which of these your database connection actually accepts depends on its configuration (e.g. Flexible Identifiers). The SDK does not validate this client-side; an unsupported or missing identifier is rejected by the Auth0 API and surfaces as a PASSKEY_CHALLENGE_FAILED error.

ParameterTypeDescription
emailstring?User's email address
phoneNumberstring?User's phone number
usernamestring?Username
namestring?Full name
givenNamestring?First/given name
familyNamestring?Last/family name
nicknamestring?Nickname
picturestring?Profile picture URL
userMetadataRecord<string, string>?Custom user metadata key-value pairs
realmstring?Database connection name
organizationstring?Auth0 organization ID

Error Handling

Passkey operations throw PasskeyError (extends AuthError) with a normalized type property. Use PasskeyErrorCodes for type-safe error handling:

Error CodeDescription
PASSKEY_CHALLENGE_FAILEDAuth0 challenge request failed
PASSKEY_EXCHANGE_FAILEDToken exchange with credential response failed
PASSKEY_NOT_AVAILABLEPasskeys not available on this device/OS version, or WebAuthn is not supported in this browser
PASSKEY_UNSUPPORTED_PLATFORMPasskeys not supported on this platform
PASSKEY_INVALID_PARAMETERNative only. authResponse passed to getTokenByPasskey was not a JSON string
PASSKEY_INVALID_CREDENTIALWeb only. The credential passed to getTokenByPasskey is neither a valid attestation (signup) nor assertion (login) response
PASSKEY_MFA_REQUIREDWeb only. MFA is required to complete the exchange — use error.getMfaRequiredPayload() to extract mfaToken and mfaRequirements, then continue with mfa.challenge()/mfa.verify()
PASSKEY_UNKNOWN_ERRORUnknown or uncategorized passkey error — check error.message for the underlying description
import { PasskeyError, PasskeyErrorCodes } from 'react-native-auth0';

try {
  const credentials = await auth0.getTokenByPasskey({
    authSession: challenge.authSession,
    authResponse: credential,
  });
} catch (error) {
  if (error instanceof PasskeyError) {
    console.log('Error type:', error.type); // e.g. "PASSKEY_CHALLENGE_FAILED"
    console.log('Error message:', error.message);
    console.log('Error code:', error.code); // Raw error code

    // Handle MFA required
    if (error.type === PasskeyErrorCodes.MFA_REQUIRED) {
      const mfaPayload = error.getMfaRequiredPayload();
      if (mfaPayload) {
        console.log('MFA token:', mfaPayload.mfaToken);
        console.log('Available factors:', mfaPayload.mfaRequirements);
        // Continue with mfa.challenge() / mfa.verify()
      }
    }
  }
}

Platform Support

PlatformSupportRequirements
iOS✅ SupportediOS 16.6+, Associated Domains with webcredentials
Android✅ SupportedAndroid API 28+, Digital Asset Links configured
Web✅ SupportedModern browser with WebAuthn support; call from a user gesture

Note: On native platforms, passkeys require a real device for the full flow — simulators/emulators may have limited support. On web, the credential-manager step (step 2) uses the browser's built-in navigator.credentials API instead of a native module or third-party library — see Signup with Passkey (Web) below. Because navigator.credentials.create()/.get() require a user gesture, call passkeySignupChallenge/passkeyLoginChallenge from within a click handler.

My Account API

Overview

The My Account API allows authenticated users to manage their own authentication methods (passkeys, phone, email, TOTP, push notifications, recovery codes). It provides endpoints for enrolling new factors, confirming enrollments with OTP, listing/updating/deleting authentication methods, and querying available factors.

Access the My Account client via the myAccount property from useAuth0() or the Auth0 class instance.

The My Account API is supported on Native (iOS/Android) and Web. The same myAccount API is used on all platforms; only the passkey credential ceremony differs (native passkey module vs. the browser's WebAuthn APIs). On Web, when DPoP is enabled (the default), the supplied access token must have been issued by the same client instance, since the DPoP proof is signed with that client's keypair; when the client is not configured with DPoP, plain bearer tokens are used and any valid access token works.

Prerequisites

  • A custom domain must be configured on your Auth0 tenant
  • iOS: Associated Domains entitlement must be configured with webcredentials:<your-custom-domain> for passkey support
  • Android: App Links must be set up with your custom domain via an assetlinks.json file for passkey support
  • Web: passkey enrollment uses the browser's WebAuthn APIs; no additional platform setup is required
  • The user must be authenticated
  • An access token with the appropriate My Account API scopes is required:
    • read:me:authentication_methods
    • create:me:authentication_methods
    • update:me:authentication_methods
    • delete:me:authentication_methods
    • read:me:factors

Use getApiCredentials with the https://<domain>/me/ audience to obtain a scoped token:

const credentials = await getApiCredentials(
  `https://${domain}/me/`,
  'read:me:authentication_methods create:me:authentication_methods delete:me:authentication_methods update:me:authentication_methods read:me:factors'
);
const accessToken = credentials.accessToken;

Passkey Enrollment

Passkey enrollment is a two-step process: request a challenge, then verify with the credential response.

import { useAuth0 } from 'react-native-auth0';
import { createPasskey } from './PasskeyModule'; // Your native passkey module

const { myAccount, getApiCredentials } = useAuth0();

// Step 1: Request the enrollment challenge
const accessToken = (await getApiCredentials(`https://${domain}/me/`, scopes))
  .accessToken;
const challenge = await myAccount.passkeyEnrollmentChallenge({ accessToken });

// Step 2: Create a passkey using the platform credential manager
const credentialJson = await createPasskey(challenge.authParamsPublicKey);

// Step 3: Verify the enrollment
const method = await myAccount.enrollPasskey({
  accessToken,
  authenticationMethodId: challenge.authenticationMethodId,
  authSession: challenge.authSession,
  authResponse: credentialJson,
  authParamsPublicKey: challenge.authParamsPublicKey,
});

console.log('Enrolled passkey:', method.id, method.keyId);

Phone Enrollment

import { PreferredAuthenticationMethods } from 'react-native-auth0';

const { myAccount } = useAuth0();

// Step 1: Enroll the phone number (sends OTP)
const challenge = await myAccount.enrollPhone({
  accessToken,
  phoneNumber: '+1234567890',
  preferredAuthenticationMethod: PreferredAuthenticationMethods.SMS, // or VOICE
});

// Step 2: Confirm with OTP
const method = await myAccount.confirmPhoneEnrollment({
  accessToken,
  id: challenge.id,
  authSession: challenge.authSession,
  otpCode: '123456',
});

Email Enrollment

// Step 1: Enroll the email (sends OTP)
const challenge = await myAccount.enrollEmail({
  accessToken,
  emailAddress: 'user@example.com',
});

// Step 2: Confirm with OTP
const method = await myAccount.confirmEmailEnrollment({
  accessToken,
  id: challenge.id,
  authSession: challenge.authSession,
  otpCode: '123456',
});

TOTP Enrollment

// Step 1: Enroll TOTP (returns QR code / manual code)
const challenge = await myAccount.enrollTOTP({ accessToken });
// Display challenge.barcodeUri as a QR code, or show challenge.manualInputCode

// Step 2: Confirm with OTP from authenticator app
const method = await myAccount.confirmTOTPEnrollment({
  accessToken,
  id: challenge.id,
  authSession: challenge.authSession,
  otpCode: '123456',
});

Recovery Code Enrollment

// Step 1: Enroll recovery code
const challenge = await myAccount.enrollRecoveryCode({ accessToken });
// Store challenge.recoveryCode securely

// Step 2: Confirm enrollment
const method = await myAccount.confirmRecoveryCodeEnrollment({
  accessToken,
  id: challenge.id,
  authSession: challenge.authSession,
});

Managing Authentication Methods

import { AuthenticationMethodTypes } from 'react-native-auth0';

// List all methods
const methods = await myAccount.getAuthenticationMethods({ accessToken });

// List only passkey methods
const passkeys = await myAccount.getAuthenticationMethods({
  accessToken,
  type: AuthenticationMethodTypes.PASSKEY,
});

// Get a specific method
const method = await myAccount.getAuthenticationMethodById({
  accessToken,
  id: 'authentication-method-id',
});

// Update a method name
const updated = await myAccount.updateAuthenticationMethodById({
  accessToken,
  id: 'authentication-method-id',
  name: 'My Work Phone',
});

// Delete a method
await myAccount.deleteAuthenticationMethodById({
  accessToken,
  id: 'authentication-method-id',
});

Getting Available Factors

const factors = await myAccount.getFactors({ accessToken });
// Returns available factor types (e.g., sms, email, totp, push-notification, webauthn-platform)

Error Handling

import { MyAccountError, MyAccountErrorCodes, PasskeyError, PasskeyErrorCodes } from 'react-native-auth0';

try {
  await myAccount.enrollPasskey({ ... });
} catch (e) {
  if (e instanceof PasskeyError) {
    switch (e.type) {
      case PasskeyErrorCodes.NOT_AVAILABLE:
        // Passkeys not supported on this device
        break;
      default:
        console.error(`Passkey error: [${e.type}] ${e.message}`);
    }
  } else if (e instanceof MyAccountError) {
    switch (e.type) {
      case MyAccountErrorCodes.ENROLLMENT_FAILED:
        // Enrollment failed
        break;
      case MyAccountErrorCodes.VERIFICATION_FAILED:
        // OTP verification failed
        break;
      case MyAccountErrorCodes.UNAUTHORIZED:
        // Token expired or insufficient scopes
        break;
      default:
        console.error(`My Account error: [${e.type}] ${e.message}`);
    }
  }
}

Platform Support

PlatformSupportNotes
iOS✅ SupportedPasskey enrollment requires iOS 16.6+
Android✅ SupportedPasskey enrollment requires Android API 28+
Web❌ Not SupportedThrows PasskeyError with PASSKEY_UNSUPPORTED_PLATFORM

Native to Web SSO

Native to Web SSO Overview

Native to Web SSO allows authenticated users in your native mobile application to seamlessly transition to your web application without requiring them to log in again. This is achieved by exchanging a refresh token for a Session Transfer Token, which can then be used to establish a session in the web application.

The Session Transfer Token is:

  • Short-lived: Expires after approximately 1 minute
  • Single-use: Can only be used once to establish a web session
  • Secure: Can be bound to the user's device through IP address or ASN

For detailed configuration and implementation guidance, see the Auth0 Native to Web SSO documentation.

Native to Web SSO Prerequisites

Before using Native to Web SSO:

  1. Enable Native to Web SSO on your Auth0 tenant - This feature requires an Enterprise plan
  2. Configure your native application
  3. Request offline_access scope during login to ensure a refresh token is issued

Using Native to Web SSO with Hooks

import { useAuth0 } from 'react-native-auth0';
import { Linking } from 'react-native';

function MyComponent() {
  const { authorize, getSSOCredentials } = useAuth0();

  const login = async () => {
    // Login with offline_access to get a refresh token
    await authorize({
      scope: 'openid profile email offline_access',
    });
  };

  const openWebApp = async () => {
    try {
      // Get session transfer credentials
      const ssoCredentials = await getSSOCredentials();

      console.log('Session Transfer Token:', ssoCredentials.sessionTransferToken);
      console.log('Token Type:', ssoCredentials.tokenType);
      console.log('Expires In:', ssoCredentials.expiresIn, 'seconds');

      // Open web app with session transfer token as query parameter
      const webAppUrl = `https://your-web-app.com/login?session_transfer_token=${ssoCredentials.sessionTransferToken}`;
      await Linking.openURL(webAppUrl);
    } catch (error) {
      console.error('Failed to get SSO credentials:', error);
    }
  };

  return (
    // Your UI components
  );
}

Using Native to Web SSO with Auth0 Class

import Auth0 from 'react-native-auth0';
import { Linking } from 'react-native';

const auth0 = new Auth0({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
});

// Login with offline_access scope
await auth0.webAuth.authorize({
  scope: 'openid profile email offline_access',
});

// Get session transfer credentials
const ssoCredentials = await auth0.credentialsManager.getSSOCredentials();

console.log('Session Transfer Token:', ssoCredentials.sessionTransferToken);
console.log('Token Type:', ssoCredentials.tokenType);
console.log('Expires In:', ssoCredentials.expiresIn);

// Optional: ID Token and Refresh Token may be returned if RTR is enabled
if (ssoCredentials.idToken) {
  console.log('ID Token:', ssoCredentials.idToken);
}
if (ssoCredentials.refreshToken) {
  console.log('New Refresh Token received (RTR enabled)');
}

// Open your web application with the session transfer token
const webAppUrl = `https://your-web-app.com/login?session_transfer_token=${ssoCredentials.sessionTransferToken}`;
await Linking.openURL(webAppUrl);

SSO Exchange via Authentication API

If your app manages tokens independently (without using the Credentials Manager), you can use auth.ssoExchange() to exchange a refresh token for a session transfer token directly via the Authentication API.

This is useful when:

  • Your app stores tokens outside of the built-in Credentials Manager
  • You need more control over the token exchange process
  • You want to perform the exchange as a standalone API call

Note: This method is only available on native platforms (iOS/Android). It is not supported on the web platform.

Using SSO Exchange with Hooks

import { useAuth0 } from 'react-native-auth0';
import { Linking } from 'react-native';

function SSOExchangeScreen() {
  const { ssoExchange } = useAuth0();

  const handleSSOExchange = async (refreshToken) => {
    try {
      const ssoCredentials = await ssoExchange({ refreshToken });

      console.log('Session Transfer Token:', ssoCredentials.sessionTransferToken);
      console.log('Token Type:', ssoCredentials.tokenType);
      console.log('Expires In:', ssoCredentials.expiresIn);

      // Open your web application with the session transfer token
      const webAppUrl = `https://your-web-app.com/login?session_transfer_token=${ssoCredentials.sessionTransferToken}`;
      await Linking.openURL(webAppUrl);
    } catch (error) {
      console.error('SSO Exchange failed:', error);
    }
  };

  return (
    // Your UI components
  );
}

Using SSO Exchange with Auth0 Class

import Auth0 from 'react-native-auth0';
import { Linking } from 'react-native';

const auth0 = new Auth0({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
});

// You must already have a refresh token (e.g., from a previous login with offline_access scope)
const refreshToken = 'YOUR_REFRESH_TOKEN';

// Exchange the refresh token for a session transfer token
const ssoCredentials = await auth0.auth.ssoExchange({ refreshToken });

console.log('Session Transfer Token:', ssoCredentials.sessionTransferToken);
console.log('Token Type:', ssoCredentials.tokenType);
console.log('Expires In:', ssoCredentials.expiresIn);

// Open your web application with the session transfer token
const webAppUrl = `https://your-web-app.com/login?session_transfer_token=${ssoCredentials.sessionTransferToken}`;
await Linking.openURL(webAppUrl);

Sending the Session Transfer Token

There are two ways to send the Session Transfer Token to your web application:

Option 1: As a Query Parameter

Pass the token as a URL parameter when opening your web application:

const ssoCredentials = await auth0.credentialsManager.getSSOCredentials();

// Your web app should extract the token and pass it to Auth0's /authorize endpoint
const webAppUrl = `https://your-web-app.com/login?session_transfer_token=${ssoCredentials.sessionTransferToken}`;
await Linking.openURL(webAppUrl);

Your web application should then include the session_transfer_token in the /authorize request:

// In your web application
const urlParams = new URLSearchParams(window.location.search);
const sessionTransferToken = urlParams.get('session_transfer_token');

if (sessionTransferToken) {
  // Include in your authorization request
  const authorizeUrl =
    `https://YOUR_AUTH0_DOMAIN/authorize?` +
    `client_id=YOUR_WEB_CLIENT_ID&` +
    `redirect_uri=${encodeURIComponent('https://your-web-app.com/callback')}&` +
    `response_type=code&` +
    `scope=openid profile email&` +
    `session_transfer_token=${sessionTransferToken}`;

  window.location.href = authorizeUrl;
}

If your application uses a WebView that supports cookie injection:

import { WebView } from 'react-native-webview';

function WebAppView() {
  const [cookies, setCookies] = useState('');

  const prepareWebSession = async () => {
    const ssoCredentials = await auth0.credentialsManager.getSSOCredentials();

    // Set cookie that will be sent to Auth0
    const cookie = `auth0_session_transfer_token=${ssoCredentials.sessionTransferToken}; path=/; domain=.your-auth0-domain.auth0.com; secure`;
    setCookies(cookie);
  };

  return (
    <WebView
      source={{ uri: 'https://your-web-app.com' }}
      sharedCookiesEnabled={true}
      // Additional WebView configuration for cookie injection
    />
  );
}

Note: Cookie injection is platform-specific and may require additional configuration. The query parameter method is generally more straightforward and recommended for most use cases.

MFA Flexible Factors Grant

The MFA Flexible Factors Grant provides programmatic control over Multi-Factor Authentication flows. Instead of relying solely on Universal Login, you can build custom MFA experiences — listing enrolled authenticators, enrolling new factors, triggering challenges, and verifying codes — all from your own UI.

This feature works across all platforms (iOS, Android, and Web).

Using MFA with Hooks

import React, { useState } from 'react';
import { View, Button, TextInput, Text, Alert } from 'react-native';
import { useAuth0, MfaError, MfaErrorCodes } from 'react-native-auth0';

function MfaScreen({ mfaToken }: { mfaToken: string }) {
  const { mfa } = useAuth0();
  const [otp, setOtp] = useState('');

  // List enrolled authenticators
  const listAuthenticators = async () => {
    try {
      const authenticators = await mfa.getAuthenticators({ mfaToken });
      console.log('Enrolled authenticators:', authenticators);
    } catch (error) {
      if (error instanceof MfaError) {
        console.error('MFA error:', error.type, error.message);
      }
    }
  };

  // Enroll a new TOTP authenticator
  const enrollTotp = async () => {
    try {
      const challenge = await mfa.enroll({ mfaToken, factorType: 'otp' });
      if (challenge.type === 'totp') {
        console.log('Scan this barcode:', challenge.barcodeUri);
        console.log('Or enter this secret:', challenge.secret);
      }
    } catch (error) {
      if (error instanceof MfaError) {
        switch (error.type) {
          case MfaErrorCodes.ENROLLMENT_FAILED:
            Alert.alert('Error', 'Enrollment failed. Please try again.');
            break;
          case MfaErrorCodes.EXPIRED_MFA_TOKEN:
            Alert.alert('Error', 'MFA session expired. Please start over.');
            break;
        }
      }
    }
  };

  // Enroll an SMS factor
  const enrollSms = async () => {
    try {
      const challenge = await mfa.enroll({
        mfaToken,
        factorType: 'sms',
        phoneNumber: '+12025550135',
      });
      if (challenge.type === 'oob') {
        // Keep challenge.oobCode in component state if you need it for verify().
      }
    } catch (error) {
      if (
        error instanceof MfaError &&
        error.type === MfaErrorCodes.INVALID_PHONE_NUMBER
      ) {
        Alert.alert('Error', 'Invalid phone number.');
      }
    }
  };

  // Trigger a challenge for an existing authenticator
  const triggerChallenge = async (authenticatorId: string) => {
    try {
      const result = await mfa.challenge({ mfaToken, authenticatorId });
      console.log('Challenge type:', result.challengeType);
      // Keep result.oobCode in component state for the verify() step;
      // avoid logging it.
    } catch (error) {
      if (error instanceof MfaError) {
        console.error('Challenge failed:', error.type);
      }
    }
  };

  // Verify an OTP code - this completes authentication
  const verifyOtp = async () => {
    try {
      const credentials = await mfa.verify({ mfaToken, otp });
      console.log('Authentication complete!');
      // User is now logged in - state is automatically updated
    } catch (error) {
      if (error instanceof MfaError) {
        switch (error.type) {
          case MfaErrorCodes.INVALID_OTP:
            Alert.alert('Error', 'Incorrect code. Please try again.');
            break;
          case MfaErrorCodes.TOO_MANY_ATTEMPTS:
            Alert.alert('Error', 'Too many attempts. Please wait.');
            break;
          case MfaErrorCodes.EXPIRED_MFA_TOKEN:
            Alert.alert('Error', 'Session expired. Please start over.');
            break;
        }
      }
    }
  };

  return (
    <View>
      <Button title="List Authenticators" onPress={listAuthenticators} />
      <Button title="Enroll TOTP" onPress={enrollTotp} />
      <Button title="Enroll SMS" onPress={enrollSms} />
      <TextInput
        placeholder="Enter OTP code"
        value={otp}
        onChangeText={setOtp}
        keyboardType="number-pad"
      />
      <Button title="Verify OTP" onPress={verifyOtp} />
    </View>
  );
}

Using MFA with Auth0 Class

import Auth0, { MfaError, MfaErrorCodes } from 'react-native-auth0';

const auth0 = new Auth0({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
});

// List enrolled authenticators
const authenticators = await auth0.mfa.getAuthenticators({
  mfaToken: 'mfa_token_from_login',
  // Optional: filter by factor type. Accepts the public MfaFactorType values
  // ('otp', 'sms', 'voice', 'email', 'push'). Omit to return all authenticators.
  factorsAllowed: ['otp', 'sms', 'email'],
});

// Enroll a TOTP authenticator
const totpChallenge = await auth0.mfa.enroll({
  mfaToken: 'mfa_token',
  factorType: 'otp',
});
// totpChallenge.type === 'totp'
// totpChallenge.barcodeUri - QR code URI
// totpChallenge.secret - Manual entry secret

// Enroll via SMS
const smsChallenge = await auth0.mfa.enroll({
  mfaToken: 'mfa_token',
  factorType: 'sms',
  phoneNumber: '+12025550135',
});

// Enroll via email
const emailChallenge = await auth0.mfa.enroll({
  mfaToken: 'mfa_token',
  factorType: 'email',
  email: 'user@example.com',
});

// Enroll via voice call
// Note: native (iOS/Android) enrolls voice as SMS on the same number;
// only the web platform supports a distinct voice channel.
const voiceChallenge = await auth0.mfa.enroll({
  mfaToken: 'mfa_token',
  factorType: 'voice',
  phoneNumber: '+12025550135',
});

// Enroll push notification (Auth0 Guardian)
const pushChallenge = await auth0.mfa.enroll({
  mfaToken: 'mfa_token',
  factorType: 'push',
});
// pushChallenge.type === 'push'
// pushChallenge.barcodeUri - QR code URI to pair the Guardian app
// pushChallenge.oobCode - used to verify the enrollment (not available on Android)

// Trigger an OOB challenge
const challenge = await auth0.mfa.challenge({
  mfaToken: 'mfa_token',
  authenticatorId: 'sms|dev_123',
});

// Verify with OTP
const credentials = await auth0.mfa.verify({
  mfaToken: 'mfa_token',
  otp: '123456',
});

// Verify with OOB code
const credentialsOob = await auth0.mfa.verify({
  mfaToken: 'mfa_token',
  oobCode: 'oob_code_from_challenge',
  bindingCode: '654321', // Optional, for SMS/email OOB
});

// Verify with recovery code
const credentialsRecovery = await auth0.mfa.verify({
  mfaToken: 'mfa_token',
  recoveryCode: 'ABCDEF123456',
});

MFA Error Handling

All MFA operations throw MfaError with a normalized, platform-agnostic type property:

import { MfaError, MfaErrorCodes } from 'react-native-auth0';

try {
  await auth0.mfa.verify({ mfaToken, otp: '123456' });
} catch (error) {
  if (error instanceof MfaError) {
    switch (error.type) {
      case MfaErrorCodes.INVALID_OTP:
        // OTP code is incorrect
        break;
      case MfaErrorCodes.INVALID_OOB_CODE:
        // OOB code is incorrect
        break;
      case MfaErrorCodes.INVALID_RECOVERY_CODE:
        // Recovery code is incorrect
        break;
      case MfaErrorCodes.EXPIRED_MFA_TOKEN:
        // MFA token has expired - restart the MFA flow
        break;
      case MfaErrorCodes.INVALID_MFA_TOKEN:
        // MFA token is invalid
        break;
      case MfaErrorCodes.TOO_MANY_ATTEMPTS:
        // Rate limited - wait before retrying
        break;
      case MfaErrorCodes.ENROLLMENT_FAILED:
        // Enrollment operation failed
        break;
      case MfaErrorCodes.INVALID_PHONE_NUMBER:
        // Phone number is invalid for enrollment
        break;
      case MfaErrorCodes.INVALID_EMAIL:
        // Email is invalid for enrollment
        break;
      case MfaErrorCodes.CHALLENGE_FAILED:
        // Challenge request failed
        break;
      case MfaErrorCodes.AUTHENTICATOR_NOT_FOUND:
        // Authenticator not found or not enrolled
        break;
      case MfaErrorCodes.UNSUPPORTED_FACTOR:
        // MFA factor type is not supported
        break;
      case MfaErrorCodes.ASSOCIATION_REQUIRED:
        // User must enroll before using the authenticator
        break;
      default:
        console.error('MFA error:', error.message);
    }
  }
}
Error CodeDescriptionAuth0 API CodeNative Bridge Code
INVALID_OTPOTP code is incorrectinvalid_otp, invalid_grant
INVALID_OOB_CODEOOB code is incorrectinvalid_oob_code
INVALID_BINDING_CODEBinding code is incorrectinvalid_binding_code
INVALID_RECOVERY_CODERecovery code is incorrectinvalid_recovery_code
ENROLLMENT_FAILEDMFA enrollment failedmfa_enrollment_failedMFA_ENROLLMENT_ERROR
INVALID_PHONE_NUMBERPhone number is invalid for enrollmentinvalid_phone_number
INVALID_EMAILEmail is invalid for enrollmentinvalid_email
EXPIRED_MFA_TOKENMFA token has expiredexpired_token
INVALID_MFA_TOKENMFA token is invalidmfa_token_invalid
TOO_MANY_ATTEMPTSRate limited - too many verification attemptstoo_many_attempts
CHALLENGE_FAILEDMFA challenge request failedmfa_challenge_failedMFA_CHALLENGE_ERROR
AUTHENTICATOR_NOT_FOUNDAuthenticator not found or not enrolled
UNSUPPORTED_FACTORMFA factor type is not supportedunsupported_challenge_type
ASSOCIATION_REQUIREDUser must enroll before using the authenticatorassociation_required
MFA_ERRORGeneric MFA errorMFA_VERIFY_ERROR
UNKNOWN_MFA_ERRORUnknown or uncategorized MFA error

Bot Protection

If you are using the Bot Protection feature and performing database login/signup via the Authentication API, you need to handle the requires_verification error. It indicates that the request was flagged as suspicious and an additional verification step is necessary to log the user in. That verification step is web-based, so you need to use Universal Login to complete it.

const email = 'support@auth0.com';
const realm = 'Username-Password-Authentication';
const scope = 'openid profile';

auth0.auth
  .passwordRealm({
    username: email,
    password: 'secret-password',
    realm: realm,
    scope: scope,
  })
  .then((credentials) => {
    // Logged in!
  })
  .catch((error) => {
    if (error.name === 'requires_verification') {
      auth0.webAuth
        .authorize({
          connection: realm,
          scope: scope,
          login_hint: email, // So the user doesn't have to type it again
        })
        .then((credentials) => {
          // Logged in!
        })
        .catch(console.error);
    } else {
      console.error(error);
    }
  });

In the case of signup, you can add an additional parameter to make the user land directly on the signup page:

auth0.webAuth.authorize({
  connection: realm,
  scope: scope,
  additionalParameters: {
    login_hint: email,
    screen_hint: 'signup', // 👈🏻
  },
});

Domain Switching

To switch between two different domains for authentication in your Android and iOS applications, follow these steps:

Android

To switch between two different domains for authentication in your Android application, you need to manually update your AndroidManifest.xml file. This involves adding an intent filter for the activity com.auth0.android.provider.RedirectActivity. Unlike using a single domain where you can add the domain and scheme values within the manifestPlaceholders of your app's build.gradle file, you need to add a <data> tag for each domain along with its scheme within the intent filter.

Here is an example:

<activity
    android:name="com.auth0.android.provider.RedirectActivity"
    tools:node="replace"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
            android:host="${domain1}"
            android:pathPrefix="/android/${applicationId}/callback"
            android:scheme="${applicationId}.auth0" />
        <data
            android:host="${domain2}"
            android:pathPrefix="/android/${applicationId}/callback"
            android:scheme="${applicationId}.auth0" />
    </intent-filter>
</activity>

If you customize the scheme by removing the default value of ${applicationId}.auth0, you will also need to pass it as the customScheme option parameter of the authorize and clearSession methods.

iOS

For iOS, if you are not customizing the scheme, adding $(PRODUCT_BUNDLE_IDENTIFIER).auth0 as an entry to the CFBundleURLSchemes array in your Info.plist file should be sufficient. However, if you want to customize the scheme for the domains, you need to add the customized scheme for each domain as an entry to the CFBundleURLSchemes array.

Here is an example:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>None</string>
        <key>CFBundleURLName</key>
        <string>auth0</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>$(customScheme1)</string>
            <string>$(customScheme2)</string>
        </array>
    </dict>
</array>

By following these steps, you can configure your Android and iOS applications to handle authentication for multiple domains.

Expo

If using a single domain, you can simply pass an object in the format to the react-native-auth0 plugin in your app.json as shown below:

"plugins": [
  "expo-router",
  ["react-native-auth0",
    {
      "domain": "sample.auth0.com",
      "customScheme": "sampleScheme"
    }
  ]
]

If you want to support multiple domains, you would have to pass an array of objects as shown below:

"plugins": [
  "expo-router",
  ["react-native-auth0",
    [{
      "domain": "sample.auth0.com",
      "customScheme": "sampleScheme"
    },
    {
      "domain": "sample2.auth0.com",
      "customScheme": "sampleScheme2"
    }]
  ]
]

You can skip sending the customScheme property if you do not want to customize it.

Switching tenants at runtime

The configuration above is build-time setup: it registers the redirect callback for every domain you intend to use. Switching the active tenant while the app is running is done in JavaScript by changing the domain/clientId you pass to the SDK.

When you change an identity-defining prop on Auth0Provider — domain, clientId, localAuthenticationOptions, timeout, or useDPoP — the provider rebuilds its underlying client so subsequent calls target the newly selected configuration. An unchanged config (or a change limited to options like headers or maxRetries) reuses the same client instance. Keep the props in state and update them to switch:

import React, { useState } from 'react';
import { Auth0Provider, useAuth0 } from 'react-native-auth0';

const TENANTS = [
  { domain: 'tenant-a.us.auth0.com', clientId: 'CLIENT_ID_A' },
  { domain: 'tenant-b.us.auth0.com', clientId: 'CLIENT_ID_B' },
];

const App = () => {
  const [index, setIndex] = useState(0);
  const tenant = TENANTS[index];

  return (
    // Changing domain/clientId recreates the client for the new tenant.
    <Auth0Provider domain={tenant.domain} clientId={tenant.clientId}>
      <Button
        title="Switch Tenant"
        onPress={() => setIndex((i) => (i + 1) % TENANTS.length)}
      />
      <LoginScreen />
    </Auth0Provider>
  );
};

After switching, the next authorize() call opens the login page for the newly selected tenant and the redirect resolves correctly, provided that tenant's domain/scheme was registered using the build-time configuration shown above.

Note: Switching tenants does not immediately clear the displayed auth state. The provider re-runs its initialization for the new tenant and updates user once that check completes, so the previously shown user may briefly remain until then.

Important — credentials are shared across tenants by default. The native credentials store (Android SharedPreferences, iOS Keychain) is not keyed by domain/clientId. With no extra configuration, every client reads and writes the same store, so after switching tenants getCredentials() / hasValidCredentials() return whatever was saved last — i.e. the previous tenant's session — and a fresh login on the new tenant overwrites it. To give each tenant its own isolated store, set credentialsManagerStorageKey (below).

If you are using the Auth0 class directly instead of the hooks, simply create (or reuse) an instance per tenant and call the one matching the active tenant:

import Auth0 from 'react-native-auth0';

const clients = {
  tenantA: new Auth0({
    domain: 'tenant-a.us.auth0.com',
    clientId: 'CLIENT_ID_A',
  }),
  tenantB: new Auth0({
    domain: 'tenant-b.us.auth0.com',
    clientId: 'CLIENT_ID_B',
  }),
};

// Use whichever client corresponds to the active tenant.
const credentials = await clients[activeTenant].webAuth.authorize();

Isolating credentials per tenant (credentialsManagerStorageKey)

By default all clients share a single native credentials store, so credentials from one tenant are visible to another after a switch (see the important note above). To keep each tenant's session separate, pass a unique credentialsManagerStorageKey. It maps to the Android SharedPreferences file name and the iOS Keychain service, so a distinct value gives that client its own physically separate store for saveCredentials / getCredentials / hasValidCredentials / clearCredentials.

const TENANTS = [
  // No key → uses the default shared store (keeps any existing logged-in user).
  { domain: 'tenant-a.us.auth0.com', clientId: 'CLIENT_ID_A' },
  // Distinct key → isolated store for this tenant.
  {
    domain: 'tenant-b.us.auth0.com',
    clientId: 'CLIENT_ID_B',
    credentialsManagerStorageKey: 'tenant-b',
  },
];

// Hooks: pass it as a prop. Changing it rebuilds the client.
<Auth0Provider
  domain={tenant.domain}
  clientId={tenant.clientId}
  credentialsManagerStorageKey={tenant.credentialsManagerStorageKey}
>
  <LoginScreen />
</Auth0Provider>;
// Auth0 class: pass it in the constructor options.
const auth0 = new Auth0({
  domain: 'tenant-b.us.auth0.com',
  clientId: 'CLIENT_ID_B',
  credentialsManagerStorageKey: 'tenant-b',
});

With the keys above, logging in to Tenant A and switching to Tenant B no longer surfaces Tenant A's credentials, and each tenant's login persists independently. Switching back to Tenant A restores its session without re-login.

Recommended usage

  • Leave the primary/default tenant without a key so existing installs keep using the store they already wrote to — those users are not logged out on upgrade.
  • Give every additional tenant a unique, stable key (e.g. the tenant slug or client ID).
  • Treat the key as permanent: it is the address of the store, not data inside it.

When does this make existing users log in again?

There is no automatic migration between stores — a client only ever reads the store its key points to. Changing which store a client uses therefore makes it start from an empty store, requiring a fresh login:

ScenarioExisting logged-in user re-login required?
Upgrade to this version, no credentialsManagerStorageKey set anywhereNo — still the default shared store.
Add a key to a client that previously had none (e.g. you now key your primary tenant)Yes — its old session lives in the default store, which the keyed client no longer reads.
Change an existing key to a different valueYes — points at a different (empty) store.
Remove a key that was previously setYes — falls back to the default store, not the keyed one.
Add a new tenant with its own new key (others unchanged)No for the others; the new tenant simply starts logged out.

Because changing or removing a key strands the credentials saved under the old key, choose each tenant's key once and keep it stable across releases. If you must change it, call clearCredentials() on the old configuration first (or accept that those credentials become orphaned in the old store).

Allowed Browsers (Android)

On Android, some browsers do not correctly handle App Link redirects. For example, Firefox renders the callback URL as a web page instead of handing the redirect back to your app, causing the authentication flow to fail silently.

You can restrict which browsers are allowed to handle the web authentication flow by passing allowedBrowserPackages in the options object. When set, only browsers whose package names appear in the list will be used.

Behaviour:

  • If the user's default browser is in the list, it is used.
  • If the user's default browser is not in the list but another allowed browser is installed, that browser is used instead.
  • If no allowed browser is installed, an a0.browser_not_available error is returned.

Platform Support: Android only. This option is ignored on iOS.

Using with Hooks

import { useAuth0 } from 'react-native-auth0';

const { authorize } = useAuth0();

await authorize(
  { scope: 'openid profile email' },
  {
    allowedBrowserPackages: [
      'com.android.chrome',
      'com.chrome.beta',
      'com.microsoft.emmx', // Edge
      'com.brave.browser',
      'com.sec.android.app.sbrowser', // Samsung Internet
    ],
  }
);

Using with Auth0 Class

import Auth0 from 'react-native-auth0';

const auth0 = new Auth0({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
});

await auth0.webAuth.authorize(
  { scope: 'openid profile email' },
  {
    allowedBrowserPackages: [
      'com.android.chrome',
      'com.chrome.beta',
      'com.microsoft.emmx', // Edge
      'com.brave.browser',
      'com.sec.android.app.sbrowser', // Samsung Internet
    ],
  }
);

The same allowedBrowserPackages option is also accepted by clearSession to restrict which browser handles the logout flow.

Trusted Web Activity (Android)

On Android, web authentication defaults to a Custom Tab, which shows a read-only URL bar at the top of the browser. A Trusted Web Activity (TWA) renders the login page full-screen with no address bar, giving a more integrated, app-like experience. Pass useTrustedWebActivity: true in the options object to opt in.

Behaviour:

  • When enabled, the login (and logout) page opens in a Trusted Web Activity instead of a Custom Tab.
  • TWA relies on Digital Asset Links verification between your app and your Auth0 domain. If verification fails — for example because the setup below is missing — it automatically falls back to a Custom Tab, so login still completes (just with the URL bar visible).

Platform Support: Android only. This option is ignored on iOS and web.

Required setup

TWA will only render full-screen if your app's signing certificate is registered with your Auth0 tenant. Without this, Digital Asset Links verification fails and the flow falls back to a Custom Tab.

  1. Get your app's SHA-256 certificate fingerprint. For a debug build:

    keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
    

    For a release build, run the same command against your release keystore. Copy the SHA256 fingerprint.

  2. In the Auth0 Dashboard, go to Application → Settings → Advanced → Device Settings → Key Hashes and add:

    • Your app's package name (e.g. com.myapp).
    • The SHA-256 fingerprint from step 1.
  3. Save. Digital Asset Links verification now succeeds and the login page renders full-screen.

You can confirm the registration by visiting https://YOUR_AUTH0_DOMAIN/.well-known/assetlinks.json and checking that your app's package name and SHA-256 fingerprint are listed.

Register the fingerprint for every signing config you ship (debug, release, and Play App Signing if you use it), otherwise TWA silently falls back to a Custom Tab for the unregistered builds.

Using with Hooks

import { useAuth0 } from 'react-native-auth0';

const { authorize } = useAuth0();

await authorize(
  { scope: 'openid profile email' },
  { useTrustedWebActivity: true }
);

Using with Auth0 Class

import Auth0 from 'react-native-auth0';

const auth0 = new Auth0({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
});

await auth0.webAuth.authorize(
  { scope: 'openid profile email' },
  { useTrustedWebActivity: true }
);

The same useTrustedWebActivity option is also accepted by clearSession so the logout flow opens in a Trusted Web Activity as well.

Recovering Login After Process Death (Android)

On Android, the OS can kill your app's process while the user is completing login in the browser — this is common on devices with aggressive memory management (e.g. Samsung One UI, Xiaomi MIUI), especially during MFA when the user switches apps to fetch a code. When the user finishes and the browser redirects back, the app cold-starts and, without recovery, the in-flight login is lost and the user lands back on the login screen.

resumeSession() recovers that login. The underlying native SDK finishes the token exchange after the process restarts and buffers the result; calling resumeSession() once on cold start drains it and returns the recovered Credentials (or null if there was nothing to recover).

This is an Android-only concern. On iOS and web resumeSession() is a no-op that resolves with null, so it is safe to call unconditionally. It requires react-native-auth0 bundling Auth0.Android 3.19.0+ (included). No native MainActivity changes are needed, so it works the same in bare React Native and Expo.

Recovering Login Using Hooks

Call resumeSession() once when your app mounts. If it returns credentials, the hook updates the auth state and persists them automatically, so user becomes populated.

import { useEffect } from 'react';
import { useAuth0 } from 'react-native-auth0';

const App = () => {
  const { resumeSession } = useAuth0();

  useEffect(() => {
    resumeSession()
      .then((credentials) => {
        if (credentials) {
          // A login interrupted by process death was recovered.
          console.log('Recovered session', credentials.accessToken);
        }
      })
      .catch((error) => {
        console.log('Failed to recover session', error);
      });
  }, [resumeSession]);

  // ...
};

Recovering Login Using the Auth0 Class

import Auth0 from 'react-native-auth0';

const auth0 = new Auth0({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
});

// Call once on cold start, before showing the login screen.
const credentials = await auth0.webAuth.resumeSession();
if (credentials) {
  await auth0.credentialsManager.saveCredentials(credentials);
  // The user is logged in — route them into the app.
}

DPoP (Demonstrating Proof-of-Possession)

DPoP (Demonstrating Proof-of-Possession) is an OAuth 2.0 extension that cryptographically binds access and refresh tokens to a client-specific key pair. This prevents token theft and replay attacks by ensuring that even if a token is intercepted, it cannot be used from a different device.

Enabling DPoP

DPoP is enabled by default (useDPoP: true) when you initialize the Auth0 client:

import Auth0 from 'react-native-auth0';

// DPoP is enabled by default
const auth0 = new Auth0({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
});

// Or explicitly enable it
const auth0 = new Auth0({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
  useDPoP: true, // Explicitly enable DPoP
});

Using Auth0Provider (React Hooks):

import { Auth0Provider } from 'react-native-auth0';

function App() {
  return (
    <Auth0Provider
      domain="YOUR_AUTH0_DOMAIN"
      clientId="YOUR_AUTH0_CLIENT_ID"
      // DPoP is enabled by default
    >
      {/* Your app components */}
    </Auth0Provider>
  );
}

Important: DPoP will only be used for new user sessions created after enabling it. Existing sessions with Bearer tokens will continue to work until the user logs in again. See Handling DPoP token migration for how to handle this transition.

Making API calls with DPoP

When calling your own APIs with DPoP-bound tokens, you need to include both the Authorization header and the DPoP proof header. The SDK provides a getDPoPHeaders() method to generate these headers:

import Auth0 from 'react-native-auth0';

const auth0 = new Auth0({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
  useDPoP: true,
});

async function callApi() {
  try {
    // Get credentials
    const credentials = await auth0.credentialsManager.getCredentials();

    // Generate DPoP headers for your API request
    const headers = await auth0.getDPoPHeaders({
      url: 'https://api.example.com/data',
      method: 'GET',
      accessToken: credentials.accessToken,
      tokenType: credentials.tokenType,
    });

    // Make the API call with the headers
    const response = await fetch('https://api.example.com/data', {
      method: 'GET',
      headers: {
        ...headers,
        'Content-Type': 'application/json',
      },
    });

    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('API call failed:', error);
  }
}

Using React Hooks:

import { useAuth0 } from 'react-native-auth0';

function MyComponent() {
  const { getCredentials, getDPoPHeaders } = useAuth0();

  const callApi = async () => {
    try {
      const credentials = await getCredentials();

      const headers = await getDPoPHeaders({
        url: 'https://api.example.com/data',
        method: 'POST',
        accessToken: credentials.accessToken,
        tokenType: credentials.tokenType,
      });

      const response = await fetch('https://api.example.com/data', {
        method: 'POST',
        headers: {
          ...headers,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ message: 'Hello' }),
      });

      return await response.json();
    } catch (error) {
      console.error('API call failed:', error);
    }
  };

  return <Button title="Call API" onPress={callApi} />;
}

Handling DPoP token migration

When you enable DPoP in your app, existing users will still have Bearer tokens until they log in again. You should implement logic to detect old tokens and prompt users to re-authenticate:

import Auth0 from 'react-native-auth0';

const auth0 = new Auth0({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
  useDPoP: true,
});

async function ensureDPoPTokens() {
  try {
    // Check if user has credentials
    const hasCredentials = await auth0.credentialsManager.hasValidCredentials();

    if (!hasCredentials) {
      // No credentials, user needs to log in
      return await auth0.webAuth.authorize();
    }

    // Get existing credentials
    const credentials = await auth0.credentialsManager.getCredentials();

    // Check if the token is DPoP
    if (credentials.tokenType !== 'DPoP') {
      console.log(
        'User has old Bearer token, clearing and re-authenticating...'
      );

      // Clear old credentials
      await auth0.credentialsManager.clearCredentials();

      // Prompt user to log in again with DPoP
      return await auth0.webAuth.authorize();
    }

    console.log('User already has DPoP token');
    return credentials;
  } catch (error) {
    console.error('Token migration failed:', error);
    throw error;
  }
}

// Call this when your app starts or when accessing protected resources
ensureDPoPTokens()
  .then((credentials) => console.log('Ready with DPoP tokens:', credentials))
  .catch((error) => console.error('Failed to ensure DPoP tokens:', error));

Using React Hooks:

import { useAuth0 } from 'react-native-auth0';
import { useEffect, useState } from 'react';

function App() {
  const { authorize, getCredentials, clearSession, hasValidCredentials } =
    useAuth0();
  const [isReady, setIsReady] = useState(false);
  const [needsMigration, setNeedsMigration] = useState(false);

  useEffect(() => {
    checkAndMigrateToDPoP();
  }, []);

  const checkAndMigrateToDPoP = async () => {
    try {
      const hasValid = await hasValidCredentials();

      if (!hasValid) {
        setIsReady(true);
        return;
      }

      const credentials = await getCredentials();

      if (credentials.tokenType !== 'DPoP') {
        setNeedsMigration(true);
        // Optionally auto-clear or wait for user action
        // await clearSession();
        // await authorize();
      }

      setIsReady(true);
    } catch (error) {
      console.error('Migration check failed:', error);
      setIsReady(true);
    }
  };

  const handleMigration = async () => {
    try {
      await clearSession();
      await authorize();
      setNeedsMigration(false);
    } catch (error) {
      console.error('Migration failed:', error);
    }
  };

  if (!isReady) {
    return <LoadingScreen />;
  }

  if (needsMigration) {
    return (
      <View>
        <Text>Security Update Required</Text>
        <Text>
          Please log in again to enhance your account security with DPoP.
        </Text>
        <Button title="Log In Again" onPress={handleMigration} />
      </View>
    );
  }

  return <YourApp />;
}

Checking token type

You can check whether credentials use DPoP or Bearer tokens:

const credentials = await auth0.credentialsManager.getCredentials();

if (credentials.tokenType === 'DPoP') {
  console.log('Using DPoP token - enhanced security enabled');

  // Generate DPoP headers for API calls
  const headers = await auth0.getDPoPHeaders({
    url: 'https://api.example.com/data',
    method: 'GET',
    accessToken: credentials.accessToken,
    tokenType: credentials.tokenType,
  });
} else {
  console.log('Using Bearer token - consider migrating to DPoP');

  // Standard Bearer authorization
  const headers = {
    Authorization: `Bearer ${credentials.accessToken}`,
  };
}

Handling nonce errors

Some APIs may require DPoP nonces to prevent replay attacks. If your API responds with a use_dpop_nonce error, you can retry the request with the nonce:

async function callApiWithNonce(url, method, credentials, retryCount = 0) {
  try {
    // Generate headers (initially without nonce)
    const headers = await auth0.getDPoPHeaders({
      url,
      method,
      accessToken: credentials.accessToken,
      tokenType: credentials.tokenType,
    });

    const response = await fetch(url, {
      method,
      headers: {
        ...headers,
        'Content-Type': 'application/json',
      },
    });

    // Check if nonce is required
    if (response.status === 401 && retryCount === 0) {
      const authHeader = response.headers.get('WWW-Authenticate');

      if (authHeader && authHeader.includes('use_dpop_nonce')) {
        // Extract nonce from response
        const nonce = response.headers.get('DPoP-Nonce');

        if (nonce) {
          console.log('Retrying with DPoP nonce...');

          // Retry with nonce
          const headersWithNonce = await auth0.getDPoPHeaders({
            url,
            method,
            accessToken: credentials.accessToken,
            tokenType: credentials.tokenType,
            nonce,
          });

          return await fetch(url, {
            method,
            headers: {
              ...headersWithNonce,
              'Content-Type': 'application/json',
            },
          });
        }
      }
    }

    return response;
  } catch (error) {
    console.error('API call with nonce failed:', error);
    throw error;
  }
}

// Usage
const credentials = await auth0.credentialsManager.getCredentials();
const response = await callApiWithNonce(
  'https://api.example.com/data',
  'GET',
  credentials
);
const data = await response.json();