← Files Auth0ARCHIVED FILE

skills/auth0/references/framework-expo/index.md

45.2 KB · Sep 30, 2026 · 23:15 UTC

↓ Download file

# Auth0 Expo Integration

Add authentication to Expo (React Native) applications using `react-native-auth0` with the Expo Config Plugin.

## Prerequisites

- Expo SDK 53 or higher (react-native-auth0 v5.x requires Expo 53+)
- React 19 and React Native 0.78.0 or higher
- Auth0 account with a **Native** application configured
- If Auth0 isn't set up yet, set it up first with the Auth0 CLI (`auth0 login`, then `auth0 apps create`)
- **Not compatible with Expo Go** — requires custom development client or EAS Build

## When NOT to Use

| Use Case | Use Instead |
|----------|------------------|
| Bare React Native CLI project (no Expo) | the Auth0 integration workflow for React Native |
| React web SPA (Vite/CRA) | the Auth0 integration workflow for React |
| Next.js application | the Auth0 integration workflow for Next.js |
| Vue.js SPA | the Auth0 integration workflow for Vue.js |
| Angular SPA | the Auth0 integration workflow for Angular |
| Express.js backend | the Auth0 integration workflow for Express.js |
| Native Android (Kotlin/Java) | the Auth0 integration workflow for Android |
| Backend API (JWT validation) | the Auth0 integration workflow for Fastify or Express.js |

## Files to Change

These are the files an Expo integration touches. Use the table as your starting scope: read these paths
directly instead of searching the project from scratch, but confirm the real entry point before editing
— an expo-router project roots the provider in `app/_layout.tsx`, not `App.tsx`. You do not need to
read anything under `node_modules/` to confirm the SDK's API.

| File | Change |
|------|--------|
| `package.json` | add `react-native-auth0` (and `expo-dev-client` if missing) |
| `.env` | `EXPO_PUBLIC_AUTH0_DOMAIN`, `EXPO_PUBLIC_AUTH0_CLIENT_ID` |
| `app.json` | `react-native-auth0` plugin with `domain` + `customScheme`; `ios.bundleIdentifier`, `android.package` |
| `App.tsx` (or your root component) | `Auth0Provider` + `useAuth0`, guarded by `isLoading` |

Do **not** hand-edit `ios/` or `android/` native files — the config plugin generates them at prebuild.

## Quick Start Workflow

### 1. Configure Auth0

**For automated setup with Auth0 CLI**, see the Setup Guide section (below) for complete scripts.

