diff --git a/.version b/.version
index 339cbf23..ff9c6e14 100644
--- a/.version
+++ b/.version
@@ -1 +1 @@
-v5.10.0
+v5.11.0
diff --git a/A0Auth0.podspec b/A0Auth0.podspec
index 5c110043..88d86e12 100644
--- a/A0Auth0.podspec
+++ b/A0Auth0.podspec
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
s.source_files = 'ios/**/*.{h,m,mm,swift}'
s.requires_arc = true
- s.dependency 'Auth0', '2.23.0'
+ s.dependency 'Auth0', '2.24.1'
s.dependency 'SimpleKeychain', '1.3.0'
install_modules_dependencies(s)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4e053e8d..e0b208fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Change Log
+## [v5.11.0](https://github.com/auth0/react-native-auth0/tree/v5.11.0) (2026-07-31)
+
+[Full Changelog](https://github.com/auth0/react-native-auth0/compare/v5.10.0...v5.11.0)
+
+**Added**
+
+- feat: enforce IPSIE session_expiry with a SESSION_EXPIRED error [\#1597](https://github.com/auth0/react-native-auth0/pull/1597) ([subhankarmaiti](https://github.com/subhankarmaiti))
+- feat: add passkeys support for web [\#1604](https://github.com/auth0/react-native-auth0/pull/1604) ([NandanPrabhu](https://github.com/NandanPrabhu))
+- feat: add My Account API support on the web platform [\#1608](https://github.com/auth0/react-native-auth0/pull/1608) ([subhankarmaiti](https://github.com/subhankarmaiti))
+- feat: add actor token support for Custom Token Exchange impersonation & delegation [\#1595](https://github.com/auth0/react-native-auth0/pull/1595) ([subhankarmaiti](https://github.com/subhankarmaiti))
+- feat: add INVALID_CALLBACK_URL web auth error code [\#1603](https://github.com/auth0/react-native-auth0/pull/1603) ([subhankarmaiti](https://github.com/subhankarmaiti))
+
## [v5.10.0](https://github.com/auth0/react-native-auth0/tree/v5.10.0) (2026-07-17)
[Full Changelog](https://github.com/auth0/react-native-auth0/compare/v5.9.0...v5.10.0)
diff --git a/CLAUDE.md b/CLAUDE.md
index 6d879498..d0e40c3f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -27,7 +27,7 @@ Apply these on every task in this repo — they keep changes correct, small, and
- **Tech Stack:** React Native (New Architecture / TurboModules), iOS (Swift + ObjC++), Android (Kotlin), web (auth0-spa-js)
- **Package Manager:** Yarn (Berry, `.yarnrc.yml`); Node pinned via `.nvmrc` (v22.15.0)
- **Minimum Platform Version:** React Native ≥ 0.78.0, React ≥ 19.0.0 (peer deps); iOS/Android minimums come from the native SDKs
-- **Dependencies:** `@auth0/auth0-spa-js` 2.19.3, `jwt-decode` 4, `base-64`, `url` · test: Jest 29 + `fetch-mock`, `@testing-library/react`. See `package.json` for the authoritative list.
+- **Dependencies:** `@auth0/auth0-spa-js` 2.24.0, `jwt-decode` 4, `base-64`, `url` · test: Jest 29 + `fetch-mock`, `@testing-library/react`. See `package.json` for the authoritative list.
---
diff --git a/EXAMPLES-WEB.md b/EXAMPLES-WEB.md
index c6fded61..92308a3b 100644
--- a/EXAMPLES-WEB.md
+++ b/EXAMPLES-WEB.md
@@ -201,7 +201,7 @@ function MfaScreen({ mfaToken }: { mfaToken: string }) {
const verifyOtp = async () => {
try {
const credentials = await mfa.verify({ mfaToken, otp });
- console.log('Authenticated!', credentials.accessToken);
+ console.log('Authenticated!');
} catch (error) {
if (error instanceof MfaError) {
switch (error.type) {
@@ -277,6 +277,63 @@ const credentials = await auth0.mfa.verify({
});
```
+## 4. Passkeys (Web)
+
+Passkeys are supported on web via `@auth0/auth0-spa-js`. The flow is the same three steps as native (challenge → WebAuthn ceremony → exchange), but step 2 uses the browser's built-in [WebAuthn API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) (`navigator.credentials.create()`/`.get()`) instead of a native module — the app calls it directly, the SDK does not perform this step for you. `getTokenByPasskey`'s `authResponse` accepts the raw `PublicKeyCredential` returned by `navigator.credentials` directly on web — no manual serialization needed (unlike native, which takes a JSON string; see [Signup with Passkey (Web)](./EXAMPLES.md#signup-with-passkey-web) in `EXAMPLES.md` for the full signup example).
+
+**Secure context requirement:** `navigator.credentials.create()`/`.get()` only work over HTTPS (or `localhost` for local development). Apps served over plain HTTP will fail at the WebAuthn ceremony with a `SecurityError`.
+
+Because `navigator.credentials.create()`/`.get()` require a user gesture, call `passkeySignupChallenge` / `passkeyLoginChallenge` from within a click handler (not, for example, from a `useEffect`).
+
+```tsx
+import { useAuth0, PasskeyError } from 'react-native-auth0';
+
+function PasskeyLoginButton() {
+ const { passkeyLoginChallenge, getTokenByPasskey } = useAuth0();
+
+ const handleLogin = async () => {
+ try {
+ const challenge = await passkeyLoginChallenge({
+ realm: 'Username-Password-Authentication',
+ });
+
+ // App calls navigator.credentials directly (not wrapped by SDK)
+ const credential = (await navigator.credentials.get({
+ publicKey:
+ challenge.authParamsPublicKey as PublicKeyCredentialRequestOptions,
+ })) as PublicKeyCredential | null;
+
+ if (!credential) {
+ // User cancelled or no credential available
+ throw new Error('No passkey credential returned');
+ }
+
+ 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 login failed:', error.type, error.message);
+
+ // Handle MFA required scenario
+ const mfaPayload = error.getMfaRequiredPayload();
+ if (mfaPayload) {
+ console.log('MFA required. Token:', mfaPayload.mfaToken);
+ console.log('Available factors:', mfaPayload.mfaRequirements);
+ // Continue with mfa.challenge() and mfa.verify()
+ }
+ }
+ }
+ };
+
+ return ;
+}
+```
+
## Web Platform Notes
The web platform supports direct authentication grants including `auth.passwordRealm()`, `auth.createUser()`, `auth.resetPassword()`, and the MFA Flexible Factors Grant. These methods make direct HTTP calls to the Auth0 API.
diff --git a/EXAMPLES.md b/EXAMPLES.md
index 795a015c..243facb0 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -21,6 +21,7 @@
- [Using Retry with Auth0 Class](#using-retry-with-auth0-class)
- [Platform Support](#platform-support)
- [Error Handling](#error-handling)
+- [IPSIE Session Expiry](#ipsie-session-expiry)
- [Biometric Authentication](#biometric-authentication)
- [Biometric Policy Types](#biometric-policy-types)
- [Using with Auth0Provider (Hooks)](#using-with-auth0provider-hooks)
@@ -57,7 +58,9 @@
- [Prerequisites](#prerequisites-1)
- [Signup with Passkey](#signup-with-passkey)
- [Signin with Passkey](#signin-with-passkey)
- - [Auth Response Format](#auth-response-format)
+ - [Signup with Passkey (Web)](#signup-with-passkey-web)
+ - [Signin with Passkey (Web)](#signin-with-passkey-web)
+ - [Auth Response Format](#passkeys-auth-response-format)
- [Using Passkeys with Auth0 Class](#using-passkeys-with-auth0-class)
- [Signup Challenge Parameters](#signup-challenge-parameters)
- [Error Handling](#error-handling-1)
@@ -522,7 +525,7 @@ function MyComponent() {
// The retry mechanism is automatically applied to all credential renewal attempts
const credentials = await getCredentials();
- console.log('Access Token:', credentials.accessToken);
+ console.log('Authenticated successfully');
// Use credentials for API calls...
} catch (error) {
console.error('Failed to get credentials after retries:', error);
@@ -559,7 +562,7 @@ const auth0 = new Auth0({
try {
const credentials = await auth0.credentialsManager.getCredentials();
- console.log('Access Token:', credentials.accessToken);
+ console.log('Credentials retrieved successfully');
} catch (error) {
console.error('Credential renewal failed after retries:', error);
}
@@ -638,6 +641,66 @@ function MyComponent() {
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](https://openid.github.io/ipsie-openid-sl1/draft-openid-ipsie-sl1-profile.html) `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:
+
+```jsx
+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:
+
+```jsx
+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](#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)
@@ -848,7 +911,7 @@ function MyComponent() {
'https://first-api.example.com',
'read:data write:data'
);
- console.log('First API Access Token:', credentials.accessToken);
+ console.log('First API authenticated successfully');
console.log('Expires At:', new Date(credentials.expiresAt * 1000));
} catch (error) {
console.error('Error:', error);
@@ -862,7 +925,7 @@ function MyComponent() {
'https://second-api.example.com',
'read:reports'
);
- console.log('Second API Access Token:', credentials.accessToken);
+ console.log('Second API authenticated successfully');
} catch (error) {
console.error('Error:', error);
}
@@ -1231,15 +1294,15 @@ For detailed examples of validating different token types in Actions, see:
### Overview
-Passkeys provide a passwordless authentication experience using platform biometrics (Face ID, Touch ID, fingerprint) backed by public-key cryptography. The SDK provides the Auth0 challenge and token exchange steps, while you handle the platform credential manager interaction using native modules or libraries like `react-native-passkey`.
+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. **Credential Manager** — Present the OS credential manager UI to create or assert a passkey (using your own native module or a library)
+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:** Native only (iOS 16.6+ / Android). Not supported on Web.
+> **Platform Support:** iOS 16.6+, Android, and Web (modern browsers with WebAuthn support).
@@ -1251,6 +1314,7 @@ Before using passkeys:
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.
@@ -1289,7 +1353,7 @@ function PasskeySignupScreen() {
scope: 'openid profile email offline_access',
});
- console.log('Signed up with passkey:', credentials.accessToken);
+ console.log('Signed up with passkey');
} catch (error) {
if (error instanceof PasskeyError) {
console.error('Passkey signup failed:', error.type, error.message);
@@ -1334,7 +1398,7 @@ function PasskeySigninScreen() {
scope: 'openid profile email offline_access',
});
- console.log('Signed in with passkey:', credentials.accessToken);
+ console.log('Signed in with passkey');
} catch (error) {
if (error instanceof PasskeyError) {
console.error('Passkey signin failed:', error.type, error.message);
@@ -1346,9 +1410,111 @@ function PasskeySigninScreen() {
}
```
+
+
+### 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](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_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](#passkeys-auth-response-format) for the native, JSON-string form.)
+
+```tsx
+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 ;
+}
+```
+
+
+
+### 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.
+
+```tsx
+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 ;
+}
+```
+
+
+
### Auth Response Format
-The `authResponse` parameter passed to `getTokenByPasskey` must be a JSON string representing the [PublicKeyCredential](https://www.w3.org/TR/webauthn-2/#publickeycredential) response from the platform credential manager.
+On **iOS and Android**, the `authResponse` parameter passed to `getTokenByPasskey` must be a JSON string representing the [PublicKeyCredential](https://www.w3.org/TR/webauthn-2/#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):**
@@ -1431,7 +1597,7 @@ const loginCredentials = await auth0.getTokenByPasskey({
### Signup Challenge Parameters
-The `passkeySignupChallenge` method accepts the following parameters to create a user profile along with the passkey:
+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.
| Parameter | Type | Description |
| -------------- | ------------------------- | ------------------------------------ |
@@ -1453,26 +1619,40 @@ The `passkeySignupChallenge` method accepts the following parameters to create a
Passkey operations throw `PasskeyError` (extends `AuthError`) with a normalized `type` property. Use `PasskeyErrorCodes` for type-safe error handling:
-| Error Code | Description |
-| ------------------------------ | --------------------------------------------------- |
-| `PASSKEY_CHALLENGE_FAILED` | Auth0 challenge request failed |
-| `PASSKEY_EXCHANGE_FAILED` | Token exchange with credential response failed |
-| `PASSKEY_NOT_AVAILABLE` | Passkeys not available on this device or OS version |
-| `PASSKEY_UNSUPPORTED_PLATFORM` | Passkeys not supported on this platform (Web) |
-| `PASSKEY_UNKNOWN_ERROR` | Unknown or uncategorized passkey error |
+| Error Code | Description |
+| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `PASSKEY_CHALLENGE_FAILED` | Auth0 challenge request failed |
+| `PASSKEY_EXCHANGE_FAILED` | Token exchange with credential response failed |
+| `PASSKEY_NOT_AVAILABLE` | Passkeys not available on this device/OS version, or WebAuthn is not supported in this browser |
+| `PASSKEY_UNSUPPORTED_PLATFORM` | Passkeys not supported on this platform |
+| `PASSKEY_INVALID_PARAMETER` | **Native only.** `authResponse` passed to `getTokenByPasskey` was not a JSON string |
+| `PASSKEY_INVALID_CREDENTIAL` | **Web only.** The credential passed to `getTokenByPasskey` is neither a valid attestation (signup) nor assertion (login) response |
+| `PASSKEY_MFA_REQUIRED` | **Web 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_ERROR` | Unknown or uncategorized passkey error — check `error.message` for the underlying description |
```typescript
import { PasskeyError, PasskeyErrorCodes } from 'react-native-auth0';
try {
- const challenge = await auth0.passkeyLoginChallenge({
- realm: 'Username-Password-Authentication',
+ 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 native error code
+ 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()
+ }
+ }
}
}
```
@@ -1481,13 +1661,13 @@ try {
### Platform Support
-| Platform | Support | Requirements |
-| ----------- | ---------------- | --------------------------------------------------------- |
-| **iOS** | ✅ Supported | iOS 16.6+, Associated Domains with `webcredentials` |
-| **Android** | ✅ Supported | Android API 28+, Digital Asset Links configured |
-| **Web** | ❌ Not Supported | Throws `PasskeyError` with `PASSKEY_UNSUPPORTED_PLATFORM` |
+| Platform | Support | Requirements |
+| ----------- | ------------ | -------------------------------------------------------------- |
+| **iOS** | ✅ Supported | iOS 16.6+, Associated Domains with `webcredentials` |
+| **Android** | ✅ Supported | Android API 28+, Digital Asset Links configured |
+| **Web** | ✅ Supported | Modern browser with WebAuthn support; call from a user gesture |
-> **Note:** Passkeys require a real device for the full flow. Simulators/emulators may have limited support.
+> **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)](#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
@@ -2031,7 +2211,7 @@ function MfaScreen({ mfaToken }: { mfaToken: string }) {
const verifyOtp = async () => {
try {
const credentials = await mfa.verify({ mfaToken, otp });
- console.log('Authentication complete!', credentials.accessToken);
+ console.log('Authentication complete!');
// User is now logged in - state is automatically updated
} catch (error) {
if (error instanceof MfaError) {
diff --git a/README.md b/README.md
index 4643db9f..2328358c 100644
--- a/README.md
+++ b/README.md
@@ -730,6 +730,11 @@ try {
'DPoP credential state error. Clear credentials and re-authenticate.'
);
break;
+ case CredentialsManagerErrorCodes.SESSION_EXPIRED:
+ console.log(
+ 'Upstream IdP session ceiling reached. Clear credentials and restart the login flow.'
+ );
+ break;
default:
console.error('Credentials error:', error.message);
}
@@ -754,6 +759,9 @@ try {
| `DPOP_KEY_MISSING` | `DPOP_KEY_MISSING` | `dpopKeyMissing` | |
| `DPOP_NOT_CONFIGURED` | `DPOP_NOT_CONFIGURED` | `dpopNotConfigured` | |
| `DPOP_KEY_MISMATCH` | `DPOP_KEY_MISMATCH` | `dpopKeyMismatch` | |
+| `SESSION_EXPIRED` | `SESSION_EXPIRED` | `sessionExpired` | `session_expired` |
+
+> **`SESSION_EXPIRED`** is raised when the upstream IdP session ceiling (IPSIE `session_expiry` claim) has been reached. The local session is no longer valid and the user must re-authenticate. See [IPSIE Session Expiry](https://github.com/auth0/react-native-auth0/blob/master/EXAMPLES.md#ipsie-session-expiry) for details.
### MFA errors
@@ -916,9 +924,9 @@ This library provides a unified API across Native (iOS/Android) and Web platform
| `auth.passwordless...()` | ✅ | ❌ | **Not supported on Web.** Passwordless flows on the web should be configured via Universal Login and initiated with `webAuth.authorize()`. |
| `auth.loginWith...()` (OTP/SMS etc) | ✅ | ❌ | **Not supported on Web.** These direct grant flows are not secure for public clients like browsers. |
| **Passkeys** | | | --- |
-| `passkeySignupChallenge()` | ✅ | ❌ | **Native-only.** Gets a WebAuthn registration challenge from Auth0. Requires iOS 16.6+ or Android API 28+. |
-| `passkeyLoginChallenge()` | ✅ | ❌ | **Native-only.** Gets a WebAuthn assertion challenge from Auth0. Requires iOS 16.6+ or Android API 28+. |
-| `getTokenByPasskey()` | ✅ | ❌ | **Native-only.** Exchanges a passkey credential response for Auth0 tokens. Requires iOS 16.6+ or Android API 28+. |
+| `passkeySignupChallenge()` | ✅ | ✅ | Gets a WebAuthn registration challenge from Auth0. Requires iOS 16.6+, Android API 28+, or a browser with WebAuthn support. |
+| `passkeyLoginChallenge()` | ✅ | ✅ | Gets a WebAuthn assertion challenge from Auth0. Requires iOS 16.6+, Android API 28+, or a browser with WebAuthn support. |
+| `getTokenByPasskey()` | ✅ | ✅ | Exchanges a passkey credential response for Auth0 tokens. On Web, uses `@auth0/auth0-spa-js`. |
| **Token & User Management** | | | --- |
| `auth.refreshToken()` | ✅ | ❌ | **Not supported on Web.** Token refresh is handled automatically by `getCredentials()` via `getTokenSilently()` on the web. |
| `auth.userInfo()` | ✅ | ✅ | Fetches the user's profile from the `/userinfo` endpoint using an access token. |
diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.kt b/android/src/main/java/com/auth0/react/A0Auth0Module.kt
index e034a269..ff5080ad 100644
--- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt
+++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt
@@ -105,7 +105,8 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
CredentialsManagerException.NO_NETWORK to "NO_NETWORK",
CredentialsManagerException.DPOP_KEY_MISSING to "DPOP_KEY_MISSING",
CredentialsManagerException.DPOP_NOT_CONFIGURED to "DPOP_NOT_CONFIGURED",
- CredentialsManagerException.DPOP_KEY_MISMATCH to "DPOP_KEY_MISMATCH"
+ CredentialsManagerException.DPOP_KEY_MISMATCH to "DPOP_KEY_MISMATCH",
+ CredentialsManagerException.SESSION_EXPIRED to "SESSION_EXPIRED"
)
// DPoP enabled by default
private var useDPoP: Boolean = true
diff --git a/android/src/main/java/com/auth0/react/CredentialsParser.kt b/android/src/main/java/com/auth0/react/CredentialsParser.kt
index ab2af622..1fa043a8 100644
--- a/android/src/main/java/com/auth0/react/CredentialsParser.kt
+++ b/android/src/main/java/com/auth0/react/CredentialsParser.kt
@@ -10,6 +10,7 @@ object CredentialsParser {
private const val ACCESS_TOKEN_KEY = "accessToken"
private const val ID_TOKEN_KEY = "idToken"
private const val EXPIRES_AT_KEY = "expiresAt"
+ private const val SESSION_EXPIRES_AT_KEY = "sessionExpiresAt"
private const val SCOPE = "scope"
private const val REFRESH_TOKEN_KEY = "refreshToken"
private const val TOKEN_TYPE_KEY = "tokenType"
@@ -23,6 +24,9 @@ object CredentialsParser {
map.putString(SCOPE, credentials.scope)
map.putString(REFRESH_TOKEN_KEY, credentials.refreshToken)
map.putString(TOKEN_TYPE_KEY, credentials.type)
+ credentials.sessionExpiresAt?.let {
+ map.putDouble(SESSION_EXPIRES_AT_KEY, it.toDouble())
+ }
return map
}
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index 2498dc8b..05340273 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -1,6 +1,6 @@
PODS:
- A0Auth0 (5.10.0):
- - Auth0 (= 2.23.0)
+ - Auth0 (= 2.24.1)
- hermes-engine
- RCTRequired
- RCTTypeSafety
@@ -23,7 +23,7 @@ PODS:
- ReactNativeDependencies
- SimpleKeychain (= 1.3.0)
- Yoga
- - Auth0 (2.23.0):
+ - Auth0 (2.24.1):
- JWTDecode (= 3.3.0)
- SimpleKeychain (= 1.3.0)
- FBLazyVector (0.86.0)
@@ -2242,8 +2242,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
- A0Auth0: 869305cb94ca60b68cfb66d2bdd9b843f75ddeda
- Auth0: dd46c309079f995e91dfd7af38e8fefd1a43bf4e
+ A0Auth0: 88b0133241904699bfb3234731983d30dbf9cba0
+ Auth0: 700271885e7d3bab08f372f7e119adb7b2ba662b
FBLazyVector: b3e7ad108f0d882e30445c5527d774e3fd432f3d
hermes-engine: 4ba8c4848d46c6a441e09d1c62f883ba69bc5b76
JWTDecode: 1ca6f765844457d0dd8690436860fecee788f631
diff --git a/example/src/App.web.tsx b/example/src/App.web.tsx
index b53c1a94..337f50e0 100644
--- a/example/src/App.web.tsx
+++ b/example/src/App.web.tsx
@@ -71,12 +71,17 @@ const HooksAuthContent = (): React.JSX.Element => {
mfa,
users,
myAccount,
+ passkeySignupChallenge,
+ passkeyLoginChallenge,
+ getTokenByPasskey,
} = useAuth0();
const [result, setResult] = useState