Skip to content

Commit 330fd97

Browse files
feat(passkeys): Refactor passkey authentication flow
1 parent 5969fdd commit 330fd97

20 files changed

Lines changed: 1818 additions & 382 deletions

File tree

EXAMPLES.md

Lines changed: 169 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
- [Prerequisites](#passkeys-prerequisites)
5555
- [Signup with Passkey](#signup-with-passkey)
5656
- [Signin with Passkey](#signin-with-passkey)
57+
- [Advanced: Manual Credential Manager Handling](#advanced-manual-credential-manager-handling)
5758
- [Using Passkeys with Auth0 Class](#using-passkeys-with-auth0-class)
5859
- [Error Handling](#passkeys-error-handling)
5960
- [Platform Support](#passkeys-platform-support)
@@ -1025,7 +1026,13 @@ For detailed examples of validating different token types in Actions, see:
10251026

10261027
### Overview
10271028

1028-
Passkeys provide a passwordless authentication experience using platform biometrics (Face ID, Touch ID, fingerprint) backed by public-key cryptography. The SDK orchestrates the full passkey flow — requesting a challenge from Auth0, presenting the OS passkey UI, and completing authentication — in a single method call.
1029+
Passkeys provide a passwordless authentication experience using platform biometrics (Face ID, Touch ID, fingerprint) backed by public-key cryptography. The SDK handles the full passkey flow — challenge, platform credential manager interaction, and token exchange — so you can implement passkeys without writing any custom native code.
1030+
1031+
The passkey flow has three steps:
1032+
1033+
1. **Challenge** — Request a WebAuthn challenge from Auth0 (`passkeySignupChallenge` or `passkeyLoginChallenge`)
1034+
2. **Credential Manager** — Present the OS credential manager UI to create or assert a passkey (`passkeyRegistration` or `passkeyAssertion`)
1035+
3. **Exchange** — Send the credential response back to Auth0 to get tokens (`passkeyExchange`)
10291036

10301037
> **Platform Support:** Native only (iOS 16.6+ / Android). Not supported on Web.
10311038

@@ -1040,44 +1047,47 @@ Before using passkeys:
10401047
3. **iOS:** Requires iOS 16.6 or later. Add an Associated Domain with the `webcredentials` service pointing to your Auth0 custom domain
10411048
4. **Android:** Requires Android API 28+. Configure your app's Digital Asset Links for the Auth0 custom domain
10421049
1043-
> **Important:** `signupWithPasskey` creates a **new** user account with a passkey. It will fail if the email already exists in the database connection. Use `signinWithPasskey` for existing users who have already registered a passkey.
1050+
> **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.
10441051
10451052
### Signup with Passkey
10461053
1047-
Register a new passkey for a user and obtain Auth0 credentials:
1054+
The signup flow requests a registration challenge, presents the OS credential manager UI to create a new passkey, then exchanges the result for Auth0 tokens. The SDK handles the platform credential manager interaction for you.
10481055
10491056
```tsx
1050-
import { useAuth0, PasskeyError, PasskeyErrorCodes } from 'react-native-auth0';
1057+
import { useAuth0, PasskeyError } from 'react-native-auth0';
10511058
10521059
function PasskeySignupScreen() {
1053-
const { signupWithPasskey } = useAuth0();
1060+
const { passkeySignupChallenge, passkeyRegistration, passkeyExchange } =
1061+
useAuth0();
10541062
10551063
const handleSignup = async () => {
10561064
try {
1057-
const credentials = await signupWithPasskey({
1065+
// Step 1: Get the signup challenge from Auth0
1066+
const challenge = await passkeySignupChallenge({
10581067
email: 'user@example.com',
10591068
name: 'John Doe',
10601069
realm: 'Username-Password-Authentication',
1070+
});
1071+
1072+
// Step 2: Present the OS credential manager UI to create a passkey
1073+
// This invokes CredentialManager on Android / ASAuthorizationController on iOS
1074+
const credentialJson = await passkeyRegistration({
1075+
challengeJson: JSON.stringify(challenge.authParamsPublicKey),
1076+
});
1077+
1078+
// Step 3: Exchange the credential response for Auth0 tokens
1079+
const credentials = await passkeyExchange({
1080+
authSession: challenge.authSession,
1081+
authResponse: credentialJson,
1082+
realm: 'Username-Password-Authentication',
10611083
audience: 'https://api.example.com',
10621084
scope: 'openid profile email offline_access',
10631085
});
10641086

10651087
console.log('Signed up with passkey:', credentials.accessToken);
10661088
} catch (error) {
10671089
if (error instanceof PasskeyError) {
1068-
switch (error.type) {
1069-
case PasskeyErrorCodes.USER_CANCELLED:
1070-
console.log('User dismissed the passkey prompt');
1071-
break;
1072-
case PasskeyErrorCodes.NOT_AVAILABLE:
1073-
console.log('Passkeys not supported on this device');
1074-
break;
1075-
case PasskeyErrorCodes.CHALLENGE_FAILED:
1076-
console.log('Failed to get challenge from Auth0');
1077-
break;
1078-
default:
1079-
console.error('Passkey signup failed:', error.message);
1080-
}
1090+
console.error('Passkey signup failed:', error.type, error.message);
10811091
}
10821092
}
10831093
};
@@ -1088,17 +1098,32 @@ function PasskeySignupScreen() {
10881098
10891099
### Signin with Passkey
10901100
1091-
Authenticate with an existing passkey:
1101+
The login flow requests an assertion challenge, presents the OS credential manager UI to assert an existing passkey, then exchanges the result for Auth0 tokens.
10921102
10931103
```tsx
1094-
import { useAuth0, PasskeyError, PasskeyErrorCodes } from 'react-native-auth0';
1104+
import { useAuth0, PasskeyError } from 'react-native-auth0';
10951105

10961106
function PasskeySigninScreen() {
1097-
const { signinWithPasskey } = useAuth0();
1107+
const { passkeyLoginChallenge, passkeyAssertion, passkeyExchange } =
1108+
useAuth0();
10981109

10991110
const handleSignin = async () => {
11001111
try {
1101-
const credentials = await signinWithPasskey({
1112+
// Step 1: Get the login challenge from Auth0
1113+
const challenge = await passkeyLoginChallenge({
1114+
realm: 'Username-Password-Authentication',
1115+
});
1116+
1117+
// Step 2: Present the OS credential manager UI to assert an existing passkey
1118+
// This invokes CredentialManager on Android / ASAuthorizationController on iOS
1119+
const credentialJson = await passkeyAssertion({
1120+
challengeJson: JSON.stringify(challenge.authParamsPublicKey),
1121+
});
1122+
1123+
// Step 3: Exchange the credential response for Auth0 tokens
1124+
const credentials = await passkeyExchange({
1125+
authSession: challenge.authSession,
1126+
authResponse: credentialJson,
11021127
realm: 'Username-Password-Authentication',
11031128
audience: 'https://api.example.com',
11041129
scope: 'openid profile email offline_access',
@@ -1107,16 +1132,7 @@ function PasskeySigninScreen() {
11071132
console.log('Signed in with passkey:', credentials.accessToken);
11081133
} catch (error) {
11091134
if (error instanceof PasskeyError) {
1110-
switch (error.type) {
1111-
case PasskeyErrorCodes.USER_CANCELLED:
1112-
console.log('User dismissed the passkey prompt');
1113-
break;
1114-
case PasskeyErrorCodes.NOT_AVAILABLE:
1115-
console.log('Passkeys not supported on this device');
1116-
break;
1117-
default:
1118-
console.error('Passkey signin failed:', error.message);
1119-
}
1135+
console.error('Passkey signin failed:', error.type, error.message);
11201136
}
11211137
}
11221138
};
@@ -1125,53 +1141,157 @@ function PasskeySigninScreen() {
11251141
}
11261142
```
11271143
1144+
### Advanced: Manual Credential Manager Handling
1145+
1146+
If you need full control over the platform credential manager interaction (e.g., custom UI, conditional mediation, or hybrid security key support), you can skip `passkeyRegistration`/`passkeyAssertion` and handle it yourself. The challenge and exchange methods give you the raw WebAuthn data:
1147+
1148+
```tsx
1149+
// Step 1: Get challenge (same as above)
1150+
const challenge = await passkeySignupChallenge({
1151+
email: 'user@example.com',
1152+
realm: '...',
1153+
});
1154+
1155+
// Step 2: Use your own native module or library to interact with the credential manager
1156+
// challenge.authParamsPublicKey contains the raw WebAuthn PublicKeyCredentialCreationOptions
1157+
// You must serialize the resulting PublicKeyCredential as JSON
1158+
1159+
const authResponse = await yourCustomCredentialManagerCall(
1160+
challenge.authParamsPublicKey
1161+
);
1162+
1163+
// Step 3: Exchange (same as above)
1164+
const credentials = await passkeyExchange({
1165+
authSession: challenge.authSession,
1166+
authResponse: JSON.stringify(authResponse),
1167+
realm: '...',
1168+
});
1169+
```
1170+
1171+
### Auth Response Format
1172+
1173+
The `authResponse` parameter passed to `passkeyExchange` must be a JSON string representing the [PublicKeyCredential](https://www.w3.org/TR/webauthn-2/#publickeycredential) response from the platform credential manager.
1174+
1175+
**For registration (signup):**
1176+
1177+
```json
1178+
{
1179+
"id": "<base64url-encoded credential ID>",
1180+
"rawId": "<base64url-encoded credential ID>",
1181+
"type": "public-key",
1182+
"response": {
1183+
"clientDataJSON": "<base64url-encoded>",
1184+
"attestationObject": "<base64url-encoded>"
1185+
},
1186+
"authenticatorAttachment": "platform"
1187+
}
1188+
```
1189+
1190+
**For assertion (login):**
1191+
1192+
```json
1193+
{
1194+
"id": "<base64url-encoded credential ID>",
1195+
"rawId": "<base64url-encoded credential ID>",
1196+
"type": "public-key",
1197+
"response": {
1198+
"clientDataJSON": "<base64url-encoded>",
1199+
"authenticatorData": "<base64url-encoded>",
1200+
"signature": "<base64url-encoded>",
1201+
"userHandle": "<base64url-encoded>"
1202+
},
1203+
"authenticatorAttachment": "platform"
1204+
}
1205+
```
1206+
11281207
### Using Passkeys with Auth0 Class
11291208
11301209
```typescript
1131-
import Auth0, { PasskeyError, PasskeyErrorCodes } from 'react-native-auth0';
1210+
import Auth0, { PasskeyError } from 'react-native-auth0';
11321211

11331212
const auth0 = new Auth0({
11341213
domain: 'YOUR_AUTH0_DOMAIN',
11351214
clientId: 'YOUR_AUTH0_CLIENT_ID',
11361215
});
11371216

1138-
// Signup with passkey
1139-
const signupCredentials = await auth0.signupWithPasskey({
1217+
// Signup flow
1218+
const signupChallenge = await auth0.passkeySignupChallenge({
11401219
email: 'user@example.com',
11411220
name: 'John Doe',
11421221
realm: 'Username-Password-Authentication',
11431222
});
11441223

1145-
// Signin with passkey
1146-
const signinCredentials = await auth0.signinWithPasskey({
1224+
const registrationJson = await auth0.passkeyRegistration({
1225+
challengeJson: JSON.stringify(signupChallenge.authParamsPublicKey),
1226+
});
1227+
1228+
const signupCredentials = await auth0.passkeyExchange({
1229+
authSession: signupChallenge.authSession,
1230+
authResponse: registrationJson,
1231+
realm: 'Username-Password-Authentication',
1232+
});
1233+
1234+
// Login flow
1235+
const loginChallenge = await auth0.passkeyLoginChallenge({
1236+
realm: 'Username-Password-Authentication',
1237+
});
1238+
1239+
const assertionJson = await auth0.passkeyAssertion({
1240+
challengeJson: JSON.stringify(loginChallenge.authParamsPublicKey),
1241+
});
1242+
1243+
const loginCredentials = await auth0.passkeyExchange({
1244+
authSession: loginChallenge.authSession,
1245+
authResponse: assertionJson,
11471246
realm: 'Username-Password-Authentication',
11481247
});
11491248
```
11501249
1250+
### Signup Challenge Parameters
1251+
1252+
The `passkeySignupChallenge` method accepts the following parameters to create a user profile along with the passkey:
1253+
1254+
| Parameter | Type | Description |
1255+
| -------------- | ------------------------- | ------------------------------------ |
1256+
| `email` | `string?` | User's email address |
1257+
| `phoneNumber` | `string?` | User's phone number |
1258+
| `username` | `string?` | Username |
1259+
| `name` | `string?` | Full name |
1260+
| `givenName` | `string?` | First/given name |
1261+
| `familyName` | `string?` | Last/family name |
1262+
| `nickname` | `string?` | Nickname |
1263+
| `picture` | `string?` | Profile picture URL |
1264+
| `userMetadata` | `Record<string, string>?` | Custom user metadata key-value pairs |
1265+
| `realm` | `string?` | Database connection name |
1266+
| `organization` | `string?` | Auth0 organization ID |
1267+
11511268
<a name="passkeys-error-handling"></a>
11521269
11531270
### Error Handling
11541271
11551272
Passkey operations throw `PasskeyError` (extends `AuthError`) with a normalized `type` property. Use `PasskeyErrorCodes` for type-safe error handling:
11561273
1157-
| Error Code | Description |
1158-
| ------------------------------ | --------------------------------------------------- |
1159-
| `PASSKEY_SIGNUP_FAILED` | Passkey registration failed |
1160-
| `PASSKEY_SIGNIN_FAILED` | Passkey authentication failed |
1161-
| `PASSKEY_NOT_AVAILABLE` | Device or OS does not support passkeys (iOS < 16.6) |
1162-
| `PASSKEY_USER_CANCELLED` | User dismissed the OS passkey prompt |
1163-
| `PASSKEY_CHALLENGE_FAILED` | Auth0 challenge request failed |
1164-
| `PASSKEY_UNSUPPORTED_PLATFORM` | Passkeys not supported on this platform (Web) |
1165-
| `PASSKEY_UNKNOWN_ERROR` | Unknown or uncategorized passkey error |
1274+
| Error Code | Description |
1275+
| ------------------------------ | ---------------------------------------------------------------- |
1276+
| `PASSKEY_CHALLENGE_FAILED` | Auth0 challenge request failed |
1277+
| `PASSKEY_EXCHANGE_FAILED` | Token exchange with credential response failed |
1278+
| `PASSKEY_REGISTRATION_FAILED` | Platform credential manager failed to create a new passkey |
1279+
| `PASSKEY_ASSERTION_FAILED` | Platform credential manager failed to assert an existing passkey |
1280+
| `PASSKEY_USER_CANCELLED` | User cancelled the passkey OS prompt |
1281+
| `PASSKEY_NOT_AVAILABLE` | Passkeys not available on this device or OS version |
1282+
| `PASSKEY_UNSUPPORTED_PLATFORM` | Passkeys not supported on this platform (Web) |
1283+
| `PASSKEY_UNKNOWN_ERROR` | Unknown or uncategorized passkey error |
11661284
11671285
```typescript
11681286
import { PasskeyError, PasskeyErrorCodes } from 'react-native-auth0';
11691287

11701288
try {
1171-
await auth0.signinWithPasskey({ realm: 'Username-Password-Authentication' });
1289+
const challenge = await auth0.passkeyLoginChallenge({
1290+
realm: 'Username-Password-Authentication',
1291+
});
11721292
} catch (error) {
11731293
if (error instanceof PasskeyError) {
1174-
console.log('Error type:', error.type); // e.g. "PASSKEY_USER_CANCELLED"
1294+
console.log('Error type:', error.type); // e.g. "PASSKEY_CHALLENGE_FAILED"
11751295
console.log('Error message:', error.message);
11761296
console.log('Error code:', error.code); // Raw native error code
11771297
}

android/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ dependencies {
9797
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
9898
implementation "androidx.browser:browser:1.2.0"
9999
implementation 'com.auth0.android:auth0:3.17.0'
100+
implementation "androidx.credentials:credentials:1.5.0"
100101
}
101102

102103
if (isNewArchitectureEnabled()) {

0 commit comments

Comments
 (0)