**For manual setup**, configure a **Native** application in the [Auth0 Dashboard](https://manage.auth0.com/) and note your Domain and Client ID.

### 2. Verify Expo Dev Client

> **Agent instruction:** Before installing the Auth0 SDK, check if the project has `expo-dev-client` installed. Read the project's `package.json` and look for `expo-dev-client` in `dependencies` or `devDependencies`.
>
> - **If `expo-dev-client` is found:** Proceed to step 3.
> - **If `expo-dev-client` is NOT found:** Use `AskUserQuestion` with the following message:
>
>   "The `react-native-auth0` SDK requires a custom Expo development client — it does **not** work with Expo Go. Your project does not have `expo-dev-client` installed.
>
>   How would you like to proceed?
>   1. **Install it for me** — I'll run `npx expo install expo-dev-client` and continue setup
>   2. **I'll set it up myself** — skip this step and continue to Auth0 SDK installation"
>
>   If the user picks option 1, run:
>   ```bash
>   npx expo install expo-dev-client
>   ```
>   Then proceed to step 3. If option 2, proceed to step 3 directly.

### 3. Install SDK

```bash
npx expo install react-native-auth0
```

Create `.env` for the values your JavaScript reads. Expo inlines `EXPO_PUBLIC_*` variables at build
time, so no extra config is needed:

```bash
EXPO_PUBLIC_AUTH0_DOMAIN=your-tenant.auth0.com
EXPO_PUBLIC_AUTH0_CLIENT_ID=your-client-id
```

Never hardcode the Client ID into a `.tsx`/`.ts` source file — read it from `process.env` (see step 6),
and add `.env` to `.gitignore`. That includes fallbacks: write
`process.env.EXPO_PUBLIC_AUTH0_CLIENT_ID!`, never
`process.env.EXPO_PUBLIC_AUTH0_CLIENT_ID ?? 'barkbook_client_abc123xyz'` — a default puts the literal
straight back into source.

The `domain` and `customScheme` in `app.json` (step 4) stay as literal values: they are not secrets,
and the config plugin is read at prebuild time rather than runtime.

### 4. Configure Expo Config Plugin

Add the react-native-auth0 plugin to `app.json` (or `app.config.js`) with your Auth0 domain and a custom scheme. Also ensure `bundleIdentifier` (iOS) and `package` (Android) are set:

```json
{
  "expo": {
    "ios": { "bundleIdentifier": "com.yourcompany.yourapp" },
    "android": { "package": "com.yourcompany.yourapp" },
    "plugins": [
      ["react-native-auth0", {
        "domain": "YOUR_AUTH0_DOMAIN",
        "customScheme": "YOUR_CUSTOM_SCHEME"
      }]
    ]
  }
}
```

The `customScheme` must be all lowercase with no special characters (e.g., `auth0sample`). See the **Setup Guide** section (below) for HTTPS callbacks, multiple domains, EAS Build, and secret management.

### 5. Configure Callback URLs

Add to **Allowed Callback URLs** and **Allowed Logout URLs** in the [Auth0 Dashboard](https://manage.auth0.com/):

```text
YOUR_CUSTOM_SCHEME://YOUR_AUTH0_DOMAIN/ios/YOUR_BUNDLE_ID/callback,
YOUR_CUSTOM_SCHEME://YOUR_AUTH0_DOMAIN/android/YOUR_PACKAGE/callback
```

All values must be **lowercase** with **no trailing slash**. For HTTPS callback URLs (App Links / Universal Links), see the Setup Guide — Using HTTPS Callback URLs section (below).

### 6. Add Authentication with Auth0Provider

Wrap your app with `Auth0Provider` and use the `useAuth0` hook:

> **Agent instruction:** Before adding new UI elements, search the project for existing click handlers for login, logout, sign-in, or sign-out buttons. If existing handlers are found, hook the Auth0 code into them. Only create new buttons if no existing handlers are found.

```typescript
import React from 'react';
import { Auth0Provider, useAuth0 } from 'react-native-auth0';
import { View, Button, Text, ActivityIndicator } from 'react-native';

function HomeScreen() {
  const { authorize, clearSession, user, isLoading, error } = useAuth0();

  const login = async () => {
    try {
      await authorize(
        { scope: 'openid profile email' },
        { customScheme: 'YOUR_CUSTOM_SCHEME' }
      );
    } catch (e) {
      console.error('Login error:', e);
    }
  };

  const logout = async () => {
    try {
      await clearSession({ customScheme: 'YOUR_CUSTOM_SCHEME' });
    } catch (e) {
      console.error('Logout error:', e);
    }
  };

  if (isLoading) {
    return <ActivityIndicator size="large" />;
  }

  return (
    <View>
      {user ? (
        <>
          <Text>Welcome, {user.name}!</Text>
          <Text>{user.email}</Text>
          <Button title="Log Out" onPress={logout} />
        </>
      ) : (
        <Button title="Log In" onPress={login} />
      )}
      {error && <Text>{error.message}</Text>}
    </View>
  );
}

export default function App() {
  return (
    <Auth0Provider
      domain={process.env.EXPO_PUBLIC_AUTH0_DOMAIN!}
      clientId={process.env.EXPO_PUBLIC_AUTH0_CLIENT_ID!}
    >
      <HomeScreen />
    </Auth0Provider>
  );
}
```

### 7. Verify

Verification has two parts, because a typecheck proves nothing about native configuration.

**Source** — run the cheapest check that proves the JavaScript compiles:

```bash
npm run typecheck   # or: npx tsc --noEmit
```

**Native configuration** — a typecheck cannot see whether the config plugin applied, so verify these by
reading the files back:

- `app.json` registers `react-native-auth0` in `plugins` with `domain` and `customScheme`
- `ios.bundleIdentifier` and `android.package` are set
- `authorize()` / `clearSession()` pass the same `customScheme` declared in `app.json`

> **Agent instruction:** If you need to confirm the plugin actually writes the native projects, run
> `npx expo prebuild --no-install` and check that `ios/{AppName}/Info.plist` contains the URL scheme and
> `android/app/build.gradle` contains the `auth0Domain` / `auth0Scheme` placeholders. A full
> `expo run:ios` / `run:android` cycle takes many minutes and needs a configured native toolchain — run
> it only when the user asks for a device or simulator run. Do not loop on native build failures by
> default.

Common build failures, when you do run a native build:

| Failure | Cause and fix |
|---------|---------------|
| `Invariant Violation: Native module cannot be null` | Using Expo Go instead of a development build — run `npx expo run:ios` / `run:android`, not `npx expo start` |
| Plugin not applied | `react-native-auth0` missing from the `app.json` plugins array |
| Pod install fails (iOS) | Run `npx expo prebuild --clean` to regenerate native projects |
| Manifest merge failure (Android) | Conflicting `auth0Domain` placeholder — only the config plugin should set the domain |

## Detailed Documentation

- **Setup Guide** (see the Setup Guide section below) — Dev client requirement, automated setup, Expo config plugin, callback URLs, EAS Build, secret management
- **Integration Patterns** (see the Integration Patterns section below) — Login/logout, credential management, biometric auth, token refresh, organizations, DPoP, error handling
- **API Reference & Testing** (see the API Reference & Testing section below) — Configuration options, useAuth0 hook API, testing checklist, common issues, security

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| Using Expo Go instead of development build | react-native-auth0 requires native code. Use `npx expo run:ios` / `npx expo run:android` or create a development build with EAS. |
| Missing `customScheme` in authorize/clearSession calls | Pass `{ customScheme: 'your-scheme' }` as the second argument to `authorize()` and `clearSession()`. Must match the value in app.json plugin config. |
| Callback URL mismatch | Ensure callback URL is all lowercase, no trailing slash, and matches Auth0 Dashboard exactly: `{customScheme}://{domain}/ios/{bundleId}/callback` |
| App type not set to Native | The Auth0 application must be type **Native** in the Dashboard, not SPA or Regular Web. |
| Missing bundleIdentifier or package in app.json | Both `expo.ios.bundleIdentifier` and `expo.android.package` must be set in app.json for callback URLs to work. |
| Forgot to wrap app with Auth0Provider | All components using `useAuth0()` must be children of `Auth0Provider`. |
| Using react-native-auth0 v5.x with Expo < 53 | Version 5.x requires Expo 53+. Use v4.x for older Expo versions. |
| Not testing on physical device | Biometric authentication (Face ID, fingerprint) only works on a physical device, not simulators. Always test the full auth flow on a real device before release. |

## Related Capabilities

- Auth0 setup — set up an account and application with the Auth0 CLI (`auth0 login`, then `auth0 apps create`)
- Bare React Native CLI projects → the Auth0 integration workflow for React Native
- Multi-factor authentication → ask for MFA (feature:mfa)
- Managing Auth0 resources from the terminal → the Auth0 CLI (`tooling-cli`)

## References

- [Auth0 Expo Quickstart](https://auth0.com/docs/quickstart/native/react-native-expo/interactive)
- [react-native-auth0 GitHub Repository](https://github.com/auth0/react-native-auth0)
- [react-native-auth0 API Documentation](https://auth0.github.io/react-native-auth0/)
- [Expo Sample App](https://github.com/auth0-samples/auth0-react-native-sample/tree/master/00-Login-Expo)
- [EXAMPLES.md](https://github.com/auth0/react-native-auth0/blob/master/EXAMPLES.md)

---

# auth0-expo API Reference & Testing

## Table of Contents

- [Configuration Reference](#configuration-reference) — Auth0Provider props, authorize/clearSession/getCredentials options
- [Expo Config Plugin Reference](#expo-config-plugin-reference) — app.json plugin fields and auto-configuration
- [User Profile Claims](#user-profile-claims) — Standard OIDC claims
- [Credentials Object](#credentials-object) — Token properties
- [Testing Checklist](#testing-checklist) — Dev build, platform-specific, Auth0 config, EAS
- [Common Issues](#common-issues) — Error table with causes and solutions
- [Security Considerations](#security-considerations) — PKCE, secure storage, custom scheme, tokens, network

## Configuration Reference

This reference is authoritative for `react-native-auth0` v5. Treat it as complete: you do not need to
read `.d.ts` files, the SDK README, or anything else under `node_modules/` to confirm these names.

### useAuth0() Hook

| Value | Description |
|-------|-------------|
| `authorize(params?, options?)` | Initiate login |
| `clearSession(options?)` | Logout |
| `user` | User profile object, or `null` when unauthenticated |
| `isLoading` | Loading state; guard auth-dependent UI on this |
| `error` | Last authentication error, or `null` |
| `getCredentials()` | Get tokens for API calls |

### Auth0Provider Props

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `domain` | `string` | Yes | Auth0 tenant domain (e.g., `your-tenant.auth0.com`) |
| `clientId` | `string` | Yes | Auth0 application Client ID |
| `localAuthenticationOptions` | `LocalAuthenticationOptions` | No | Biometric authentication configuration |
| `maxRetries` | `number` | No | Credential renewal retry count (iOS only, default: 0) |
| `useDPoP` | `boolean` | No | Enable DPoP token binding (default: true) |
| `headers` | `Record<string, string>` | No | Custom headers for all API requests |

### authorize() Options

**First argument (parameters):**

| Parameter | Type | Description |
|-----------|------|-------------|
| `scope` | `string` | OAuth scopes (default: `openid profile email`) |
| `audience` | `string` | API identifier for access token |
| `organization` | `string` | Organization ID for enterprise login |
| `invitationUrl` | `string` | Organization invitation URL |
| `connection` | `string` | Force a specific connection (e.g., `google-oauth2`) |
| `additionalParameters` | `object` | Extra parameters for the /authorize endpoint |

**Second argument (options):**

| Option | Type | Description |
|--------|------|-------------|
| `customScheme` | `string` | **Required for Expo.** URL scheme matching app.json plugin config. |

### clearSession() Options

| Option | Type | Description |
|--------|------|-------------|
| `customScheme` | `string` | **Required for Expo.** Must match the scheme used in authorize(). |
| `federated` | `boolean` | If true, also logs out from the identity provider |

### getCredentials() Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `scope` | `string` | Minimum required scope |
| `minTtl` | `number` | Minimum time-to-live in seconds for the access token |
| `parameters` | `object` | Additional parameters |
| `forceRefresh` | `boolean` | Force token refresh even if not expired |

## Expo Config Plugin Reference

### app.json Plugin Configuration

```json
["react-native-auth0", {
  "domain": "your-tenant.auth0.com",
  "customScheme": "auth0sample"
}]
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `domain` | `string` | Yes | Auth0 tenant domain |
| `customScheme` | `string` | No | Custom URL scheme (lowercase, no special chars). If `"https"`, enables Android App Links with `autoVerify`. |

**What the plugin configures automatically:**
- **iOS**: Adds URL scheme to Info.plist (`CFBundleURLSchemes`), adds deep linking handler to AppDelegate
- **Android**: Sets `manifestPlaceholders` (`auth0Domain`, `auth0Scheme`) in build.gradle

## User Profile Claims

| Claim | Type | Description |
|-------|------|-------------|
| `sub` | `string` | Unique user identifier |
| `name` | `string` | Full name |
| `nickname` | `string` | Display name |
| `email` | `string` | Email address |
| `email_verified` | `boolean` | Whether email is verified |
| `picture` | `string` | Profile picture URL |
| `updated_at` | `string` | Last profile update timestamp |
| `org_id` | `string` | Organization ID (if using Organizations) |

## Credentials Object

| Property | Type | Description |
|----------|------|-------------|
| `accessToken` | `string` | Access token for API calls |
| `idToken` | `string` | ID token with user claims |
| `refreshToken` | `string` | Refresh token (if `offline_access` requested) |
| `tokenType` | `string` | Token type (`Bearer` or `DPoP`) |
| `expiresAt` | `number` | Token expiration timestamp |
| `scope` | `string` | Granted scopes |

## Testing Checklist

### Development Build Testing

- [ ] Login flow: Tap login → browser opens → complete login → app shows user info
- [ ] Logout flow: Tap logout → session cleared → app shows login button
- [ ] Credential persistence: Close app → reopen → user remains logged in
- [ ] Token refresh: Wait for token expiry → `getCredentials()` returns fresh token
- [ ] Error handling: Cancel login → app handles USER_CANCELLED gracefully
- [ ] Loading state: `isLoading` is true until auth state is determined

### Platform-Specific Testing

- [ ] **iOS Simulator**: Login/logout works, URL scheme redirects correctly
- [ ] **Android Emulator**: Login/logout works, custom scheme callback received
- [ ] **Physical iOS Device**: Test on a real physical device — Face ID / Touch ID prompts work (if biometrics enabled). Note: biometric authentication is not available on simulators.
- [ ] **Physical Android Device**: Test on a real physical device — fingerprint / PIN prompts work (if biometrics enabled). Test deep link redirection from browser back to app.

### Auth0 Configuration Testing

- [ ] Callback URL matches exactly (lowercase, no trailing slash)
- [ ] Application type is **Native** in Auth0 Dashboard
- [ ] Allowed Callback URLs include both iOS and Android URLs
- [ ] Allowed Logout URLs include both iOS and Android URLs
- [ ] OIDC Conformant toggle is enabled in Advanced OAuth settings

### EAS Build Testing

- [ ] Development build: `eas build --profile development` succeeds
- [ ] Config plugin applied: Native files contain Auth0 configuration after prebuild
- [ ] Production build: `eas build --profile production` succeeds

## Common Issues

| Issue | Cause | Solution |
|-------|-------|----------|
| "Invariant Violation: Native module cannot be null" | Using Expo Go instead of development build | Run `npx expo run:ios` or `npx expo run:android`, or create a development build with EAS |
| App hangs after login | Callback URL mismatch | Verify callback URL is lowercase, no trailing slash, and matches Auth0 Dashboard exactly |
| Login opens but redirects fail | Missing customScheme in authorize call | Pass `{ customScheme: 'your-scheme' }` as second argument to `authorize()` |
| "PKCE not allowed" error | App type is not Native | Change application type to **Native** in Auth0 Dashboard |
| Blank screen after authentication | React Navigation interference | Ensure Auth0Provider wraps the entire navigation container |
| Android build fails with manifest errors | Conflicting auth0Domain placeholders | Remove manual manifest changes — let the Expo config plugin handle it |
| iOS build fails with pod errors | Stale native projects | Run `npx expo prebuild --clean` to regenerate native code |
| Token refresh fails silently | Missing `offline_access` scope | Include `offline_access` in the scope parameter during login |
| Biometric prompt not showing | Simulator limitation | Test biometrics on a physical device — simulators have limited biometric support |

## Security Considerations

### PKCE (Proof Key for Code Exchange)

The SDK uses PKCE by default for all Web Auth flows. PKCE protects against authorization code interception attacks. No additional configuration is needed.

### Secure Credential Storage

Credentials are stored securely:
- **iOS**: Encrypted in the Keychain
- **Android**: Encrypted in SharedPreferences via SecureCredentialsManager

Never store tokens manually in AsyncStorage, MMKV, or other unencrypted storage.

### Custom Scheme Security

Custom URL schemes can be subject to [client impersonation attacks](https://datatracker.ietf.org/doc/html/rfc8252#section-8.6). For production apps, consider using:
- **Android App Links** (`customScheme: "https"`) — requires SHA256 fingerprint configuration
- **iOS Universal Links** — requires Associated Domains and Apple Developer account

### Token Handling Best Practices

- Never log tokens to the console in production builds
- Use `getCredentials()` to access tokens — it auto-refreshes expired tokens
- Request `offline_access` scope for refresh token support
- Do not store tokens in React state — use `getCredentials()` on demand
- Enable DPoP for enhanced token security (enabled by default)

### Network Security

- All Auth0 API communication uses HTTPS
- The SDK validates ID token signatures, issuer, audience, and nonce
- Enable certificate pinning for additional security in high-security environments

## Related Capabilities

- Auth0 setup — set up an account and application with the Auth0 CLI (`auth0 login`, then `auth0 apps create`)
- Bare React Native CLI projects → the Auth0 integration workflow for React Native
- Multi-factor authentication → ask for MFA (feature:mfa)

## References

- [react-native-auth0 API Docs](https://auth0.github.io/react-native-auth0/)
- [Auth0 Expo Quickstart](https://auth0.com/docs/quickstart/native/react-native-expo/interactive)
- [Expo Config Plugins Guide](https://docs.expo.dev/guides/config-plugins/)
- [Auth0 Universal Login](https://auth0.com/docs/authenticate/login/auth0-universal-login)
- [PKCE RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)

---

# auth0-expo Integration Patterns

## Table of Contents

- [Web Auth Login](#web-auth-login) — Basic login with hooks, Auth0 class, audience, organizations
- [Web Auth Logout](#web-auth-logout) — Hook and class-based logout
- [Credential Management](#credential-management) — Retrieve, check, auto-refresh, Auth0 class
- [Biometric Authentication](#biometric-authentication) — Auth0Provider config, policies, Auth0 class
- [DPoP](#dpop-demonstrating-proof-of-possession) — Enable, API calls, token migration
- [Multi-Resource Refresh Tokens](#multi-resource-refresh-tokens-mrrt) — Multiple API access
- [Custom Token Exchange](#custom-token-exchange-rfc-8693) — External provider tokens
- [Native to Web SSO](#native-to-web-sso) — Session transfer to web apps
- [Organization Invitations](#organization-invitations) — Deep link handling
- [Error Handling](#error-handling) — WebAuth errors, Credentials Manager errors
- [Credential Renewal Retry](#credential-renewal-retry-ios) — iOS retry with backoff
- [Using Custom Headers](#using-custom-headers) — Custom API request headers

## Web Auth Login

The primary authentication method uses Auth0 Universal Login via the system browser. The `useAuth0` hook provides the `authorize` method.

### Basic Login with Hooks

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

function LoginScreen() {
  const { authorize, user, isLoading, error } = useAuth0();

  const login = async () => {
    try {
      await authorize(
        { scope: 'openid profile email' },
        { customScheme: 'auth0sample' }
      );
    } catch (e) {
      console.error('Login error:', e);
    }
  };

  if (isLoading) return <ActivityIndicator />;

  return (
    <View>
      {!user && <Button title="Log In" onPress={login} />}
      {user && <Text>Welcome, {user.name}!</Text>}
      {error && <Text style={{ color: 'red' }}>{error.message}</Text>}
    </View>
  );
}
```

### Login with Auth0 Class (Non-Hook)

```typescript
import Auth0 from 'react-native-auth0';

const auth0 = new Auth0({
  domain: process.env.EXPO_PUBLIC_AUTH0_DOMAIN!,
  clientId: process.env.EXPO_PUBLIC_AUTH0_CLIENT_ID!,
});

const credentials = await auth0.webAuth.authorize(
  { scope: 'openid profile email' },
  { customScheme: 'auth0sample' }
);
// Access token available at credentials.accessToken
```

### Login with Audience (API Access)

To get an access token for a specific API:

```typescript
await authorize(
  {
    scope: 'openid profile email offline_access',
    audience: 'https://your-api.example.com',
  },
  { customScheme: 'auth0sample' }
);
```

### Login with Organization

```typescript
await authorize(
  {
    scope: 'openid profile email',
    organization: 'org_abc123',
  },
  { customScheme: 'auth0sample' }
);
```

## Web Auth Logout

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

function LogoutButton() {
  const { clearSession } = useAuth0();

  const logout = async () => {
    try {
      await clearSession({ customScheme: 'auth0sample' });
    } catch (e) {
      console.error('Logout error:', e);
    }
  };

  return <Button title="Log Out" onPress={logout} />;
}
```

### Logout with Auth0 Class

```typescript
await auth0.webAuth.clearSession({}, { customScheme: 'auth0sample' });
await auth0.credentialsManager.clearCredentials();
```

## Credential Management

The `Auth0Provider` automatically stores and manages credentials. When using hooks, credentials are saved after login and cleared after logout automatically.

### Retrieve Stored Credentials

```typescript
const { getCredentials } = useAuth0();

const fetchData = async () => {
  try {
    const credentials = await getCredentials();
    const response = await fetch('https://your-api.example.com/data', {
      headers: {
        Authorization: `Bearer ${credentials.accessToken}`,
      },
    });
    const data = await response.json();
  } catch (e) {
    console.error('Failed to get credentials:', e);
  }
};
```

### Check for Valid Credentials

```typescript
const { hasValidCredentials } = useAuth0();

useEffect(() => {
  const checkAuth = async () => {
    const isLoggedIn = await hasValidCredentials();
    if (isLoggedIn) {
      // User has valid stored credentials
      navigation.navigate('Home');
    } else {
      navigation.navigate('Login');
    }
  };
  checkAuth();
}, []);
```

### Credential Auto-Refresh

The credentials manager automatically refreshes expired access tokens using the refresh token. Ensure you request the `offline_access` scope during login:

```typescript
await authorize(
  { scope: 'openid profile email offline_access' },
  { customScheme: 'auth0sample' }
);
```

### Credentials with Auth0 Class

```typescript
// Check for credentials
const isLoggedIn = await auth0.credentialsManager.hasValidCredentials();

// Get credentials (auto-refreshes if expired)
const credentials = await auth0.credentialsManager.getCredentials();

// Save credentials manually (not needed with hooks — auto-managed)
await auth0.credentialsManager.saveCredentials(credentials);

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

## Biometric Authentication

Protect credential access with biometric authentication (Face ID, Touch ID, fingerprint).

### With Auth0Provider (Hooks)

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

export default function App() {
  return (
    <Auth0Provider
      domain={process.env.EXPO_PUBLIC_AUTH0_DOMAIN!}
      clientId={process.env.EXPO_PUBLIC_AUTH0_CLIENT_ID!}
      localAuthenticationOptions={{
        title: 'Authenticate to access credentials',
        subtitle: 'Please verify your identity',
        cancelTitle: 'Cancel',
        evaluationPolicy: LocalAuthenticationStrategy.deviceOwnerWithBiometrics,
        fallbackTitle: 'Use Passcode',
        authenticationLevel: LocalAuthenticationLevel.strong,
        deviceCredentialFallback: true,
        biometricPolicy: BiometricPolicy.session,
        biometricTimeout: 300, // 5 minutes
      }}
    >
      <HomeScreen />
    </Auth0Provider>
  );
}
```

### Biometric Policy Types

| Policy | Behavior |
|--------|----------|
| `BiometricPolicy.default` | System-managed. May skip prompt if recently authenticated. |
| `BiometricPolicy.always` | Always prompts for biometric on every credential access. |
| `BiometricPolicy.session` | Prompts once per session; reuses for the specified timeout. |
| `BiometricPolicy.appLifecycle` | Prompts once until app restarts or credentials are cleared. |

### With Auth0 Class

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

const auth0 = new Auth0({
  domain: process.env.EXPO_PUBLIC_AUTH0_DOMAIN!,
  clientId: process.env.EXPO_PUBLIC_AUTH0_CLIENT_ID!,
  localAuthenticationOptions: {
    title: 'Authenticate to access credentials',
    evaluationPolicy: LocalAuthenticationStrategy.deviceOwnerWithBiometrics,
    authenticationLevel: LocalAuthenticationLevel.strong,
    biometricPolicy: BiometricPolicy.always,
  },
});
```

## DPoP (Demonstrating Proof-of-Possession)

DPoP cryptographically binds tokens to a client-specific key pair, preventing token theft.

### Enable DPoP

DPoP is enabled by default in react-native-auth0:

```typescript
<Auth0Provider
  domain={process.env.EXPO_PUBLIC_AUTH0_DOMAIN!}
  clientId={process.env.EXPO_PUBLIC_AUTH0_CLIENT_ID!}
  // DPoP is enabled by default (useDPoP: true)
>
  <App />
</Auth0Provider>
```

### Make API Calls with DPoP

```typescript
const { getCredentials, getDPoPHeaders } = useAuth0();

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

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

### Handle DPoP Token Migration

```typescript
const { getCredentials, clearSession, authorize } = useAuth0();

const ensureDPoP = async () => {
  const credentials = await getCredentials();
  if (credentials.tokenType !== 'DPoP') {
    await clearSession({ customScheme: 'auth0sample' });
    await authorize(
      { scope: 'openid profile email' },
      { customScheme: 'auth0sample' }
    );
  }
};
```

## Multi-Resource Refresh Tokens (MRRT)

Access tokens for multiple APIs using a single refresh token:

```typescript
const { authorize, getApiCredentials, clearApiCredentials } = useAuth0();

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

// Get token for a different API
const apiCredentials = await getApiCredentials(
  'https://second-api.example.com',
  'read:data write:data'
);
// Access token available at apiCredentials.accessToken
```

## Custom Token Exchange (RFC 8693)

Exchange external provider tokens for Auth0 tokens:

```typescript
import { useAuth0, AuthenticationException, AuthenticationErrorCodes } from 'react-native-auth0';

const { customTokenExchange } = useAuth0();

try {
  const credentials = await customTokenExchange({
    subjectToken: 'token-from-external-provider',
    subjectTokenType: 'urn:acme:legacy-system-token',
    scope: 'openid profile email',
  });
} catch (e) {
  if (e instanceof AuthenticationException) {
    if (e.type === AuthenticationErrorCodes.INVALID_SUBJECT_TOKEN) {
      console.error('External token is invalid or expired');
    }
  }
}
```

## Native to Web SSO

Transfer authenticated sessions from the Expo app to a web application:

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

const { getSSOCredentials } = useAuth0();

const openWebApp = async () => {
  const ssoCredentials = await getSSOCredentials();
  const webAppUrl = `https://webapp.example.com/login?session_transfer_token=${ssoCredentials.sessionTransferToken}`;
  await Linking.openURL(webAppUrl);
};
```

## Organization Invitations

Handle organization invitation links:

```typescript
import { Linking } from 'react-native';

const handleInvitation = async (url: string) => {
  await auth0.webAuth.authorize(
    { invitationUrl: url },
    { customScheme: 'auth0sample' }
  );
};

// Listen for deep links
Linking.addEventListener('url', ({ url }) => {
  if (url.includes('invitation=')) {
    handleInvitation(url);
  }
});
```

## Error Handling

### WebAuth Errors

```typescript
import { WebAuthError, WebAuthErrorCodes } from 'react-native-auth0';

try {
  await authorize(
    { scope: 'openid profile email' },
    { customScheme: 'auth0sample' }
  );
} catch (e) {
  if (e instanceof WebAuthError) {
    switch (e.type) {
      case WebAuthErrorCodes.USER_CANCELLED:
        console.log('User cancelled login');
        break;
      case WebAuthErrorCodes.BROWSER_NOT_AVAILABLE:
        console.log('No browser available on device');
        break;
      case WebAuthErrorCodes.PKCE_NOT_ALLOWED:
        console.log('PKCE not enabled — set app type to Native in Auth0 Dashboard');
        break;
      case WebAuthErrorCodes.NETWORK_ERROR:
        console.log('Network error — check connectivity');
        break;
      default:
        console.error('Auth error:', e.message);
    }
  }
}
```

### Credentials Manager Errors

```typescript
import {
  CredentialsManagerError,
  CredentialsManagerErrorCodes,
} from 'react-native-auth0';

try {
  const credentials = await getCredentials();
} catch (e) {
  if (e instanceof CredentialsManagerError) {
    switch (e.type) {
      case CredentialsManagerErrorCodes.NO_CREDENTIALS:
        console.log('No credentials stored — user needs to log in');
        break;
      case CredentialsManagerErrorCodes.NO_REFRESH_TOKEN:
        console.log('No refresh token — request offline_access scope');
        break;
      case CredentialsManagerErrorCodes.RENEW_FAILED:
        console.log('Token refresh failed — re-authentication required');
        break;
      case CredentialsManagerErrorCodes.BIOMETRICS_FAILED:
        console.log('Biometric authentication failed');
        break;
      default:
        console.error('Credentials error:', e.message);
    }
  }
}
```

## Credential Renewal Retry (iOS)

For unstable network conditions, configure automatic retry for credential renewal:

```typescript
<Auth0Provider
  domain={process.env.EXPO_PUBLIC_AUTH0_DOMAIN!}
  clientId={process.env.EXPO_PUBLIC_AUTH0_CLIENT_ID!}
  maxRetries={2}
>
  <App />
</Auth0Provider>
```

This retries on network errors, HTTP 429, and HTTP 5xx responses with exponential backoff. iOS only — the parameter is ignored on Android.

If using refresh token rotation, configure a token overlap period of at least **180 seconds** in your Auth0 tenant settings.

## Using Custom Headers

```typescript
<Auth0Provider
  domain={process.env.EXPO_PUBLIC_AUTH0_DOMAIN!}
  clientId={process.env.EXPO_PUBLIC_AUTH0_CLIENT_ID!}
  headers={{
    'Accept-Language': 'fr-CA',
    'X-App-Version': '1.0.0',
  }}
>
  <App />
</Auth0Provider>
```

---

# auth0-expo Setup Guide

## Table of Contents

- [Auth0 Configuration](#auth0-configuration) — Credential check, automatic vs manual setup
- [Expo Dev Client Requirement](#expo-dev-client-requirement) — Why Expo Go doesn't work, install steps
- [Post-Setup Steps](#post-setup-steps) — Verify plugin config, callback URLs, regenerate native projects
- [SDK Installation](#sdk-installation) — Version compatibility
- [Using HTTPS Callback URLs](#using-https-callback-urls-android-app-links) — Android App Links
- [Multiple Domain Support](#multiple-domain-support) — Staging/production configs
- [Secret Management](#secret-management) — What to store, what not to store, dynamic config
- [EAS Build Configuration](#eas-build-configuration) — Development and production builds
- [Verification](#verification) — Plugin, build, and auth flow checks

## Auth0 Configuration

> **Agent instruction:**
>
> **Important:** Never echo Auth0 credentials (domain, client ID) in your response text. Write them directly into config files using the Write or Edit tool.
>
> **Credential check (always first):**
> If the user's prompt already provides Auth0 credentials (domain, client ID), use them directly — do NOT call `AskUserQuestion` to re-confirm. Go straight to writing the app.json config and proceeding with integration.
>
> **If credentials are NOT provided**, use `AskUserQuestion` to ask:
> "How would you like to configure Auth0 for this Expo project?"
> - Option A: "Automatic setup (recommended)" — uses the Auth0 CLI to create your app
> - Option B: "Manual setup" — provide Auth0 credentials manually
>
> **If Automatic Setup (Option A):**
>
> 1. **Pre-flight checks:**
>    - Verify Auth0 CLI is installed: `command -v auth0`
>    - Verify logged in: `auth0 tenants list --csv --no-input`
>    - If any check fails, guide user to install/login, or fall back to manual setup
>
> 2. **Read the project's app.json** to extract:
>    - `expo.name` (for the Auth0 app name)
>    - `expo.scheme` (custom scheme — if missing, derive one from app name, lowercase, no special chars)
>    - `expo.ios.bundleIdentifier` (for iOS callback URL)
>    - `expo.android.package` (for Android callback URL)
>
> 3. **Create the Auth0 Native application:**
>    ```bash
>    auth0 apps create \
>      --name "APP_NAME-expo" \
>      --type native \
>      --auth-method none \
>      --callbacks "SCHEME://DOMAIN/ios/BUNDLE_ID/callback,SCHEME://DOMAIN/android/PACKAGE/callback" \
>      --logout-urls "SCHEME://DOMAIN/ios/BUNDLE_ID/callback,SCHEME://DOMAIN/android/PACKAGE/callback" \
>      --json --no-input
>    ```
>    Parse the JSON output to extract `client_id` and `domain`.
>
> 4. **Enable database connection** for the new client:
>    ```bash
>    auth0 api get "connections" --query "name=Username-Password-Authentication" --no-input
>    ```
>    Parse the response to extract the connection `id` as `CONNECTION_ID`. If it doesn't exist, create it and parse `id` from the create response as `CONNECTION_ID` instead:
>
>    ```bash
>    auth0 api post "connections" --data '{"strategy":"auth0","name":"Username-Password-Authentication"}' --no-input
>    ```
>
>    Then enable it for the client, using the `CONNECTION_ID` found above (existing) or just created:
>
>    ```bash
>    auth0 api patch "connections/CONNECTION_ID/clients" --data '[{"client_id":"NEW_CLIENT_ID","status":true}]' --no-input
>    ```
>
> 5. **Write the plugin config to app.json** using the Edit tool — add `react-native-auth0` to the plugins array with the domain and custom scheme. Do not echo credentials in your response.
>
> **If Manual Setup (Option B):**
>
> Ask the user for their Auth0 credentials:
> - Auth0 Domain (e.g., `your-tenant.auth0.com`)
> - Client ID (32-character alphanumeric string)
>
> Then write the configuration to app.json using the Edit tool and proceed with integration.

## Expo Dev Client Requirement

The `react-native-auth0` SDK uses native modules and **does not work with Expo Go**. A custom Expo development client is required.

> **Agent instruction:** Before proceeding with Auth0 SDK installation, check the project's `package.json` for `expo-dev-client` in `dependencies` or `devDependencies`. If not found, ask the user how they'd like to proceed (install automatically or set it up themselves). See SKILL.md step 1 for the full agent instruction.

### Check for expo-dev-client

```bash
# Check if expo-dev-client is in the project
cat package.json | grep expo-dev-client
```

### Install expo-dev-client (if missing)

```bash
npx expo install expo-dev-client
```

After installing, the development workflow changes from `npx expo start` (Expo Go) to:

```bash
npx expo run:ios
# or
npx expo run:android
```

For cloud builds, use EAS Build with a development profile:

```bash
eas build --profile development --platform ios
eas build --profile development --platform android
```

## Post-Setup Steps

After Auth0 is configured (via automatic or manual setup), complete these steps:

### 1. Verify app.json Plugin Configuration

Ensure `app.json` contains the react-native-auth0 plugin:

```json
{
  "expo": {
    "ios": {
      "bundleIdentifier": "com.yourcompany.yourapp"
    },
    "android": {
      "package": "com.yourcompany.yourapp"
    },
    "plugins": [
      [
        "react-native-auth0",
        {
          "domain": "your-tenant.auth0.com",
          "customScheme": "auth0sample"
        }
      ]
    ]
  }
}
```

The `customScheme` must be:
- All lowercase
- No special characters
- Unique to your application
- Passed to `authorize()` and `clearSession()` calls

### 2. Configure Callback URLs in Auth0 Dashboard

Go to [Auth0 Dashboard > Applications](https://manage.auth0.com/#/applications), select your application, and add the following:

**Allowed Callback URLs:**
```text
auth0sample://your-tenant.auth0.com/ios/com.yourcompany.yourapp/callback,
auth0sample://your-tenant.auth0.com/android/com.yourcompany.yourapp/callback
```

**Allowed Logout URLs:**
```text
auth0sample://your-tenant.auth0.com/ios/com.yourcompany.yourapp/callback,
auth0sample://your-tenant.auth0.com/android/com.yourcompany.yourapp/callback
```

Replace `auth0sample` with your `customScheme`, `your-tenant.auth0.com` with your domain, and `com.yourcompany.yourapp` with your bundle ID / package name.

All values must be **lowercase** with **no trailing slash**.

### 3. Regenerate Native Projects

After modifying app.json, regenerate the native projects:

```bash
npx expo prebuild --clean
```

This applies the Auth0 config plugin, which configures:
- **iOS**: URL scheme in Info.plist and AppDelegate linking handler
- **Android**: manifest placeholders for auth0Domain and auth0Scheme in build.gradle

## SDK Installation

```bash
npx expo install react-native-auth0
```

This installs the SDK with the correct version for your Expo SDK.

For older Expo versions:
- Expo 53+: Use react-native-auth0 v5.x
- Expo < 53: Use react-native-auth0 v4.x (`npx expo install react-native-auth0@4`)

## Using HTTPS Callback URLs (Android App Links)

For enhanced security, you can use HTTPS callback URLs with Android App Links:

```json
{
  "expo": {
    "plugins": [
      [
        "react-native-auth0",
        {
          "domain": "your-tenant.auth0.com",
          "customScheme": "https"
        }
      ]
    ]
  }
}
```

When using `customScheme: "https"`, the plugin automatically adds `android:autoVerify="true"` to the Android manifest intent-filter.

You must also configure Android App Links in the Auth0 Dashboard:
1. Go to **Applications > your app > Show Advanced Settings > Device Settings**
2. Add your Android Package Name and SHA256 fingerprint

## Multiple Domain Support

To support multiple Auth0 domains (e.g., for staging/production), pass an array to the plugin:

```json
{
  "expo": {
    "plugins": [
      [
        "react-native-auth0",
        [
          {
            "domain": "staging.auth0.com",
            "customScheme": "auth0staging"
          },
          {
            "domain": "production.auth0.com",
            "customScheme": "auth0prod"
          }
        ]
      ]
    ]
  }
}
```

## Secret Management

Expo / React Native mobile apps do **not** use a Client Secret. The Auth0 Native application type uses PKCE (Proof Key for Code Exchange) for secure authentication without exposing secrets.

**What to store in code / config:**
- Auth0 Domain — in `app.json` plugin config and `Auth0Provider` props
- Auth0 Client ID — in `Auth0Provider` props only (not in app.json)
- Custom Scheme — in `app.json` plugin config and `authorize`/`clearSession` options

**What NOT to store:**
- Never include Client Secret in mobile apps
- Never commit sensitive tokens to source control

**Never write a literal credential as a fallback in a `.ts`/`.tsx` file.** A default like
`process.env.EXPO_PUBLIC_AUTH0_CLIENT_ID ?? 'your-client-id'` puts the real value back into source —
it defeats the point of reading from the environment. Read the variable with no fallback and let it
fail loudly if unset:

```typescript
// Do this
clientId: process.env.EXPO_PUBLIC_AUTH0_CLIENT_ID!,

// Not this — the literal is now committed source
clientId: process.env.EXPO_PUBLIC_AUTH0_CLIENT_ID ?? 'your-client-id',
```

The `!` only silences the TypeScript error; it is erased at compile time and checks nothing at runtime.
If the variable is unset, `Auth0Provider` receives `undefined` and login fails, so confirm `.env` is
loaded — or throw explicitly at startup if you want to fail fast:

```typescript
const clientId = process.env.EXPO_PUBLIC_AUTH0_CLIENT_ID;
if (!clientId) throw new Error('EXPO_PUBLIC_AUTH0_CLIENT_ID is not set');
```

For environment-specific configuration, use `app.config.js` (dynamic config). The `domain` and
`customScheme` here are plugin values, not secrets, so literal defaults are acceptable in this one
place:

```javascript
export default ({ config }) => ({
  ...config,
  plugins: [
    [
      'react-native-auth0',
      {
        domain: process.env.AUTH0_DOMAIN || 'dev.auth0.com',
        customScheme: process.env.AUTH0_SCHEME || 'auth0dev',
      },
    ],
  ],
});
```

## EAS Build Configuration

For production builds with EAS:

```bash
npm install -g eas-cli
eas build --platform all
```

Create `eas.json` if it doesn't exist:

```json
{
  "cli": {
    "version": ">= 3.0.0"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal"
    },
    "preview": {
      "distribution": "internal"
    },
    "production": {}
  }
}
```

For development builds (used instead of Expo Go):

```bash
eas build --profile development --platform ios
eas build --profile development --platform android
```

## Verification

After setup, verify the integration:

1. **Plugin applied correctly:**
   ```bash
   npx expo prebuild --clean
   ```
   Check that `ios/{AppName}/Info.plist` contains the URL scheme and `android/app/build.gradle` contains `manifestPlaceholders`.

2. **Build succeeds:**
   ```bash
   npx expo run:ios
   # or
   npx expo run:android
   ```

3. **Auth flow works:**
   - Tap Login — browser opens with Auth0 Universal Login
   - Complete login — app receives credentials and shows user info
   - Tap Logout — session is cleared

SHA-256: 52cc0410500149eeff31ff349ab987d592e71ecaf69372a43b28b5a744251c3b