diff --git a/A0Auth0.podspec b/A0Auth0.podspec
index d5b7ee8e..0d8248e3 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.19.0'
+ s.dependency 'Auth0', '2.21.1'
install_modules_dependencies(s)
end
diff --git a/EXAMPLES.md b/EXAMPLES.md
index 9fdac540..ae3e6ff6 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -49,6 +49,15 @@
- [Using Custom Token Exchange with Auth0 Class](#using-custom-token-exchange-with-auth0-class)
- [With Organization Context](#with-organization-context)
- [Subject Token Type Requirements](#subject-token-type-requirements)
+- [Passkeys](#passkeys)
+ - [Overview](#passkeys-overview)
+ - [Prerequisites](#passkeys-prerequisites)
+ - [Signup with Passkey](#signup-with-passkey)
+ - [Signin with Passkey](#signin-with-passkey)
+ - [Advanced: Manual Credential Manager Handling](#advanced-manual-credential-manager-handling)
+ - [Using Passkeys with Auth0 Class](#using-passkeys-with-auth0-class)
+ - [Error Handling](#passkeys-error-handling)
+ - [Platform Support](#passkeys-platform-support)
- [Native to Web SSO](#native-to-web-sso)
- [Overview](#native-to-web-sso-overview)
- [Prerequisites](#native-to-web-sso-prerequisites)
@@ -1011,6 +1020,270 @@ For detailed examples of validating different token types in Actions, see:
- Validate token expiration, issuer, and audience claims
- Implement rate limiting for failed validations using `api.access.rejectInvalidSubjectToken()`
+## Passkeys
+
+
+
+### Overview
+
+Passkeys provide a passwordless authentication experience using platform biometrics (Face ID, Touch ID, fingerprint) backed by public-key cryptography. The 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`.
+
+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)
+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.
+
+
+
+### Prerequisites
+
+Before using passkeys:
+
+1. **Enable the Passkey Grant Type** for your Auth0 application in the Auth0 Dashboard
+2. **Configure a custom domain** on your Auth0 tenant (required for passkeys)
+3. **iOS:** Requires iOS 16.6 or later. Add an Associated Domain with the `webcredentials` service pointing to your Auth0 custom domain
+4. **Android:** Requires Android API 28+. Configure your app's Digital Asset Links for the Auth0 custom domain
+
+> **Important:** `passkeySignupChallenge` is for creating **new** user accounts with a passkey. It will fail if the email already exists in the database connection. Use `passkeyLoginChallenge` for existing users who have already registered a passkey.
+
+### Signup with Passkey
+
+The signup flow requests a registration challenge from Auth0, then you use the platform credential manager (via a native module or library like `react-native-passkey`) to create a new passkey, and finally exchange the credential for Auth0 tokens.
+
+```tsx
+import { useAuth0, PasskeyError } from 'react-native-auth0';
+
+function PasskeySignupScreen() {
+ const { passkeySignupChallenge, getTokenByPasskey } = useAuth0();
+
+ const handleSignup = async () => {
+ try {
+ // Step 1: Get the signup challenge from Auth0
+ const challenge = await passkeySignupChallenge({
+ email: 'user@example.com',
+ name: 'John Doe',
+ realm: 'Username-Password-Authentication',
+ });
+
+ // Step 2: Use the platform credential manager to create a passkey
+ // challenge.authParamsPublicKey contains the WebAuthn PublicKeyCredentialCreationOptions
+ // Use your preferred library (e.g., react-native-passkey) or native module
+ const credentialJson = await yourCredentialManagerCreate(
+ challenge.authParamsPublicKey
+ );
+
+ // Step 3: Exchange the credential response for Auth0 tokens
+ const credentials = await getTokenByPasskey({
+ authSession: challenge.authSession,
+ authResponse: credentialJson,
+ realm: 'Username-Password-Authentication',
+ audience: 'https://api.example.com',
+ scope: 'openid profile email offline_access',
+ });
+
+ console.log('Signed up with passkey:', credentials.accessToken);
+ } catch (error) {
+ if (error instanceof PasskeyError) {
+ console.error('Passkey signup failed:', error.type, error.message);
+ }
+ }
+ };
+
+ return ;
+}
+```
+
+### Signin with Passkey
+
+The login flow requests an assertion challenge from Auth0, then you use the platform credential manager to assert an existing passkey, and finally exchange the credential for Auth0 tokens.
+
+```tsx
+import { useAuth0, PasskeyError } from 'react-native-auth0';
+
+function PasskeySigninScreen() {
+ const { passkeyLoginChallenge, getTokenByPasskey } = useAuth0();
+
+ const handleSignin = async () => {
+ try {
+ // Step 1: Get the login challenge from Auth0
+ const challenge = await passkeyLoginChallenge({
+ realm: 'Username-Password-Authentication',
+ });
+
+ // Step 2: Use the platform credential manager to assert an existing passkey
+ // challenge.authParamsPublicKey contains the WebAuthn PublicKeyCredentialRequestOptions
+ // Use your preferred library (e.g., react-native-passkey) or native module
+ const credentialJson = await yourCredentialManagerGet(
+ challenge.authParamsPublicKey
+ );
+
+ // Step 3: Exchange the credential response for Auth0 tokens
+ const credentials = await getTokenByPasskey({
+ authSession: challenge.authSession,
+ authResponse: credentialJson,
+ realm: 'Username-Password-Authentication',
+ audience: 'https://api.example.com',
+ scope: 'openid profile email offline_access',
+ });
+
+ console.log('Signed in with passkey:', credentials.accessToken);
+ } 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.
+
+**For registration (signup):**
+
+```json
+{
+ "id": "",
+ "rawId": "",
+ "type": "public-key",
+ "response": {
+ "clientDataJSON": "",
+ "attestationObject": ""
+ },
+ "authenticatorAttachment": "platform"
+}
+```
+
+**For assertion (login):**
+
+```json
+{
+ "id": "",
+ "rawId": "",
+ "type": "public-key",
+ "response": {
+ "clientDataJSON": "",
+ "authenticatorData": "",
+ "signature": "",
+ "userHandle": ""
+ },
+ "authenticatorAttachment": "platform"
+}
+```
+
+### Using Passkeys with Auth0 Class
+
+```typescript
+import Auth0, { PasskeyError } from 'react-native-auth0';
+
+const auth0 = new Auth0({
+ domain: 'YOUR_AUTH0_DOMAIN',
+ clientId: 'YOUR_AUTH0_CLIENT_ID',
+});
+
+// Signup flow
+const signupChallenge = await auth0.passkeySignupChallenge({
+ email: 'user@example.com',
+ name: 'John Doe',
+ realm: 'Username-Password-Authentication',
+});
+
+// Use your credential manager library to create the passkey
+// signupChallenge.authParamsPublicKey has the WebAuthn creation options
+const registrationJson = await yourCredentialManagerCreate(
+ signupChallenge.authParamsPublicKey
+);
+
+const signupCredentials = await auth0.getTokenByPasskey({
+ authSession: signupChallenge.authSession,
+ authResponse: registrationJson,
+ realm: 'Username-Password-Authentication',
+});
+
+// Login flow
+const loginChallenge = await auth0.passkeyLoginChallenge({
+ realm: 'Username-Password-Authentication',
+});
+
+// Use your credential manager library to assert the passkey
+// loginChallenge.authParamsPublicKey has the WebAuthn request options
+const assertionJson = await yourCredentialManagerGet(
+ loginChallenge.authParamsPublicKey
+);
+
+const loginCredentials = await auth0.getTokenByPasskey({
+ authSession: loginChallenge.authSession,
+ authResponse: assertionJson,
+ realm: 'Username-Password-Authentication',
+});
+```
+
+### Signup Challenge Parameters
+
+The `passkeySignupChallenge` method accepts the following parameters to create a user profile along with the passkey:
+
+| Parameter | Type | Description |
+| -------------- | ------------------------- | ------------------------------------ |
+| `email` | `string?` | User's email address |
+| `phoneNumber` | `string?` | User's phone number |
+| `username` | `string?` | Username |
+| `name` | `string?` | Full name |
+| `givenName` | `string?` | First/given name |
+| `familyName` | `string?` | Last/family name |
+| `nickname` | `string?` | Nickname |
+| `picture` | `string?` | Profile picture URL |
+| `userMetadata` | `Record?` | Custom user metadata key-value pairs |
+| `realm` | `string?` | Database connection name |
+| `organization` | `string?` | Auth0 organization ID |
+
+
+
+### Error Handling
+
+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 |
+
+```typescript
+import { PasskeyError, PasskeyErrorCodes } from 'react-native-auth0';
+
+try {
+ const challenge = await auth0.passkeyLoginChallenge({
+ realm: 'Username-Password-Authentication',
+ });
+} 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
+ }
+}
+```
+
+
+
+### 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` |
+
+> **Note:** Passkeys require a real device for the full flow. Simulators/emulators may have limited support.
+
## Native to Web SSO
### Native to Web SSO Overview
@@ -1395,6 +1668,7 @@ On Android, some browsers do not correctly handle App Link redirects. For exampl
You can restrict which browsers are allowed to handle the web authentication flow by passing `allowedBrowserPackages` in the options object. When set, only browsers whose package names appear in the list will be used.
**Behaviour:**
+
- If the user's default browser is in the list, it is used.
- If the user's default browser is not in the list but another allowed browser is installed, that browser is used instead.
- If no allowed browser is installed, an `a0.browser_not_available` error is returned.
@@ -1414,7 +1688,7 @@ await authorize(
allowedBrowserPackages: [
'com.android.chrome',
'com.chrome.beta',
- 'com.microsoft.emmx', // Edge
+ 'com.microsoft.emmx', // Edge
'com.brave.browser',
'com.sec.android.app.sbrowser', // Samsung Internet
],
@@ -1438,7 +1712,7 @@ await auth0.webAuth.authorize(
allowedBrowserPackages: [
'com.android.chrome',
'com.chrome.beta',
- 'com.microsoft.emmx', // Edge
+ 'com.microsoft.emmx', // Edge
'com.brave.browser',
'com.sec.android.app.sbrowser', // Samsung Internet
],
diff --git a/README.md b/README.md
index d618fe94..41b8348a 100644
--- a/README.md
+++ b/README.md
@@ -863,6 +863,10 @@ This library provides a unified API across Native (iOS/Android) and Web platform
| `auth.passwordRealm()` | ✅ | ❌ | **Not supported on Web for security reasons.** The Resource Owner Password Grant exposes credentials to the browser and is not recommended for Single Page Applications. |
| `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+. |
| **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/build.gradle b/android/build.gradle
index 1bc72a1c..f41f799b 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -96,7 +96,7 @@ dependencies {
implementation "com.facebook.react:react-android"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.browser:browser:1.2.0"
- implementation 'com.auth0.android:auth0:3.15.0'
+ implementation 'com.auth0.android:auth0:3.17.0'
}
if (isNewArchitectureEnabled()) {
diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.kt b/android/src/main/java/com/auth0/react/A0Auth0Module.kt
index 6a3dd6fb..ae97fae3 100644
--- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt
+++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt
@@ -4,7 +4,6 @@ import android.app.Activity
import android.content.Intent
import androidx.fragment.app.FragmentActivity
import com.auth0.android.Auth0
-import com.auth0.android.result.APICredentials
import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.authentication.storage.CredentialsManagerException
@@ -16,7 +15,12 @@ import com.auth0.android.dpop.DPoPException
import com.auth0.android.provider.BrowserPicker
import com.auth0.android.provider.CustomTabsOptions
import com.auth0.android.provider.WebAuthProvider
+import com.auth0.android.request.PublicKeyCredentials
+import com.auth0.android.request.UserData
+import com.auth0.android.result.APICredentials
import com.auth0.android.result.Credentials
+import com.auth0.android.result.PasskeyChallenge
+import com.auth0.android.result.PasskeyRegistrationChallenge
import com.facebook.react.bridge.ActivityEventListener
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
@@ -25,6 +29,7 @@ import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.bridge.WritableNativeMap
+import com.google.gson.Gson
import java.net.MalformedURLException
import java.net.URL
@@ -537,6 +542,141 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
})
}
+ @ReactMethod
+ override fun passkeySignupChallenge(
+ email: String?,
+ phoneNumber: String?,
+ username: String?,
+ name: String?,
+ givenName: String?,
+ familyName: String?,
+ nickname: String?,
+ picture: String?,
+ userMetadata: ReadableMap?,
+ realm: String?,
+ organization: String?,
+ promise: Promise
+ ) {
+ val authClient = AuthenticationAPIClient(auth0!!)
+ if (useDPoP) {
+ authClient.useDPoP(reactContext)
+ }
+
+ val finalEmail = email?.trim()?.ifEmpty { null }
+ val finalPhone = phoneNumber?.trim()?.ifEmpty { null }
+ val finalUsername = username?.trim()?.ifEmpty { null }
+ val finalName = name?.trim()?.ifEmpty { null }
+ val finalGivenName = givenName?.trim()?.ifEmpty { null }
+ val finalFamilyName = familyName?.trim()?.ifEmpty { null }
+ val finalNickname = nickname?.trim()?.ifEmpty { null }
+ val finalPicture = picture?.trim()?.ifEmpty { null }
+ val finalUserMetadata = userMetadata?.toHashMap()?.mapValues { it.value?.toString() ?: "" }?.ifEmpty { null }
+ val finalRealm = realm?.trim()?.ifEmpty { null }
+ val finalOrg = organization?.trim()?.ifEmpty { null }
+ val userData = UserData(
+ email = finalEmail,
+ phoneNumber = finalPhone,
+ userName = finalUsername,
+ name = finalName,
+ givenName = finalGivenName,
+ familyName = finalFamilyName,
+ nickName = finalNickname,
+ picture = finalPicture,
+ userMetadata = finalUserMetadata
+ )
+
+ authClient.signupWithPasskey(userData, finalRealm, finalOrg)
+ .start(object : com.auth0.android.callback.Callback {
+ override fun onSuccess(challenge: PasskeyRegistrationChallenge) {
+ val result = WritableNativeMap().apply {
+ putString("authSession", challenge.authSession)
+ val authParamsJson = Gson().toJson(challenge.authParamsPublicKey)
+ putMap("authParamsPublicKey", JsonUtils.jsonToWritableMap(authParamsJson))
+ }
+ promise.resolve(result)
+ }
+
+ override fun onFailure(error: AuthenticationException) {
+ promise.reject("PASSKEY_CHALLENGE_FAILED", error.getDescription(), error)
+ }
+ })
+ }
+
+ @ReactMethod
+ override fun passkeyLoginChallenge(
+ realm: String?,
+ organization: String?,
+ promise: Promise
+ ) {
+ val authClient = AuthenticationAPIClient(auth0!!)
+ if (useDPoP) {
+ authClient.useDPoP(reactContext)
+ }
+
+ val finalRealm = realm?.trim()?.ifEmpty { null }
+ val finalOrg = organization?.trim()?.ifEmpty { null }
+
+ authClient.passkeyChallenge(finalRealm, finalOrg)
+ .start(object : com.auth0.android.callback.Callback {
+ override fun onSuccess(challenge: PasskeyChallenge) {
+ val result = WritableNativeMap().apply {
+ putString("authSession", challenge.authSession)
+ val authParamsJson = Gson().toJson(challenge.authParamsPublicKey)
+ putMap("authParamsPublicKey", JsonUtils.jsonToWritableMap(authParamsJson))
+ }
+ promise.resolve(result)
+ }
+
+ override fun onFailure(error: AuthenticationException) {
+ promise.reject("PASSKEY_CHALLENGE_FAILED", error.getDescription(), error)
+ }
+ })
+ }
+
+ @ReactMethod
+ override fun getTokenByPasskey(
+ authSession: String,
+ authResponse: String,
+ realm: String?,
+ audience: String?,
+ scope: String?,
+ organization: String?,
+ promise: Promise
+ ) {
+ val authClient = AuthenticationAPIClient(auth0!!)
+ if (useDPoP) {
+ authClient.useDPoP(reactContext)
+ }
+
+ val finalScope = if (scope.isNullOrBlank()) "openid profile email" else scope
+ val finalRealm = realm?.trim()?.ifEmpty { null }
+ val finalAudience = audience?.trim()?.ifEmpty { null }
+ val finalOrg = organization?.trim()?.ifEmpty { null }
+
+ val publicKeyCredentials = try {
+ Gson().fromJson(authResponse, PublicKeyCredentials::class.java)
+ } catch (e: Exception) {
+ promise.reject("PASSKEY_EXCHANGE_FAILED", "Invalid authResponse JSON: ${e.message}", e)
+ return
+ }
+
+ val request = authClient.signinWithPasskey(authSession, publicKeyCredentials, finalRealm, finalOrg)
+ finalAudience?.let { request.setAudience(it) }
+ request.setScope(finalScope)
+ request.validateClaims()
+
+ request.start(object : com.auth0.android.callback.Callback {
+ override fun onSuccess(credentials: Credentials) {
+ promise.resolve(CredentialsParser.toMap(credentials))
+ }
+
+ override fun onFailure(error: AuthenticationException) {
+ promise.reject("PASSKEY_EXCHANGE_FAILED", error.getDescription(), error)
+ }
+ })
+ }
+
+
override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
// No-op
}
diff --git a/android/src/main/java/com/auth0/react/JsonUtils.kt b/android/src/main/java/com/auth0/react/JsonUtils.kt
new file mode 100644
index 00000000..e14fd9b2
--- /dev/null
+++ b/android/src/main/java/com/auth0/react/JsonUtils.kt
@@ -0,0 +1,55 @@
+package com.auth0.react
+
+import com.facebook.react.bridge.WritableArray
+import com.facebook.react.bridge.WritableMap
+import com.facebook.react.bridge.WritableNativeArray
+import com.facebook.react.bridge.WritableNativeMap
+import org.json.JSONArray
+import org.json.JSONObject
+
+object JsonUtils {
+ fun jsonToWritableMap(jsonString: String): WritableMap {
+ val jsonObject = JSONObject(jsonString)
+ return convertJsonObject(jsonObject)
+ }
+
+ private fun convertJsonObject(jsonObject: JSONObject): WritableMap {
+ val map = WritableNativeMap()
+ val keys = jsonObject.keys()
+ while (keys.hasNext()) {
+ val key = keys.next()
+ val value = jsonObject.get(key)
+ when (value) {
+ is JSONObject -> map.putMap(key, convertJsonObject(value))
+ is JSONArray -> map.putArray(key, convertJsonArray(value))
+ is String -> map.putString(key, value)
+ is Int -> map.putInt(key, value)
+ is Long -> map.putDouble(key, value.toDouble())
+ is Double -> map.putDouble(key, value)
+ is Boolean -> map.putBoolean(key, value)
+ JSONObject.NULL -> map.putNull(key)
+ else -> map.putString(key, value.toString())
+ }
+ }
+ return map
+ }
+
+ private fun convertJsonArray(jsonArray: JSONArray): WritableArray {
+ val array = WritableNativeArray()
+ for (i in 0 until jsonArray.length()) {
+ val value = jsonArray.get(i)
+ when (value) {
+ is JSONObject -> array.pushMap(convertJsonObject(value))
+ is JSONArray -> array.pushArray(convertJsonArray(value))
+ is String -> array.pushString(value)
+ is Int -> array.pushInt(value)
+ is Long -> array.pushDouble(value.toDouble())
+ is Double -> array.pushDouble(value)
+ is Boolean -> array.pushBoolean(value)
+ JSONObject.NULL -> array.pushNull()
+ else -> array.pushString(value.toString())
+ }
+ }
+ return array
+ }
+}
diff --git a/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt b/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt
index 713705be..fc56c307 100644
--- a/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt
+++ b/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt
@@ -122,4 +122,42 @@ abstract class A0Auth0Spec(context: ReactApplicationContext) : ReactContextBaseJ
organization: String?,
promise: Promise
)
+
+ @ReactMethod
+ @DoNotStrip
+ abstract fun passkeySignupChallenge(
+ email: String?,
+ phoneNumber: String?,
+ username: String?,
+ name: String?,
+ givenName: String?,
+ familyName: String?,
+ nickname: String?,
+ picture: String?,
+ userMetadata: ReadableMap?,
+ realm: String?,
+ organization: String?,
+ promise: Promise
+ )
+
+ @ReactMethod
+ @DoNotStrip
+ abstract fun passkeyLoginChallenge(
+ realm: String?,
+ organization: String?,
+ promise: Promise
+ )
+
+ @ReactMethod
+ @DoNotStrip
+ abstract fun getTokenByPasskey(
+ authSession: String,
+ authResponse: String,
+ realm: String?,
+ audience: String?,
+ scope: String?,
+ organization: String?,
+ promise: Promise
+ )
+
}
\ No newline at end of file
diff --git a/example/android/app/src/main/java/com/auth0example/MainApplication.kt b/example/android/app/src/main/java/com/auth0example/MainApplication.kt
index de72e5ab..c4a4ebc1 100644
--- a/example/android/app/src/main/java/com/auth0example/MainApplication.kt
+++ b/example/android/app/src/main/java/com/auth0example/MainApplication.kt
@@ -14,8 +14,7 @@ class MainApplication : Application(), ReactApplication {
context = applicationContext,
packageList =
PackageList(this).packages.apply {
- // Packages that cannot be autolinked yet can be added manually here, for example:
- // add(MyReactNativePackage())
+ add(PasskeyPackage())
},
)
}
diff --git a/example/android/app/src/main/java/com/auth0example/PasskeyModule.kt b/example/android/app/src/main/java/com/auth0example/PasskeyModule.kt
new file mode 100644
index 00000000..83499a4c
--- /dev/null
+++ b/example/android/app/src/main/java/com/auth0example/PasskeyModule.kt
@@ -0,0 +1,85 @@
+package com.auth0example
+
+import android.app.Activity
+import android.os.Build
+import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.CreatePublicKeyCredentialResponse
+import androidx.credentials.CredentialManager
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetPublicKeyCredentialOption
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.exceptions.CreateCredentialCancellationException
+import androidx.credentials.exceptions.GetCredentialCancellationException
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReactContextBaseJavaModule
+import com.facebook.react.bridge.ReactMethod
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class PasskeyModule(reactContext: ReactApplicationContext) :
+ ReactContextBaseJavaModule(reactContext) {
+
+ override fun getName(): String = "PasskeyModule"
+
+ @ReactMethod
+ fun createPasskey(requestJson: String, promise: Promise) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ promise.reject("PASSKEY_NOT_AVAILABLE", "Passkeys require Android API 28 or later")
+ return
+ }
+
+ val activity: Activity = reactApplicationContext.currentActivity
+ ?: run {
+ promise.reject("PASSKEY_FAILED", "No activity available")
+ return
+ }
+
+ val credentialManager = CredentialManager.create(reactApplicationContext)
+ val createRequest = CreatePublicKeyCredentialRequest(requestJson = requestJson)
+
+ CoroutineScope(Dispatchers.Main).launch {
+ try {
+ val result = credentialManager.createCredential(activity, createRequest)
+ val response = result as CreatePublicKeyCredentialResponse
+ promise.resolve(response.registrationResponseJson)
+ } catch (e: CreateCredentialCancellationException) {
+ promise.reject("USER_CANCELLED", "User cancelled passkey creation", e)
+ } catch (e: Exception) {
+ promise.reject("PASSKEY_FAILED", e.message, e)
+ }
+ }
+ }
+
+ @ReactMethod
+ fun getPasskey(requestJson: String, promise: Promise) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ promise.reject("PASSKEY_NOT_AVAILABLE", "Passkeys require Android API 28 or later")
+ return
+ }
+
+ val activity: Activity = reactApplicationContext.currentActivity
+ ?: run {
+ promise.reject("PASSKEY_FAILED", "No activity available")
+ return
+ }
+
+ val credentialManager = CredentialManager.create(reactApplicationContext)
+ val getRequest = GetCredentialRequest(
+ listOf(GetPublicKeyCredentialOption(requestJson = requestJson))
+ )
+
+ CoroutineScope(Dispatchers.Main).launch {
+ try {
+ val result = credentialManager.getCredential(activity, getRequest)
+ val credential = result.credential as PublicKeyCredential
+ promise.resolve(credential.authenticationResponseJson)
+ } catch (e: GetCredentialCancellationException) {
+ promise.reject("USER_CANCELLED", "User cancelled passkey assertion", e)
+ } catch (e: Exception) {
+ promise.reject("PASSKEY_FAILED", e.message, e)
+ }
+ }
+ }
+}
diff --git a/example/android/app/src/main/java/com/auth0example/PasskeyPackage.kt b/example/android/app/src/main/java/com/auth0example/PasskeyPackage.kt
new file mode 100644
index 00000000..53d16774
--- /dev/null
+++ b/example/android/app/src/main/java/com/auth0example/PasskeyPackage.kt
@@ -0,0 +1,16 @@
+package com.auth0example
+
+import com.facebook.react.ReactPackage
+import com.facebook.react.bridge.NativeModule
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.uimanager.ViewManager
+
+class PasskeyPackage : ReactPackage {
+ override fun createNativeModules(reactContext: ReactApplicationContext): List {
+ return listOf(PasskeyModule(reactContext))
+ }
+
+ override fun createViewManagers(reactContext: ReactApplicationContext): List> {
+ return emptyList()
+ }
+}
diff --git a/example/ios/Auth0Example.xcodeproj/project.pbxproj b/example/ios/Auth0Example.xcodeproj/project.pbxproj
index c469f3ef..03dc33ff 100644
--- a/example/ios/Auth0Example.xcodeproj/project.pbxproj
+++ b/example/ios/Auth0Example.xcodeproj/project.pbxproj
@@ -12,6 +12,8 @@
815544A03F48449898D8605F /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = C63DF729B29B9546313C3403 /* PrivacyInfo.xcprivacy */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
E99D02CA2DCD372E003D3E67 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E99D02C92DCD372E003D3E67 /* AppDelegate.swift */; };
+ E99D02D32DCD3730003D3E67 /* PasskeyModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = E99D02D02DCD3730003D3E67 /* PasskeyModule.swift */; };
+ E99D02D42DCD3730003D3E67 /* PasskeyModule.m in Sources */ = {isa = PBXBuildFile; fileRef = E99D02D12DCD3730003D3E67 /* PasskeyModule.m */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -35,6 +37,9 @@
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Auth0Example/LaunchScreen.storyboard; sourceTree = ""; };
C63DF729B29B9546313C3403 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Auth0Example/PrivacyInfo.xcprivacy; sourceTree = ""; };
E99D02C92DCD372E003D3E67 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ E99D02D02DCD3730003D3E67 /* PasskeyModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PasskeyModule.swift; path = Auth0Example/PasskeyModule.swift; sourceTree = ""; };
+ E99D02D12DCD3730003D3E67 /* PasskeyModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = PasskeyModule.m; path = Auth0Example/PasskeyModule.m; sourceTree = ""; };
+ E99D02D22DCD3730003D3E67 /* Auth0Example-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Auth0Example-Bridging-Header.h"; path = "Auth0Example/Auth0Example-Bridging-Header.h"; sourceTree = ""; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
@@ -65,6 +70,9 @@
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
C63DF729B29B9546313C3403 /* PrivacyInfo.xcprivacy */,
E99D02C92DCD372E003D3E67 /* AppDelegate.swift */,
+ E99D02D02DCD3730003D3E67 /* PasskeyModule.swift */,
+ E99D02D12DCD3730003D3E67 /* PasskeyModule.m */,
+ E99D02D22DCD3730003D3E67 /* Auth0Example-Bridging-Header.h */,
);
name = Auth0Example;
sourceTree = "";
@@ -323,6 +331,8 @@
buildActionMask = 2147483647;
files = (
E99D02CA2DCD372E003D3E67 /* AppDelegate.swift in Sources */,
+ E99D02D32DCD3730003D3E67 /* PasskeyModule.swift in Sources */,
+ E99D02D42DCD3730003D3E67 /* PasskeyModule.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/example/ios/Auth0Example/Auth0Example-Bridging-Header.h b/example/ios/Auth0Example/Auth0Example-Bridging-Header.h
new file mode 100644
index 00000000..5fff35c1
--- /dev/null
+++ b/example/ios/Auth0Example/Auth0Example-Bridging-Header.h
@@ -0,0 +1 @@
+#import
diff --git a/example/ios/Auth0Example/PasskeyModule.m b/example/ios/Auth0Example/PasskeyModule.m
new file mode 100644
index 00000000..49fcc83b
--- /dev/null
+++ b/example/ios/Auth0Example/PasskeyModule.m
@@ -0,0 +1,17 @@
+#import
+
+@interface RCT_EXTERN_MODULE(PasskeyModule, NSObject)
+
+RCT_EXTERN_METHOD(createPasskey:(NSString *)requestJson
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(getPasskey:(NSString *)requestJson
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
++ (BOOL)requiresMainQueueSetup {
+ return YES;
+}
+
+@end
diff --git a/example/ios/Auth0Example/PasskeyModule.swift b/example/ios/Auth0Example/PasskeyModule.swift
new file mode 100644
index 00000000..89e6a6d4
--- /dev/null
+++ b/example/ios/Auth0Example/PasskeyModule.swift
@@ -0,0 +1,196 @@
+import AuthenticationServices
+import Foundation
+
+@available(iOS 16.6, *)
+@objc(PasskeyModule)
+class PasskeyModule: NSObject {
+
+ @objc static func requiresMainQueueSetup() -> Bool {
+ return true
+ }
+
+ @objc func createPasskey(_ requestJson: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
+ guard #available(iOS 16.6, *) else {
+ reject("PASSKEY_NOT_AVAILABLE", "Passkeys require iOS 16.6 or later", nil)
+ return
+ }
+
+ guard let data = requestJson.data(using: .utf8),
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
+ reject("PASSKEY_FAILED", "Invalid request JSON", nil)
+ return
+ }
+
+ guard let rp = json["rp"] as? [String: Any],
+ let rpId = rp["id"] as? String,
+ let challengeStr = json["challenge"] as? String,
+ let challengeData = Data(base64URLEncoded: challengeStr),
+ let user = json["user"] as? [String: Any],
+ let userName = user["name"] as? String,
+ let userIdStr = user["id"] as? String,
+ let userId = Data(base64URLEncoded: userIdStr) else {
+ reject("PASSKEY_FAILED", "Missing required fields: rp.id, challenge, user.id, user.name", nil)
+ return
+ }
+
+ let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpId)
+ let request = provider.createCredentialRegistrationRequest(challenge: challengeData, name: userName, userID: userId)
+
+ let delegate = AuthorizationDelegate { credential in
+ guard let registration = credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration else {
+ reject("PASSKEY_FAILED", "Unexpected credential type", nil)
+ return
+ }
+ let result: [String: Any] = [
+ "id": registration.credentialID.base64URLEncodedString(),
+ "rawId": registration.credentialID.base64URLEncodedString(),
+ "type": "public-key",
+ "response": [
+ "clientDataJSON": registration.rawClientDataJSON.base64URLEncodedString(),
+ "attestationObject": (registration.rawAttestationObject ?? Data()).base64URLEncodedString()
+ ],
+ "authenticatorAttachment": "platform"
+ ]
+ if let jsonData = try? JSONSerialization.data(withJSONObject: result),
+ let jsonString = String(data: jsonData, encoding: .utf8) {
+ resolve(jsonString)
+ } else {
+ reject("PASSKEY_FAILED", "Failed to serialize credential response", nil)
+ }
+ } onError: { error in
+ if let authError = error as? ASAuthorizationError, authError.code == .canceled {
+ reject("USER_CANCELLED", "User cancelled passkey creation", error)
+ } else {
+ reject("PASSKEY_FAILED", error.localizedDescription, error)
+ }
+ }
+
+ let controller = ASAuthorizationController(authorizationRequests: [request])
+ controller.delegate = delegate
+ controller.presentationContextProvider = delegate
+ objc_setAssociatedObject(controller, "delegate", delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+ controller.performRequests()
+ }
+
+ @objc func getPasskey(_ requestJson: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
+ guard #available(iOS 16.6, *) else {
+ reject("PASSKEY_NOT_AVAILABLE", "Passkeys require iOS 16.6 or later", nil)
+ return
+ }
+
+ guard let data = requestJson.data(using: .utf8),
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
+ reject("PASSKEY_FAILED", "Invalid request JSON", nil)
+ return
+ }
+
+ guard let challengeStr = json["challenge"] as? String,
+ let challengeData = Data(base64URLEncoded: challengeStr) else {
+ reject("PASSKEY_FAILED", "Missing required 'challenge' field", nil)
+ return
+ }
+
+ let rpId = json["rpId"] as? String ?? ""
+ let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpId)
+ let assertionRequest = provider.createCredentialAssertionRequest(challenge: challengeData)
+
+ if let allowCredentials = json["allowCredentials"] as? [[String: Any]] {
+ assertionRequest.allowedCredentials = allowCredentials.compactMap { cred in
+ guard let idStr = cred["id"] as? String,
+ let idData = Data(base64URLEncoded: idStr) else { return nil }
+ return ASAuthorizationPlatformPublicKeyCredentialDescriptor(credentialID: idData)
+ }
+ }
+
+ let delegate = AuthorizationDelegate { credential in
+ guard let assertion = credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion else {
+ reject("PASSKEY_FAILED", "Unexpected credential type", nil)
+ return
+ }
+ var response: [String: Any] = [
+ "clientDataJSON": assertion.rawClientDataJSON.base64URLEncodedString(),
+ "authenticatorData": assertion.rawAuthenticatorData.base64URLEncodedString(),
+ "signature": assertion.signature.base64URLEncodedString()
+ ]
+ if let userHandle = assertion.userID {
+ response["userHandle"] = userHandle.base64URLEncodedString()
+ }
+ let result: [String: Any] = [
+ "id": assertion.credentialID.base64URLEncodedString(),
+ "rawId": assertion.credentialID.base64URLEncodedString(),
+ "type": "public-key",
+ "response": response,
+ "authenticatorAttachment": "platform"
+ ]
+ if let jsonData = try? JSONSerialization.data(withJSONObject: result),
+ let jsonString = String(data: jsonData, encoding: .utf8) {
+ resolve(jsonString)
+ } else {
+ reject("PASSKEY_FAILED", "Failed to serialize credential response", nil)
+ }
+ } onError: { error in
+ if let authError = error as? ASAuthorizationError, authError.code == .canceled {
+ reject("USER_CANCELLED", "User cancelled passkey assertion", error)
+ } else {
+ reject("PASSKEY_FAILED", error.localizedDescription, error)
+ }
+ }
+
+ let controller = ASAuthorizationController(authorizationRequests: [assertionRequest])
+ controller.delegate = delegate
+ controller.presentationContextProvider = delegate
+ objc_setAssociatedObject(controller, "delegate", delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+ controller.performRequests()
+ }
+}
+
+// MARK: - Authorization Delegate
+
+@available(iOS 16.6, *)
+private class AuthorizationDelegate: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
+ private let onSuccess: (ASAuthorizationCredential) -> Void
+ private let onError: (Error) -> Void
+
+ init(onSuccess: @escaping (ASAuthorizationCredential) -> Void, onError: @escaping (Error) -> Void) {
+ self.onSuccess = onSuccess
+ self.onError = onError
+ super.init()
+ }
+
+ func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
+ onSuccess(authorization.credential)
+ }
+
+ func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
+ onError(error)
+ }
+
+ func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
+ return UIApplication.shared.connectedScenes
+ .compactMap { $0 as? UIWindowScene }
+ .flatMap { $0.windows }
+ .first { $0.isKeyWindow } ?? ASPresentationAnchor()
+ }
+}
+
+// MARK: - Data Base64URL Extensions
+
+private extension Data {
+ init?(base64URLEncoded string: String) {
+ var base64 = string
+ .replacingOccurrences(of: "-", with: "+")
+ .replacingOccurrences(of: "_", with: "/")
+ let remainder = base64.count % 4
+ if remainder > 0 {
+ base64.append(String(repeating: "=", count: 4 - remainder))
+ }
+ self.init(base64Encoded: base64)
+ }
+
+ func base64URLEncodedString() -> String {
+ return self.base64EncodedString()
+ .replacingOccurrences(of: "+", with: "-")
+ .replacingOccurrences(of: "/", with: "_")
+ .replacingOccurrences(of: "=", with: "")
+ }
+}
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index 37204a7d..9241ea0a 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -1,6 +1,6 @@
PODS:
- - A0Auth0 (5.4.0):
- - Auth0 (= 2.18.0)
+ - A0Auth0 (5.6.0):
+ - Auth0 (= 2.21.1)
- hermes-engine
- RCTRequired
- RCTTypeSafety
@@ -22,7 +22,7 @@ PODS:
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- - Auth0 (2.18.0):
+ - Auth0 (2.21.1):
- JWTDecode (= 3.3.0)
- SimpleKeychain (= 1.3.0)
- FBLazyVector (0.84.1)
@@ -1881,7 +1881,7 @@ PODS:
- React-utils (= 0.84.1)
- ReactNativeDependencies
- ReactNativeDependencies (0.84.1)
- - RNGestureHandler (2.30.0):
+ - RNGestureHandler (2.30.1):
- hermes-engine
- RCTRequired
- RCTTypeSafety
@@ -2197,8 +2197,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
- A0Auth0: fd3fc9887adae5e3e9229312b344727323dca709
- Auth0: ddc5c6c7eb17e2484fd8d50a15d99b657c36c72c
+ A0Auth0: b178a2e57f20df2488f69c9d94c6f484606b7cd0
+ Auth0: 06748eec43fdb07f392ce2404b40ded7e6f4c3e5
FBLazyVector: e97c19a5a442429d1988f182a1940fb08df514da
hermes-engine: 5a6d36f29e9659a4242ae9acfdaafa16c394a162
JWTDecode: 1ca6f765844457d0dd8690436860fecee788f631
@@ -2273,7 +2273,7 @@ SPEC CHECKSUMS:
ReactCodegen: 797de5178718324c6eba3327b07f9a423fbd5787
ReactCommon: 07572bf9e687c8a52fbe4a3641e9e3a1a477c78e
ReactNativeDependencies: 809d263441431cc96468c6259aef2b9e49193c6e
- RNGestureHandler: 07de6f059e0ee5744ae9a56feb07ee345338cc31
+ RNGestureHandler: 695faf9c1f0aa1a6d52ab778e459a1bcbae3153b
RNScreens: 6cb648bdad8fe9bee9259fe144df95b6d1d5b707
SimpleKeychain: 9c0f3ca8458fed74e01db864d181c5cbe278603e
Yoga: c0b3f2c7e8d3e327e450223a2414ca3fa296b9a2
diff --git a/example/src/passkey/PasskeyModule.ts b/example/src/passkey/PasskeyModule.ts
new file mode 100644
index 00000000..b400d706
--- /dev/null
+++ b/example/src/passkey/PasskeyModule.ts
@@ -0,0 +1,27 @@
+import { NativeModules, Platform } from 'react-native';
+
+const { PasskeyModule } = NativeModules;
+
+export const PasskeyModuleErrorCodes = {
+ USER_CANCELLED: 'USER_CANCELLED',
+} as const;
+
+export async function createPasskey(
+ options: Record
+): Promise {
+ if (Platform.OS === 'web') {
+ throw new Error('Passkeys are not supported on web');
+ }
+ const requestJson = JSON.stringify(options);
+ return PasskeyModule.createPasskey(requestJson);
+}
+
+export async function getPasskey(
+ options: Record
+): Promise {
+ if (Platform.OS === 'web') {
+ throw new Error('Passkeys are not supported on web');
+ }
+ const requestJson = JSON.stringify(options);
+ return PasskeyModule.getPasskey(requestJson);
+}
diff --git a/example/src/screens/hooks/Home.tsx b/example/src/screens/hooks/Home.tsx
index ce09fd37..0ccd2a2d 100644
--- a/example/src/screens/hooks/Home.tsx
+++ b/example/src/screens/hooks/Home.tsx
@@ -6,13 +6,25 @@ import {
Text,
StyleSheet,
Alert,
+ Platform,
} from 'react-native';
-import { useAuth0, WebAuthError, WebAuthErrorCodes } from 'react-native-auth0';
+import {
+ useAuth0,
+ WebAuthError,
+ WebAuthErrorCodes,
+ PasskeyError,
+ PasskeyErrorCodes,
+} from 'react-native-auth0';
import Button from '../../components/Button';
import Header from '../../components/Header';
import LabeledInput from '../../components/LabeledInput';
import Result from '../../components/Result';
import config from '../../auth0-configuration';
+import {
+ createPasskey,
+ getPasskey,
+ PasskeyModuleErrorCodes,
+} from '../../passkey/PasskeyModule';
const HomeScreen = () => {
const {
@@ -20,6 +32,9 @@ const HomeScreen = () => {
loginWithPasswordRealm,
sendEmailCode,
authorizeWithEmail,
+ passkeySignupChallenge,
+ passkeyLoginChallenge,
+ getTokenByPasskey,
error,
} = useAuth0();
@@ -28,6 +43,9 @@ const HomeScreen = () => {
const [otp, setOtp] = useState('');
const [showOtpInput, setShowOtpInput] = useState(false);
const [apiError, setApiError] = useState(null);
+ const [passkeyEmail, setPasskeyEmail] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [lastResult, setLastResult] = useState