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 c4a709d0..9657d679 100644
--- a/EXAMPLES-WEB.md
+++ b/EXAMPLES-WEB.md
@@ -277,8 +277,51 @@ 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 → credential manager → 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. See [Signup with Passkey (Web)](./EXAMPLES.md#signup-with-passkey-web) in `EXAMPLES.md` for a full example, including how to serialize the `PublicKeyCredential` returned by the browser into the JSON format `getTokenByPasskey` expects.
+
+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',
+ });
+
+ const credential = (await navigator.credentials.get({
+ publicKey: challenge.authParamsPublicKey as PublicKeyCredentialRequestOptions,
+ })) as PublicKeyCredential;
+
+ // See EXAMPLES.md for the serializeCredential() helper.
+ const credentials = await getTokenByPasskey({
+ authSession: challenge.authSession,
+ authResponse: serializeCredential(credential),
+ realm: 'Username-Password-Authentication',
+ });
+
+ console.log('Signed in with passkey:', credentials.accessToken);
+ } catch (error) {
+ if (error instanceof PasskeyError) {
+ console.error('Passkey login failed:', error.type, error.message);
+ }
+ }
+ };
+
+ 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.
Token refresh is handled automatically by `credentialsManager.getCredentials()` on the web. The `auth.refreshToken()` method is not available.
+
+Passkeys are supported on web, but the My Account API's passkey enrollment (`myAccount.enrollPasskey()`) is native-only.
diff --git a/EXAMPLES.md b/EXAMPLES.md
index feb0aefc..a6cb2b12 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -56,7 +56,8 @@
- [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)
+ - [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)
@@ -1191,15 +1192,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. On native platforms, 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`. On web, the SDK drives the full flow — challenge, browser WebAuthn ceremony (`navigator.credentials`), and token exchange — internally via `@auth0/auth0-spa-js`, so no extra library is needed.
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. **Credential Manager** — Present the OS (native) or browser (web) credential manager UI to create or assert a passkey. On native, use your own native module or a library; on web, the SDK calls `navigator.credentials` for you as part of `getTokenByPasskey`.
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).
@@ -1211,6 +1212,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.
@@ -1306,9 +1308,95 @@ function PasskeySigninScreen() {
}
```
+
+
+### Signup with Passkey (Web)
+
+On web, step 2 (the credential manager) uses 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. Serialize the resulting `PublicKeyCredential` to the JSON shape described in [Auth Response Format](#passkeys-auth-response-format) before passing it to `getTokenByPasskey`.
+
+```tsx
+import { useAuth0, PasskeyError } from 'react-native-auth0';
+
+// Converts a PublicKeyCredential from navigator.credentials into the
+// JSON shape getTokenByPasskey expects (see "Auth Response Format" below).
+function serializeCredential(credential: PublicKeyCredential) {
+ const toBase64Url = (buffer: ArrayBuffer) =>
+ btoa(String.fromCharCode(...new Uint8Array(buffer)))
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=+$/, '');
+
+ const response = credential.response;
+ const isAttestation = 'attestationObject' in response;
+
+ return JSON.stringify({
+ id: credential.id,
+ rawId: toBase64Url(credential.rawId),
+ type: credential.type,
+ authenticatorAttachment: credential.authenticatorAttachment ?? undefined,
+ response: isAttestation
+ ? {
+ clientDataJSON: toBase64Url(response.clientDataJSON),
+ attestationObject: toBase64Url(
+ (response as AuthenticatorAttestationResponse).attestationObject
+ ),
+ }
+ : {
+ clientDataJSON: toBase64Url(response.clientDataJSON),
+ authenticatorData: toBase64Url(
+ (response as AuthenticatorAssertionResponse).authenticatorData
+ ),
+ signature: toBase64Url(
+ (response as AuthenticatorAssertionResponse).signature
+ ),
+ userHandle: (response as AuthenticatorAssertionResponse).userHandle
+ ? toBase64Url(
+ (response as AuthenticatorAssertionResponse).userHandle!
+ )
+ : undefined,
+ },
+ });
+}
+
+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',
+ });
+
+ const credential = (await navigator.credentials.create({
+ publicKey: challenge.authParamsPublicKey as PublicKeyCredentialCreationOptions,
+ })) as PublicKeyCredential;
+
+ const credentials = await getTokenByPasskey({
+ authSession: challenge.authSession,
+ authResponse: serializeCredential(credential),
+ realm: 'Username-Password-Authentication',
+ });
+
+ console.log('Signed up with passkey:', credentials.accessToken);
+ } catch (error) {
+ if (error instanceof PasskeyError) {
+ console.error('Passkey signup 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.
+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 (native module/library on iOS/Android, `navigator.credentials` on web).
**For registration (signup):**
@@ -1441,13 +1529,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
diff --git a/README.md b/README.md
index b62b0670..0289242e 100644
--- a/README.md
+++ b/README.md
@@ -915,9 +915,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/example/src/App.web.tsx b/example/src/App.web.tsx
index 469adeba..f627ac57 100644
--- a/example/src/App.web.tsx
+++ b/example/src/App.web.tsx
@@ -17,6 +17,7 @@ import Auth0, {
MfaError,
MfaErrorCodes,
MfaFactorType,
+ PasskeyError,
} from 'react-native-auth0';
import type {
MfaAuthenticator,
@@ -40,6 +41,47 @@ type MfaStep =
type EnrollType = MfaFactorType;
+// Converts a PublicKeyCredential from navigator.credentials into the JSON
+// shape getTokenByPasskey expects (see EXAMPLES.md "Auth Response Format").
+const toBase64Url = (buffer: ArrayBuffer): string =>
+ btoa(String.fromCharCode(...new Uint8Array(buffer)))
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=+$/, '');
+
+const serializePasskeyCredential = (credential: PublicKeyCredential): string => {
+ const response = credential.response;
+ const isAttestation = 'attestationObject' in response;
+
+ return JSON.stringify({
+ id: credential.id,
+ rawId: toBase64Url(credential.rawId),
+ type: credential.type,
+ authenticatorAttachment: credential.authenticatorAttachment ?? undefined,
+ response: isAttestation
+ ? {
+ clientDataJSON: toBase64Url(response.clientDataJSON),
+ attestationObject: toBase64Url(
+ (response as AuthenticatorAttestationResponse).attestationObject
+ ),
+ }
+ : {
+ clientDataJSON: toBase64Url(response.clientDataJSON),
+ authenticatorData: toBase64Url(
+ (response as AuthenticatorAssertionResponse).authenticatorData
+ ),
+ signature: toBase64Url(
+ (response as AuthenticatorAssertionResponse).signature
+ ),
+ userHandle: (response as AuthenticatorAssertionResponse).userHandle
+ ? toBase64Url(
+ (response as AuthenticatorAssertionResponse).userHandle!
+ )
+ : undefined,
+ },
+ });
+};
+
// ========================================================================
// --- 1. HOOKS-BASED IMPLEMENTATION (Recommended) ---
// ========================================================================
@@ -57,12 +99,17 @@ const HooksAuthContent = (): React.JSX.Element => {
loginWithPasswordRealm,
mfa,
users,
+ passkeySignupChallenge,
+ passkeyLoginChallenge,
+ getTokenByPasskey,
} = useAuth0();
const [result, setResult] = useState