diff --git a/A0Auth0.podspec b/A0Auth0.podspec index 0d8248e3..63ec9ec3 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.21.1' + s.dependency 'Auth0', '2.21.2' install_modules_dependencies(s) end diff --git a/EXAMPLES.md b/EXAMPLES.md index ae3e6ff6..8dca844d 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -14,17 +14,19 @@ - [Using custom headers with Auth0Provider component](#using-custom-headers-with-auth0provider-component) - [Set request-specific headers](#set-request-specific-headers) - [Credential Renewal Retry](#credential-renewal-retry) - - [Overview](#credential-renewal-retry-overview) - - [Prerequisites](#credential-renewal-retry-prerequisites) + - [Overview](#overview) + - [Prerequisites](#prerequisites) - [Using Retry with Hooks](#using-retry-with-hooks) - [Using Retry with Auth0 Class](#using-retry-with-auth0-class) - - [Platform Support](#credential-renewal-retry-platform-support) - - [Error Handling](#credential-renewal-retry-error-handling) + - [Platform Support](#platform-support) + - [Error Handling](#error-handling) - [Biometric Authentication](#biometric-authentication) - [Biometric Policy Types](#biometric-policy-types) - [Using with Auth0Provider (Hooks)](#using-with-auth0provider-hooks) - [Using with Auth0 Class](#using-with-auth0-class) - [Platform-Specific Behavior](#platform-specific-behavior) + - [Android](#android) + - [iOS](#ios) - [Migration from Previous Behavior](#migration-from-previous-behavior) - [Management API (Users)](#management-api-users) - [Patch user with user_metadata](#patch-user-with-user_metadata) @@ -32,15 +34,9 @@ - [Organizations](#organizations) - [Log in to an organization](#log-in-to-an-organization) - [Accept user invitations](#accept-user-invitations) -- [DPoP (Demonstrating Proof-of-Possession)](#dpop-demonstrating-proof-of-possession) - - [Enabling DPoP](#enabling-dpop) - - [Making API calls with DPoP](#making-api-calls-with-dpop) - - [Handling DPoP token migration](#handling-dpop-token-migration) - - [Checking token type](#checking-token-type) - - [Handling nonce errors](#handling-nonce-errors) - [Multi-Resource Refresh Tokens (MRRT)](#multi-resource-refresh-tokens-mrrt) - - [Overview](#mrrt-overview) - - [Prerequisites](#mrrt-prerequisites) + - [MRRT Overview](#mrrt-overview) + - [MRRT Prerequisites](#mrrt-prerequisites) - [Using MRRT with Hooks](#using-mrrt-with-hooks) - [Using MRRT with Auth0 Class](#using-mrrt-with-auth0-class) - [Web Platform Configuration](#web-platform-configuration) @@ -49,30 +45,58 @@ - [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) + - [Valid Token Type Patterns](#valid-token-type-patterns) + - [Reserved Namespaces (Forbidden)](#reserved-namespaces-forbidden) + - [Common Use Cases](#common-use-cases) + - [Error Codes Reference](#error-codes-reference) + - [Auth0 Actions Validation](#auth0-actions-validation) - [Passkeys](#passkeys) - - [Overview](#passkeys-overview) - - [Prerequisites](#passkeys-prerequisites) + - [Overview](#overview-1) + - [Prerequisites](#prerequisites-1) - [Signup with Passkey](#signup-with-passkey) - [Signin with Passkey](#signin-with-passkey) - - [Advanced: Manual Credential Manager Handling](#advanced-manual-credential-manager-handling) + - [Auth Response Format](#auth-response-format) - [Using Passkeys with Auth0 Class](#using-passkeys-with-auth0-class) - - [Error Handling](#passkeys-error-handling) - - [Platform Support](#passkeys-platform-support) + - [Signup Challenge Parameters](#signup-challenge-parameters) + - [Error Handling](#error-handling-1) + - [Platform Support](#platform-support-1) +- [My Account API](#my-account-api) + - [Overview](#overview-2) + - [Prerequisites](#prerequisites-2) + - [Passkey Enrollment](#passkey-enrollment) + - [Phone Enrollment](#phone-enrollment) + - [Email Enrollment](#email-enrollment) + - [TOTP Enrollment](#totp-enrollment) + - [Recovery Code Enrollment](#recovery-code-enrollment) + - [Managing Authentication Methods](#managing-authentication-methods) + - [Getting Available Factors](#getting-available-factors) + - [Error Handling](#error-handling-2) + - [Platform Support](#platform-support-2) - [Native to Web SSO](#native-to-web-sso) - - [Overview](#native-to-web-sso-overview) - - [Prerequisites](#native-to-web-sso-prerequisites) + - [Native to Web SSO Overview](#native-to-web-sso-overview) + - [Native to Web SSO Prerequisites](#native-to-web-sso-prerequisites) - [Using Native to Web SSO with Hooks](#using-native-to-web-sso-with-hooks) - [Using Native to Web SSO with Auth0 Class](#using-native-to-web-sso-with-auth0-class) - [SSO Exchange via Authentication API](#sso-exchange-via-authentication-api) - [Using SSO Exchange with Hooks](#using-sso-exchange-with-hooks) - [Using SSO Exchange with Auth0 Class](#using-sso-exchange-with-auth0-class) - [Sending the Session Transfer Token](#sending-the-session-transfer-token) + - [Option 1: As a Query Parameter](#option-1-as-a-query-parameter) + - [Option 2: As a Cookie (WebView only)](#option-2-as-a-cookie-webview-only) - [Bot Protection](#bot-protection) - [Domain Switching](#domain-switching) - - [Android](#android) - - [iOS](#ios) + - [Android](#android-1) + - [iOS](#ios-1) - [Expo](#expo) - [Allowed Browsers (Android)](#allowed-browsers-android) + - [Using with Hooks](#using-with-hooks) + - [Using with Auth0 Class](#using-with-auth0-class-1) +- [DPoP (Demonstrating Proof-of-Possession)](#dpop-demonstrating-proof-of-possession) + - [Enabling DPoP](#enabling-dpop) + - [Making API calls with DPoP](#making-api-calls-with-dpop) + - [Handling DPoP token migration](#handling-dpop-token-migration) + - [Checking token type](#checking-token-type) + - [Handling nonce errors](#handling-nonce-errors) ## Authentication API @@ -1284,6 +1308,222 @@ try { > **Note:** Passkeys require a real device for the full flow. Simulators/emulators may have limited support. +## My Account API + +### Overview + +The My Account API allows authenticated users to manage their own authentication methods (passkeys, phone, email, TOTP, push notifications, recovery codes). It provides endpoints for enrolling new factors, confirming enrollments with OTP, listing/updating/deleting authentication methods, and querying available factors. + +Access the My Account client via the `myAccount` property from `useAuth0()` or the `Auth0` class instance. + +### Prerequisites + +- A [custom domain](https://auth0.com/docs/customize/custom-domains) must be configured on your Auth0 tenant +- **iOS**: Associated Domains entitlement must be configured with `webcredentials:` for passkey support +- **Android**: App Links must be set up with your custom domain via an `assetlinks.json` file for passkey support +- The user must be authenticated +- An access token with the appropriate My Account API scopes is required: + - `read:me:authentication_methods` + - `create:me:authentication_methods` + - `update:me:authentication_methods` + - `delete:me:authentication_methods` + - `read:me:factors` + +Use `getApiCredentials` with the `https:///me/` audience to obtain a scoped token: + +```typescript +const credentials = await getApiCredentials( + `https://${domain}/me/`, + 'read:me:authentication_methods create:me:authentication_methods delete:me:authentication_methods update:me:authentication_methods read:me:factors' +); +const accessToken = credentials.accessToken; +``` + +### Passkey Enrollment + +Passkey enrollment is a two-step process: request a challenge, then verify with the credential response. + +```typescript +import { useAuth0 } from 'react-native-auth0'; +import { createPasskey } from './PasskeyModule'; // Your native passkey module + +const { myAccount, getApiCredentials } = useAuth0(); + +// Step 1: Request the enrollment challenge +const accessToken = (await getApiCredentials(`https://${domain}/me/`, scopes)) + .accessToken; +const challenge = await myAccount.passkeyEnrollmentChallenge({ accessToken }); + +// Step 2: Create a passkey using the platform credential manager +const credentialJson = await createPasskey(challenge.authParamsPublicKey); + +// Step 3: Verify the enrollment +const method = await myAccount.enrollPasskey({ + accessToken, + authenticationMethodId: challenge.authenticationMethodId, + authSession: challenge.authSession, + authResponse: credentialJson, + authParamsPublicKey: challenge.authParamsPublicKey, +}); + +console.log('Enrolled passkey:', method.id, method.keyId); +``` + +### Phone Enrollment + +```typescript +import { PreferredAuthenticationMethods } from 'react-native-auth0'; + +const { myAccount } = useAuth0(); + +// Step 1: Enroll the phone number (sends OTP) +const challenge = await myAccount.enrollPhone({ + accessToken, + phoneNumber: '+1234567890', + preferredAuthenticationMethod: PreferredAuthenticationMethods.SMS, // or VOICE +}); + +// Step 2: Confirm with OTP +const method = await myAccount.confirmPhoneEnrollment({ + accessToken, + id: challenge.id, + authSession: challenge.authSession, + otpCode: '123456', +}); +``` + +### Email Enrollment + +```typescript +// Step 1: Enroll the email (sends OTP) +const challenge = await myAccount.enrollEmail({ + accessToken, + emailAddress: 'user@example.com', +}); + +// Step 2: Confirm with OTP +const method = await myAccount.confirmEmailEnrollment({ + accessToken, + id: challenge.id, + authSession: challenge.authSession, + otpCode: '123456', +}); +``` + +### TOTP Enrollment + +```typescript +// Step 1: Enroll TOTP (returns QR code / manual code) +const challenge = await myAccount.enrollTOTP({ accessToken }); +// Display challenge.barcodeUri as a QR code, or show challenge.manualInputCode + +// Step 2: Confirm with OTP from authenticator app +const method = await myAccount.confirmTOTPEnrollment({ + accessToken, + id: challenge.id, + authSession: challenge.authSession, + otpCode: '123456', +}); +``` + +### Recovery Code Enrollment + +```typescript +// Step 1: Enroll recovery code +const challenge = await myAccount.enrollRecoveryCode({ accessToken }); +// Store challenge.recoveryCode securely + +// Step 2: Confirm enrollment +const method = await myAccount.confirmRecoveryCodeEnrollment({ + accessToken, + id: challenge.id, + authSession: challenge.authSession, +}); +``` + +### Managing Authentication Methods + +```typescript +import { AuthenticationMethodTypes } from 'react-native-auth0'; + +// List all methods +const methods = await myAccount.getAuthenticationMethods({ accessToken }); + +// List only passkey methods +const passkeys = await myAccount.getAuthenticationMethods({ + accessToken, + type: AuthenticationMethodTypes.PASSKEY, +}); + +// Get a specific method +const method = await myAccount.getAuthenticationMethodById({ + accessToken, + id: 'authentication-method-id', +}); + +// Update a method name +const updated = await myAccount.updateAuthenticationMethodById({ + accessToken, + id: 'authentication-method-id', + name: 'My Work Phone', +}); + +// Delete a method +await myAccount.deleteAuthenticationMethodById({ + accessToken, + id: 'authentication-method-id', +}); +``` + +### Getting Available Factors + +```typescript +const factors = await myAccount.getFactors({ accessToken }); +// Returns available factor types (e.g., sms, email, totp, push-notification, webauthn-platform) +``` + +### Error Handling + +```typescript +import { MyAccountError, MyAccountErrorCodes, PasskeyError, PasskeyErrorCodes } from 'react-native-auth0'; + +try { + await myAccount.enrollPasskey({ ... }); +} catch (e) { + if (e instanceof PasskeyError) { + switch (e.type) { + case PasskeyErrorCodes.NOT_AVAILABLE: + // Passkeys not supported on this device + break; + default: + console.error(`Passkey error: [${e.type}] ${e.message}`); + } + } else if (e instanceof MyAccountError) { + switch (e.type) { + case MyAccountErrorCodes.ENROLLMENT_FAILED: + // Enrollment failed + break; + case MyAccountErrorCodes.VERIFICATION_FAILED: + // OTP verification failed + break; + case MyAccountErrorCodes.UNAUTHORIZED: + // Token expired or insufficient scopes + break; + default: + console.error(`My Account error: [${e.type}] ${e.message}`); + } + } +} +``` + +### Platform Support + +| Platform | Support | Notes | +| ----------- | ---------------- | --------------------------------------------------------- | +| **iOS** | ✅ Supported | Passkey enrollment requires iOS 16.6+ | +| **Android** | ✅ Supported | Passkey enrollment requires Android API 28+ | +| **Web** | ❌ Not Supported | Throws `PasskeyError` with `PASSKEY_UNSUPPORTED_PLATFORM` | + ## Native to Web SSO ### Native to Web SSO Overview diff --git a/android/build.gradle b/android/build.gradle index f41f799b..35961589 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.17.0' + implementation 'com.auth0.android:auth0:3.18.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 ae97fae3..186d43bc 100644 --- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt +++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt @@ -2,6 +2,7 @@ package com.auth0.react import android.app.Activity import android.content.Intent +import android.os.Build import androidx.fragment.app.FragmentActivity import com.auth0.android.Auth0 import com.auth0.android.authentication.AuthenticationAPIClient @@ -99,6 +100,7 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 private var useDPoP: Boolean = true private var auth0: Auth0? = null + private var myAccount: MyAccount? = null private lateinit var secureCredentialsManager: SecureCredentialsManager private var webAuthPromise: Promise? = null @@ -200,6 +202,7 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 this.useDPoP = useDPoP ?: true auth0 = Auth0.getInstance(clientId, domain) + myAccount = MyAccount(auth0!!, this.useDPoP, reactContext) val authAPI = AuthenticationAPIClient(auth0!!) if (this.useDPoP) { @@ -677,6 +680,146 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 } + @ReactMethod + override fun passkeyEnrollmentChallenge( + accessToken: String, + userIdentity: String?, + connection: String?, + promise: Promise + ) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + promise.reject("PASSKEYS_NOT_SUPPORTED", "Passkeys require Android API 28 or higher", null) + return + } + myAccount!!.passkeyEnrollmentChallenge(accessToken, userIdentity, connection, promise) + } + + @ReactMethod + override fun enrollPasskey( + accessToken: String, + authenticationMethodId: String, + authSession: String, + authResponse: String, + authParamsPublicKey: String, + promise: Promise + ) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + promise.reject("PASSKEYS_NOT_SUPPORTED", "Passkeys require Android API 28 or higher", null) + return + } + myAccount!!.enrollPasskey(accessToken, authenticationMethodId, authSession, authResponse, authParamsPublicKey, promise) + } + + @ReactMethod + override fun getAuthenticationMethods( + accessToken: String, + type: String?, + promise: Promise + ) { + myAccount!!.getAuthenticationMethods(accessToken, type, promise) + } + + @ReactMethod + override fun getAuthenticationMethodById( + accessToken: String, + id: String, + promise: Promise + ) { + myAccount!!.getAuthenticationMethodById(accessToken, id, promise) + } + + @ReactMethod + override fun updateAuthenticationMethodById( + accessToken: String, + id: String, + name: String?, + preferredAuthenticationMethod: String?, + promise: Promise + ) { + myAccount!!.updateAuthenticationMethodById(accessToken, id, name, preferredAuthenticationMethod, promise) + } + + @ReactMethod + override fun deleteAuthenticationMethodById( + accessToken: String, + id: String, + promise: Promise + ) { + myAccount!!.deleteAuthenticationMethodById(accessToken, id, promise) + } + + @ReactMethod + override fun enrollPhone( + accessToken: String, + phoneNumber: String, + preferredAuthenticationMethod: String?, + promise: Promise + ) { + myAccount!!.enrollPhone(accessToken, phoneNumber, preferredAuthenticationMethod, promise) + } + + @ReactMethod + override fun enrollEmail( + accessToken: String, + emailAddress: String, + promise: Promise + ) { + myAccount!!.enrollEmail(accessToken, emailAddress, promise) + } + + @ReactMethod + override fun enrollTOTP( + accessToken: String, + promise: Promise + ) { + myAccount!!.enrollTOTP(accessToken, promise) + } + + @ReactMethod + override fun enrollPushNotification( + accessToken: String, + promise: Promise + ) { + myAccount!!.enrollPushNotification(accessToken, promise) + } + + @ReactMethod + override fun enrollRecoveryCode( + accessToken: String, + promise: Promise + ) { + myAccount!!.enrollRecoveryCode(accessToken, promise) + } + + @ReactMethod + override fun confirmEnrollmentWithOtp( + accessToken: String, + id: String, + authSession: String, + otpCode: String, + promise: Promise + ) { + myAccount!!.confirmEnrollmentWithOtp(accessToken, id, authSession, otpCode, promise) + } + + @ReactMethod + override fun confirmEnrollment( + accessToken: String, + id: String, + authSession: String, + promise: Promise + ) { + myAccount!!.confirmEnrollment(accessToken, id, authSession, promise) + } + + @ReactMethod + override fun getFactors( + accessToken: String, + promise: Promise + ) { + myAccount!!.getFactors(accessToken, promise) + } + override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) { // No-op } diff --git a/android/src/main/java/com/auth0/react/MyAccount.kt b/android/src/main/java/com/auth0/react/MyAccount.kt new file mode 100644 index 00000000..5b187ed8 --- /dev/null +++ b/android/src/main/java/com/auth0/react/MyAccount.kt @@ -0,0 +1,453 @@ +package com.auth0.react + +import com.auth0.android.Auth0 +import com.auth0.android.myaccount.AuthenticationMethodType +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.myaccount.PhoneAuthenticationMethodType +import com.auth0.android.request.PublicKeyCredentials +import com.auth0.android.result.AuthenticationMethod +import com.auth0.android.result.AuthenticatorSelection +import com.auth0.android.result.AuthnParamsPublicKey +import com.auth0.android.result.EnrollmentChallenge +import com.auth0.android.result.Factor +import com.auth0.android.result.MfaEnrollmentChallenge +import com.auth0.android.result.OobEnrollmentChallenge +import com.auth0.android.result.PasskeyAuthenticationMethod +import com.auth0.android.result.PasskeyEnrollmentChallenge +import com.auth0.android.result.PasskeyUser +import com.auth0.android.result.RecoveryCodeEnrollmentChallenge +import com.auth0.android.result.RelyingParty +import com.auth0.android.result.TotpEnrollmentChallenge +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.WritableNativeArray +import com.facebook.react.bridge.WritableNativeMap +import com.google.gson.Gson + +class MyAccount( + private val auth0: Auth0, + private val useDPoP: Boolean, + private val reactContext: ReactApplicationContext +) { + + private fun createClient(accessToken: String): MyAccountAPIClient { + val client = MyAccountAPIClient(auth0, accessToken) + if (useDPoP) { + client.useDPoP(reactContext) + } + return client + } + + private fun rejectWithMyAccountError(promise: Promise, error: MyAccountException) { + val code = error.type ?: "MY_ACCOUNT_ERROR" + val message = Gson().toJson(mapOf( + "type" to error.type, + "title" to error.title, + "detail" to error.detail, + "statusCode" to error.statusCode + )) + promise.reject(code, message, error) + } + + fun passkeyEnrollmentChallenge( + accessToken: String, + userIdentity: String?, + connection: String?, + promise: Promise + ) { + val myAccountClient = createClient(accessToken) + + val finalUserIdentity = userIdentity?.trim()?.ifEmpty { null } + val finalConnection = connection?.trim()?.ifEmpty { null } + + myAccountClient.passkeyEnrollmentChallenge(finalUserIdentity, finalConnection) + .start(object : com.auth0.android.callback.Callback { + override fun onSuccess(challenge: PasskeyEnrollmentChallenge) { + val result = WritableNativeMap().apply { + putString("authenticationMethodId", challenge.authenticationMethodId) + putString("authSession", challenge.authSession) + val authParamsJson = Gson().toJson(challenge.authParamsPublicKey) + putMap("authParamsPublicKey", JsonUtils.jsonToWritableMap(authParamsJson)) + } + promise.resolve(result) + } + + override fun onFailure(error: MyAccountException) { + rejectWithMyAccountError(promise, error) + } + }) + } + + fun enrollPasskey( + accessToken: String, + authenticationMethodId: String, + authSession: String, + authResponse: String, + authParamsPublicKey: String, + promise: Promise + ) { + val myAccountClient = createClient(accessToken) + + val publicKeyCredentials = try { + Gson().fromJson(authResponse, PublicKeyCredentials::class.java) + } catch (e: Exception) { + promise.reject("MY_ACCOUNT_ERROR", "Invalid authResponse JSON: ${e.message}", e) + return + } + + val authParams = try { + Gson().fromJson(authParamsPublicKey, AuthnParamsPublicKey::class.java) + } catch (e: Exception) { + promise.reject("MY_ACCOUNT_ERROR", "Invalid authParamsPublicKey JSON: ${e.message}", e) + return + } + val challenge = PasskeyEnrollmentChallenge(authenticationMethodId, authSession, authParams) + + myAccountClient.enroll(publicKeyCredentials, challenge) + .start(object : com.auth0.android.callback.Callback { + override fun onSuccess(method: PasskeyAuthenticationMethod) { + val result = WritableNativeMap().apply { + putString("id", method.id) + putString("type", method.type) + putString("userIdentityId", method.identityUserId) + putString("userAgent", method.userAgent) + putString("keyId", method.keyId) + putString("publicKey", method.publicKey) + putString("userHandle", method.userHandle) + putString("credentialDeviceType", method.credentialDeviceType) + putBoolean("credentialBackedUp", method.credentialBackedUp ?: false) + putString("createdAt", method.createdAt) + putString("aaguid", method.aaguid) + putString("relyingPartyId", method.relyingPartyId) + } + promise.resolve(result) + } + + override fun onFailure(error: MyAccountException) { + rejectWithMyAccountError(promise, error) + } + }) + } + + fun getAuthenticationMethods( + accessToken: String, + type: String?, + promise: Promise + ) { + val myAccountClient = createClient(accessToken) + + val methodType = if (!type.isNullOrEmpty()) { + parseAuthenticationMethodType(type) ?: run { + promise.reject("MY_ACCOUNT_ERROR", "Invalid authentication method type: $type", null) + return + } + } else null + + myAccountClient.getAuthenticationMethods(methodType) + .start(object : com.auth0.android.callback.Callback, MyAccountException> { + override fun onSuccess(methods: List) { + val result = WritableNativeArray() + for (method in methods) { + val json = Gson().toJson(method) + result.pushMap(JsonUtils.jsonToWritableMap(json)) + } + promise.resolve(result) + } + + override fun onFailure(error: MyAccountException) { + rejectWithMyAccountError(promise, error) + } + }) + } + + fun getAuthenticationMethodById( + accessToken: String, + id: String, + promise: Promise + ) { + val myAccountClient = createClient(accessToken) + + myAccountClient.getAuthenticationMethodById(id) + .start(object : com.auth0.android.callback.Callback { + override fun onSuccess(method: AuthenticationMethod) { + val json = Gson().toJson(method) + promise.resolve(JsonUtils.jsonToWritableMap(json)) + } + + override fun onFailure(error: MyAccountException) { + rejectWithMyAccountError(promise, error) + } + }) + } + + fun updateAuthenticationMethodById( + accessToken: String, + id: String, + name: String?, + preferredAuthenticationMethod: String?, + promise: Promise + ) { + val myAccountClient = createClient(accessToken) + + val nameValue = name?.ifEmpty { null } + + val phoneMethod = if (!preferredAuthenticationMethod.isNullOrEmpty()) { + parsePhoneAuthenticationMethodType(preferredAuthenticationMethod) ?: run { + promise.reject("MY_ACCOUNT_ERROR", "Invalid preferred authentication method: $preferredAuthenticationMethod", null) + return + } + } else null + + myAccountClient.updateAuthenticationMethodById(id, nameValue, phoneMethod) + .start(object : com.auth0.android.callback.Callback { + override fun onSuccess(method: AuthenticationMethod) { + val json = Gson().toJson(method) + promise.resolve(JsonUtils.jsonToWritableMap(json)) + } + + override fun onFailure(error: MyAccountException) { + rejectWithMyAccountError(promise, error) + } + }) + } + + fun deleteAuthenticationMethodById( + accessToken: String, + id: String, + promise: Promise + ) { + val myAccountClient = createClient(accessToken) + + myAccountClient.deleteAuthenticationMethod(id) + .start(object : com.auth0.android.callback.Callback { + override fun onSuccess(result: Void?) { + promise.resolve(null) + } + + override fun onFailure(error: MyAccountException) { + rejectWithMyAccountError(promise, error) + } + }) + } + + fun enrollPhone( + accessToken: String, + phoneNumber: String, + preferredAuthenticationMethod: String?, + promise: Promise + ) { + val myAccountClient = createClient(accessToken) + val preferredMethod = if (!preferredAuthenticationMethod.isNullOrEmpty()) { + parsePhoneAuthenticationMethodType(preferredAuthenticationMethod) ?: run { + promise.reject("MY_ACCOUNT_ENROLLMENT_FAILED", "Invalid preferred authentication method: $preferredAuthenticationMethod", null) + return + } + } else PhoneAuthenticationMethodType.SMS + + myAccountClient.enrollPhone(phoneNumber, preferredMethod) + .start(object : com.auth0.android.callback.Callback { + override fun onSuccess(challenge: EnrollmentChallenge) { + val result = WritableNativeMap().apply { + putString("id", challenge.id) + putString("authSession", challenge.authSession) + } + promise.resolve(result) + } + + override fun onFailure(error: MyAccountException) { + rejectWithMyAccountError(promise, error) + } + }) + } + + fun enrollEmail( + accessToken: String, + emailAddress: String, + promise: Promise + ) { + val myAccountClient = createClient(accessToken) + + myAccountClient.enrollEmail(emailAddress) + .start(object : com.auth0.android.callback.Callback { + override fun onSuccess(challenge: EnrollmentChallenge) { + val result = WritableNativeMap().apply { + putString("id", challenge.id) + putString("authSession", challenge.authSession) + } + promise.resolve(result) + } + + override fun onFailure(error: MyAccountException) { + rejectWithMyAccountError(promise, error) + } + }) + } + + fun enrollTOTP( + accessToken: String, + promise: Promise + ) { + val myAccountClient = createClient(accessToken) + + myAccountClient.enrollTotp() + .start(object : com.auth0.android.callback.Callback { + override fun onSuccess(challenge: TotpEnrollmentChallenge) { + val result = WritableNativeMap().apply { + putString("id", challenge.id) + putString("authSession", challenge.authSession) + putString("barcodeUri", challenge.barcodeUri) + challenge.manualInputCode?.let { putString("manualInputCode", it) } + } + promise.resolve(result) + } + + override fun onFailure(error: MyAccountException) { + rejectWithMyAccountError(promise, error) + } + }) + } + + fun enrollPushNotification( + accessToken: String, + promise: Promise + ) { + val myAccountClient = createClient(accessToken) + + myAccountClient.enrollPushNotification() + .start(object : com.auth0.android.callback.Callback { + override fun onSuccess(challenge: TotpEnrollmentChallenge) { + val result = WritableNativeMap().apply { + putString("id", challenge.id) + putString("authSession", challenge.authSession) + putString("barcodeUri", challenge.barcodeUri) + challenge.manualInputCode?.let { putString("manualInputCode", it) } + } + promise.resolve(result) + } + + override fun onFailure(error: MyAccountException) { + rejectWithMyAccountError(promise, error) + } + }) + } + + fun enrollRecoveryCode( + accessToken: String, + promise: Promise + ) { + val myAccountClient = createClient(accessToken) + + myAccountClient.enrollRecoveryCode() + .start(object : com.auth0.android.callback.Callback { + override fun onSuccess(challenge: RecoveryCodeEnrollmentChallenge) { + val result = WritableNativeMap().apply { + putString("id", challenge.id) + putString("authSession", challenge.authSession) + putString("recoveryCode", challenge.recoveryCode) + } + promise.resolve(result) + } + + override fun onFailure(error: MyAccountException) { + rejectWithMyAccountError(promise, error) + } + }) + } + + fun confirmEnrollmentWithOtp( + accessToken: String, + id: String, + authSession: String, + otpCode: String, + promise: Promise + ) { + val myAccountClient = createClient(accessToken) + + myAccountClient.verifyOtp(id, otpCode, authSession) + .start(object : com.auth0.android.callback.Callback { + override fun onSuccess(method: AuthenticationMethod) { + val json = Gson().toJson(method) + promise.resolve(JsonUtils.jsonToWritableMap(json)) + } + + override fun onFailure(error: MyAccountException) { + rejectWithMyAccountError(promise, error) + } + }) + } + + fun confirmEnrollment( + accessToken: String, + id: String, + authSession: String, + promise: Promise + ) { + val myAccountClient = createClient(accessToken) + + myAccountClient.verify(id, authSession) + .start(object : com.auth0.android.callback.Callback { + override fun onSuccess(method: AuthenticationMethod) { + val json = Gson().toJson(method) + promise.resolve(JsonUtils.jsonToWritableMap(json)) + } + + override fun onFailure(error: MyAccountException) { + rejectWithMyAccountError(promise, error) + } + }) + } + + fun getFactors( + accessToken: String, + promise: Promise + ) { + val myAccountClient = createClient(accessToken) + + myAccountClient.getFactors() + .start(object : com.auth0.android.callback.Callback, MyAccountException> { + override fun onSuccess(factors: List) { + val result = WritableNativeArray() + for (factor in factors) { + val map = WritableNativeMap().apply { + putString("type", factor.type) + factor.usage?.let { usageList -> + val usageArray = WritableNativeArray() + usageList.forEach { usageArray.pushString(it) } + putArray("usage", usageArray) + } + } + result.pushMap(map) + } + promise.resolve(result) + } + + override fun onFailure(error: MyAccountException) { + rejectWithMyAccountError(promise, error) + } + }) + } + + private fun parseAuthenticationMethodType(type: String): AuthenticationMethodType? { + return when (type) { + "passkey" -> AuthenticationMethodType.PASSKEY + "phone" -> AuthenticationMethodType.PHONE + "email" -> AuthenticationMethodType.EMAIL + "totp" -> AuthenticationMethodType.TOTP + "push-notification" -> AuthenticationMethodType.PUSH + "recovery-code" -> AuthenticationMethodType.RECOVERY_CODE + "webauthn-platform" -> AuthenticationMethodType.WEBAUTHN_PLATFORM + "webauthn-roaming" -> AuthenticationMethodType.WEBAUTHN_ROAMING + "password" -> AuthenticationMethodType.PASSWORD + else -> null + } + } + + private fun parsePhoneAuthenticationMethodType(method: String): PhoneAuthenticationMethodType? { + return when (method) { + "sms" -> PhoneAuthenticationMethodType.SMS + "voice" -> PhoneAuthenticationMethodType.VOICE + else -> null + } + } +} diff --git a/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt b/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt index fc56c307..21434199 100644 --- a/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt +++ b/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt @@ -160,4 +160,122 @@ abstract class A0Auth0Spec(context: ReactApplicationContext) : ReactContextBaseJ promise: Promise ) + @ReactMethod + @DoNotStrip + abstract fun passkeyEnrollmentChallenge( + accessToken: String, + userIdentity: String?, + connection: String?, + promise: Promise + ) + + @ReactMethod + @DoNotStrip + abstract fun enrollPasskey( + accessToken: String, + authenticationMethodId: String, + authSession: String, + authResponse: String, + authParamsPublicKey: String, + promise: Promise + ) + + @ReactMethod + @DoNotStrip + abstract fun getAuthenticationMethods( + accessToken: String, + type: String?, + promise: Promise + ) + + @ReactMethod + @DoNotStrip + abstract fun getAuthenticationMethodById( + accessToken: String, + id: String, + promise: Promise + ) + + @ReactMethod + @DoNotStrip + abstract fun updateAuthenticationMethodById( + accessToken: String, + id: String, + name: String?, + preferredAuthenticationMethod: String?, + promise: Promise + ) + + @ReactMethod + @DoNotStrip + abstract fun deleteAuthenticationMethodById( + accessToken: String, + id: String, + promise: Promise + ) + + @ReactMethod + @DoNotStrip + abstract fun enrollPhone( + accessToken: String, + phoneNumber: String, + preferredAuthenticationMethod: String?, + promise: Promise + ) + + @ReactMethod + @DoNotStrip + abstract fun enrollEmail( + accessToken: String, + emailAddress: String, + promise: Promise + ) + + @ReactMethod + @DoNotStrip + abstract fun enrollTOTP( + accessToken: String, + promise: Promise + ) + + @ReactMethod + @DoNotStrip + abstract fun enrollPushNotification( + accessToken: String, + promise: Promise + ) + + @ReactMethod + @DoNotStrip + abstract fun enrollRecoveryCode( + accessToken: String, + promise: Promise + ) + + @ReactMethod + @DoNotStrip + abstract fun confirmEnrollmentWithOtp( + accessToken: String, + id: String, + authSession: String, + otpCode: String, + promise: Promise + ) + + @ReactMethod + @DoNotStrip + abstract fun confirmEnrollment( + accessToken: String, + id: String, + authSession: String, + promise: Promise + ) + + @ReactMethod + @DoNotStrip + abstract fun getFactors( + accessToken: String, + promise: Promise + ) + } \ No newline at end of file diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 9241ea0a..09f55925 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,6 +1,6 @@ PODS: - A0Auth0 (5.6.0): - - Auth0 (= 2.21.1) + - Auth0 (= 2.21.2) - hermes-engine - RCTRequired - RCTTypeSafety @@ -22,7 +22,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - Auth0 (2.21.1): + - Auth0 (2.21.2): - JWTDecode (= 3.3.0) - SimpleKeychain (= 1.3.0) - FBLazyVector (0.84.1) @@ -1427,7 +1427,7 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - ReactNativeDependencies - - react-native-safe-area-context (5.7.0): + - react-native-safe-area-context (5.8.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -1439,8 +1439,8 @@ PODS: - React-graphics - React-ImageManager - React-jsi - - react-native-safe-area-context/common (= 5.7.0) - - react-native-safe-area-context/fabric (= 5.7.0) + - react-native-safe-area-context/common (= 5.8.0) + - react-native-safe-area-context/fabric (= 5.8.0) - React-NativeModulesApple - React-RCTFabric - React-renderercss @@ -1451,7 +1451,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - react-native-safe-area-context/common (5.7.0): + - react-native-safe-area-context/common (5.8.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -1473,7 +1473,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - react-native-safe-area-context/fabric (5.7.0): + - react-native-safe-area-context/fabric (5.8.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2197,8 +2197,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - A0Auth0: b178a2e57f20df2488f69c9d94c6f484606b7cd0 - Auth0: 06748eec43fdb07f392ce2404b40ded7e6f4c3e5 + A0Auth0: 7017e8ccdeda46385612e0b9ab231d81de53d128 + Auth0: e15bc9c1e39a53efc8853d16460b9be409d5346f FBLazyVector: e97c19a5a442429d1988f182a1940fb08df514da hermes-engine: 5a6d36f29e9659a4242ae9acfdaafa16c394a162 JWTDecode: 1ca6f765844457d0dd8690436860fecee788f631 @@ -2238,7 +2238,7 @@ SPEC CHECKSUMS: React-logger: b5521614afb8690420146dfc61a1447bb5d65419 React-Mapbuffer: f4ee8c62e0ef8359d139124f35a471518a172cd3 React-microtasksnativemodule: d1956f0eec54c619b63a379520fb4c618a55ccb9 - react-native-safe-area-context: ae7587b95fb580d1800c5b0b2a7bd48c2868e67a + react-native-safe-area-context: 5727bd761ca52fa393b40989a6483bf859622136 React-NativeModulesApple: 5ba0903927f6b8d335a091700e9fda143980f819 React-networking: 3a4b7f9ed2b2d1c0441beacb79674323a24bcca6 React-oscompat: ff26abf0ae3e3fdbe47b44224571e3fc7226a573 diff --git a/example/src/navigation/MainTabNavigator.tsx b/example/src/navigation/MainTabNavigator.tsx index af4632ea..f470fdf8 100644 --- a/example/src/navigation/MainTabNavigator.tsx +++ b/example/src/navigation/MainTabNavigator.tsx @@ -5,12 +5,14 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import ProfileScreen from '../screens/hooks/Profile'; import MoreScreen from '../screens/hooks/More'; import CredentialsScreen from '../screens/hooks/CredentialsScreen'; +import MyAccountScreen from '../screens/hooks/MyAccountScreen'; export type MainTabParamList = { Profile: undefined; Api: undefined; More: undefined; Credentials: undefined; + MyAccount: undefined; }; const Tab = createBottomTabNavigator(); @@ -33,6 +35,11 @@ const MainTabNavigator = () => { // You can add icons here if desired /> + ); diff --git a/example/src/screens/hooks/MyAccountScreen.tsx b/example/src/screens/hooks/MyAccountScreen.tsx new file mode 100644 index 00000000..0570c781 --- /dev/null +++ b/example/src/screens/hooks/MyAccountScreen.tsx @@ -0,0 +1,634 @@ +import React, { useState } from 'react'; +import { + SafeAreaView, + ScrollView, + View, + Text, + StyleSheet, + Alert, + Platform, + TextInput, +} from 'react-native'; +import { + useAuth0, + MyAccountError, + PasskeyError, + PasskeyErrorCodes, + PreferredAuthenticationMethods, +} from 'react-native-auth0'; +import Button from '../../components/Button'; +import Header from '../../components/Header'; +import Result from '../../components/Result'; +import { createPasskey } from '../../passkey/PasskeyModule'; +import config from '../../auth0-configuration'; + +const MyAccountScreen = () => { + const { getApiCredentials, myAccount } = useAuth0(); + + const [loading, setLoading] = useState(false); + const [apiResult, setApiResult] = useState(null); + const [apiError, setApiError] = useState(null); + const [challengeState, setChallengeState] = useState<{ + authenticationMethodId: string; + authSession: string; + authParamsPublicKey: Record; + } | null>(null); + const [enrollmentState, setEnrollmentState] = useState<{ + id: string; + authSession: string; + } | null>(null); + const [otpCode, setOtpCode] = useState(''); + const [phoneNumber, setPhoneNumber] = useState(''); + const [emailAddress, setEmailAddress] = useState(''); + const [methodId, setMethodId] = useState(''); + const [methodName, setMethodName] = useState(''); + + const handleError = (e: any) => { + if (e instanceof PasskeyError) { + switch (e.type) { + case PasskeyErrorCodes.USER_CANCELLED: + Alert.alert('Cancelled', 'You dismissed the passkey prompt.'); + break; + case PasskeyErrorCodes.NOT_AVAILABLE: + Alert.alert( + 'Not Available', + 'Passkeys are not supported on this device.' + ); + break; + default: + Alert.alert('Passkey Error', `[${e.type}] ${e.message}`); + } + } else if (e instanceof MyAccountError) { + Alert.alert(e.title || 'My Account Error', e.detail || e.message); + } else { + Alert.alert('Error', (e as Error).message); + } + setApiError(e as Error); + }; + + const getMyAccountAccessToken = async (): Promise => { + const credentials = await getApiCredentials( + `https://${config.domain}/me/`, + 'read:me:authentication_methods delete:me:authentication_methods update:me:authentication_methods read:me:factors create:me:authentication_methods' + ); + return credentials.accessToken; + }; + + // --- Passkey Enrollment --- + + const onPasskeyEnrollmentChallenge = async () => { + setApiError(null); + setApiResult(null); + setChallengeState(null); + setLoading(true); + try { + const accessToken = await getMyAccountAccessToken(); + const challenge = await myAccount.passkeyEnrollmentChallenge({ + accessToken, + }); + + setChallengeState(challenge); + setApiResult({ + step: 'passkeyEnrollmentChallenge', + authenticationMethodId: challenge.authenticationMethodId, + authSession: challenge.authSession, + }); + } catch (e) { + handleError(e); + } finally { + setLoading(false); + } + }; + + const onPasskeyEnrollmentVerify = async () => { + if (!challengeState) { + Alert.alert('Error', 'Run Enrollment Challenge first.'); + return; + } + + setApiError(null); + setLoading(true); + try { + const credentialJson = await createPasskey( + challengeState.authParamsPublicKey + ); + + const accessToken = await getMyAccountAccessToken(); + const method = await myAccount.enrollPasskey({ + accessToken, + authenticationMethodId: challengeState.authenticationMethodId, + authSession: challengeState.authSession, + authResponse: credentialJson, + authParamsPublicKey: challengeState.authParamsPublicKey, + }); + + setChallengeState(null); + setApiResult({ + step: 'passkeyEnrollmentVerify', + id: method.id, + type: method.type, + keyId: method.keyId, + credentialDeviceType: method.credentialDeviceType, + credentialBackedUp: method.credentialBackedUp, + relyingPartyId: method.relyingPartyId, + createdAt: method.createdAt, + }); + Alert.alert('Success', 'Passkey enrolled successfully!'); + } catch (e) { + handleError(e); + } finally { + setLoading(false); + } + }; + + // --- Phone Enrollment --- + + const onEnrollPhone = async () => { + if (!phoneNumber.trim()) { + Alert.alert('Error', 'Please enter a phone number.'); + return; + } + setApiError(null); + setApiResult(null); + setEnrollmentState(null); + setLoading(true); + try { + const accessToken = await getMyAccountAccessToken(); + const challenge = await myAccount.enrollPhone({ + accessToken, + phoneNumber: phoneNumber.trim(), + preferredAuthenticationMethod: PreferredAuthenticationMethods.SMS, + }); + setEnrollmentState(challenge); + setApiResult({ step: 'enrollPhone', ...challenge }); + Alert.alert('OTP Sent', 'Check your phone for the verification code.'); + } catch (e) { + handleError(e); + } finally { + setLoading(false); + } + }; + + // --- Email Enrollment --- + + const onEnrollEmail = async () => { + if (!emailAddress.trim()) { + Alert.alert('Error', 'Please enter an email address.'); + return; + } + setApiError(null); + setApiResult(null); + setEnrollmentState(null); + setLoading(true); + try { + const accessToken = await getMyAccountAccessToken(); + const challenge = await myAccount.enrollEmail({ + accessToken, + emailAddress: emailAddress.trim(), + }); + setEnrollmentState(challenge); + setApiResult({ step: 'enrollEmail', ...challenge }); + Alert.alert('OTP Sent', 'Check your email for the verification code.'); + } catch (e) { + handleError(e); + } finally { + setLoading(false); + } + }; + + // --- TOTP Enrollment --- + + const onEnrollTOTP = async () => { + setApiError(null); + setApiResult(null); + setEnrollmentState(null); + setLoading(true); + try { + const accessToken = await getMyAccountAccessToken(); + const challenge = await myAccount.enrollTOTP({ accessToken }); + setEnrollmentState({ + id: challenge.id, + authSession: challenge.authSession, + }); + setApiResult({ + step: 'enrollTOTP', + id: challenge.id, + barcodeUri: challenge.barcodeUri, + manualInputCode: challenge.manualInputCode, + }); + Alert.alert( + 'TOTP Enrolled', + 'Scan the QR code with your authenticator app, then confirm with OTP.' + ); + } catch (e) { + handleError(e); + } finally { + setLoading(false); + } + }; + + // --- Recovery Code Enrollment --- + + const onEnrollRecoveryCode = async () => { + setApiError(null); + setApiResult(null); + setEnrollmentState(null); + setLoading(true); + try { + const accessToken = await getMyAccountAccessToken(); + const challenge = await myAccount.enrollRecoveryCode({ accessToken }); + setEnrollmentState({ + id: challenge.id, + authSession: challenge.authSession, + }); + setApiResult({ + step: 'enrollRecoveryCode', + id: challenge.id, + recoveryCode: challenge.recoveryCode, + }); + Alert.alert( + 'Recovery Code', + `Store this code securely: ${challenge.recoveryCode}` + ); + } catch (e) { + handleError(e); + } finally { + setLoading(false); + } + }; + + // --- Confirm Enrollment with OTP --- + + const onConfirmEnrollment = async () => { + if (!enrollmentState) { + Alert.alert('Error', 'Start an enrollment first.'); + return; + } + if (!otpCode.trim()) { + Alert.alert('Error', 'Please enter the OTP code.'); + return; + } + setApiError(null); + setLoading(true); + try { + const accessToken = await getMyAccountAccessToken(); + const method = await myAccount.confirmPhoneEnrollment({ + accessToken, + id: enrollmentState.id, + authSession: enrollmentState.authSession, + otpCode: otpCode.trim(), + }); + setEnrollmentState(null); + setOtpCode(''); + setApiResult({ step: 'confirmEnrollment', ...method }); + Alert.alert('Success', 'Enrollment confirmed!'); + } catch (e) { + handleError(e); + } finally { + setLoading(false); + } + }; + + // --- Confirm Recovery Code Enrollment --- + + const onConfirmRecoveryCode = async () => { + if (!enrollmentState) { + Alert.alert('Error', 'Start a recovery code enrollment first.'); + return; + } + setApiError(null); + setLoading(true); + try { + const accessToken = await getMyAccountAccessToken(); + const method = await myAccount.confirmRecoveryCodeEnrollment({ + accessToken, + id: enrollmentState.id, + authSession: enrollmentState.authSession, + }); + setEnrollmentState(null); + setApiResult({ step: 'confirmRecoveryCode', ...method }); + Alert.alert('Success', 'Recovery code enrollment confirmed!'); + } catch (e) { + handleError(e); + } finally { + setLoading(false); + } + }; + + // --- Get Factors --- + + const onGetFactors = async () => { + setApiError(null); + setApiResult(null); + setLoading(true); + try { + const accessToken = await getMyAccountAccessToken(); + const factors = await myAccount.getFactors({ accessToken }); + setApiResult({ step: 'getFactors', factors }); + } catch (e) { + handleError(e); + } finally { + setLoading(false); + } + }; + + // --- Get Authentication Methods --- + + const onGetAuthenticationMethods = async () => { + setApiError(null); + setApiResult(null); + setLoading(true); + try { + const accessToken = await getMyAccountAccessToken(); + const methods = await myAccount.getAuthenticationMethods({ accessToken }); + setApiResult({ + step: 'getAuthenticationMethods', + count: methods.length, + methods, + }); + } catch (e) { + handleError(e); + } finally { + setLoading(false); + } + }; + + // --- Update Authentication Method --- + + const onUpdateAuthenticationMethod = async () => { + if (!methodId.trim()) { + Alert.alert('Error', 'Please enter an authentication method ID.'); + return; + } + setApiError(null); + setApiResult(null); + setLoading(true); + try { + const accessToken = await getMyAccountAccessToken(); + const method = await myAccount.updateAuthenticationMethodById({ + accessToken, + id: methodId.trim(), + name: methodName.trim() || undefined, + }); + setApiResult({ step: 'updateAuthenticationMethodById', ...method }); + Alert.alert('Success', 'Authentication method updated!'); + } catch (e) { + handleError(e); + } finally { + setLoading(false); + } + }; + + // --- Delete Authentication Method --- + + const onDeleteAuthenticationMethod = async () => { + if (!methodId.trim()) { + Alert.alert('Error', 'Please enter an authentication method ID.'); + return; + } + Alert.alert( + 'Confirm Delete', + `Are you sure you want to delete method ${methodId.trim()}?`, + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Delete', + style: 'destructive', + onPress: async () => { + setApiError(null); + setApiResult(null); + setLoading(true); + try { + const accessToken = await getMyAccountAccessToken(); + await myAccount.deleteAuthenticationMethodById({ + accessToken, + id: methodId.trim(), + }); + setApiResult({ + step: 'deleteAuthenticationMethodById', + deleted: methodId.trim(), + }); + setMethodId(''); + Alert.alert('Success', 'Authentication method deleted!'); + } catch (e) { + handleError(e); + } finally { + setLoading(false); + } + }, + }, + ] + ); + }; + + if (Platform.OS === 'web') { + return ( + +
+ + + My Account API is only available on native platforms. + + + + ); + } + + return ( + +
+ + + Manage authentication methods for the currently authenticated user via + the My Account API. + + + + +
+
+ +
+ +
+ +
+ +
+ +
+
+ +
+
+ +
+ + After enrolling phone, email, or TOTP, confirm with the OTP code. + + +
+ +
+
+ +
+ + Enter an authentication method ID to update or delete it. + + + +
+ + {enrollmentState && ( + + Pending Enrollment: + + id: {enrollmentState.id} + {'\n'}authSession: {enrollmentState.authSession.substring(0, 20)} + ... + + + )} +
+ + ); +}; + +const Section = ({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) => ( + + {title} + {children} + +); + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: '#FFFFFF' }, + content: { padding: 16, gap: 16 }, + description: { fontSize: 14, color: '#666', textAlign: 'center' }, + section: { + borderWidth: 1, + borderColor: '#E0E0E0', + borderRadius: 8, + padding: 16, + gap: 10, + }, + sectionTitle: { fontSize: 18, fontWeight: 'bold', marginBottom: 4 }, + sectionDescription: { fontSize: 13, color: '#666', marginBottom: 4 }, + input: { + borderWidth: 1, + borderColor: '#CCC', + borderRadius: 6, + padding: 10, + fontSize: 14, + }, + resultBox: { + backgroundColor: '#F5F5F5', + borderRadius: 6, + padding: 10, + gap: 4, + }, + resultLabel: { fontSize: 12, fontWeight: '600', color: '#333' }, + resultValue: { fontSize: 11, color: '#555', fontFamily: 'monospace' }, +}); + +export default MyAccountScreen; diff --git a/ios/A0Auth0.mm b/ios/A0Auth0.mm index a47044d5..cabf15a9 100644 --- a/ios/A0Auth0.mm +++ b/ios/A0Auth0.mm @@ -25,6 +25,7 @@ @interface A0Auth0 () @property (strong, nonatomic) NativeBridge *nativeBridge; +@property (strong, nonatomic) A0MyAccount *myAccount; @end @implementation A0Auth0 @@ -214,6 +215,109 @@ - (dispatch_queue_t)methodQueue [self.nativeBridge getTokenByPasskeyWithAuthSession:authSession authResponse:authResponse realm:realm audience:audience scope:scope organization:organization resolve:resolve reject:reject]; } +RCT_EXPORT_METHOD(passkeyEnrollmentChallenge:(NSString *)accessToken + userIdentity:(NSString * _Nullable)userIdentity + connection:(NSString * _Nullable)connection + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [self.myAccount passkeyEnrollmentChallengeWithAccessToken:accessToken userIdentity:userIdentity connection:connection resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(enrollPasskey:(NSString *)accessToken + authenticationMethodId:(NSString *)authenticationMethodId + authSession:(NSString *)authSession + authResponse:(NSString *)authResponse + authParamsPublicKey:(NSString *)authParamsPublicKey + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [self.myAccount enrollPasskeyWithAccessToken:accessToken authenticationMethodId:authenticationMethodId authSession:authSession authResponse:authResponse authParamsPublicKey:authParamsPublicKey resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(getAuthenticationMethods:(NSString *)accessToken + type:(NSString * _Nullable)type + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [self.myAccount getAuthenticationMethodsWithAccessToken:accessToken type:type resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(getAuthenticationMethodById:(NSString *)accessToken + id:(NSString *)id + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [self.myAccount getAuthenticationMethodByIdWithAccessToken:accessToken id:id resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(updateAuthenticationMethodById:(NSString *)accessToken + id:(NSString *)id + name:(NSString * _Nullable)name + preferredAuthenticationMethod:(NSString * _Nullable)preferredAuthenticationMethod + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [self.myAccount updateAuthenticationMethodByIdWithAccessToken:accessToken id:id name:name preferredAuthenticationMethod:preferredAuthenticationMethod resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(deleteAuthenticationMethodById:(NSString *)accessToken + id:(NSString *)id + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [self.myAccount deleteAuthenticationMethodByIdWithAccessToken:accessToken id:id resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(enrollPhone:(NSString *)accessToken + phoneNumber:(NSString *)phoneNumber + preferredAuthenticationMethod:(NSString * _Nullable)preferredAuthenticationMethod + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [self.myAccount enrollPhoneWithAccessToken:accessToken phoneNumber:phoneNumber preferredAuthenticationMethod:preferredAuthenticationMethod resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(enrollEmail:(NSString *)accessToken + emailAddress:(NSString *)emailAddress + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [self.myAccount enrollEmailWithAccessToken:accessToken emailAddress:emailAddress resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(enrollTOTP:(NSString *)accessToken + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [self.myAccount enrollTOTPWithAccessToken:accessToken resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(enrollPushNotification:(NSString *)accessToken + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [self.myAccount enrollPushNotificationWithAccessToken:accessToken resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(enrollRecoveryCode:(NSString *)accessToken + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [self.myAccount enrollRecoveryCodeWithAccessToken:accessToken resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(confirmEnrollmentWithOtp:(NSString *)accessToken + id:(NSString *)id + authSession:(NSString *)authSession + otpCode:(NSString *)otpCode + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [self.myAccount confirmEnrollmentWithOtpWithAccessToken:accessToken id:id authSession:authSession otpCode:otpCode resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(confirmEnrollment:(NSString *)accessToken + id:(NSString *)id + authSession:(NSString *)authSession + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [self.myAccount confirmEnrollmentWithAccessToken:accessToken id:id authSession:authSession resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(getFactors:(NSString *)accessToken + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [self.myAccount getFactorsWithAccessToken:accessToken resolve:resolve reject:reject]; +} - (NSDictionary *)constantsToExport { return @{ @"bundleIdentifier": [[NSBundle mainBundle] bundleIdentifier] }; @@ -236,6 +340,7 @@ - (void)tryAndInitializeNativeBridge:(NSString *)clientId domain:(NSString *)dom BOOL useDPoPBool = [useDPoP boolValue]; NativeBridge *bridge = [[NativeBridge alloc] initWithClientId:clientId domain:domain localAuthenticationOptions:options useDPoP:useDPoPBool maxRetries:maxRetries resolve:resolve reject:reject]; self.nativeBridge = bridge; + self.myAccount = [[A0MyAccount alloc] initWithDomain:domain useDPoP:useDPoPBool]; } #ifdef RCT_NEW_ARCH_ENABLED - (std::shared_ptr)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params { diff --git a/ios/MyAccount.swift b/ios/MyAccount.swift new file mode 100644 index 00000000..7de90eb3 --- /dev/null +++ b/ios/MyAccount.swift @@ -0,0 +1,416 @@ +import Auth0 +import AuthenticationServices +import Foundation + +@objc +public class A0MyAccount: NSObject { + + private let domain: String + private let useDPoP: Bool + + @objc public init(domain: String, useDPoP: Bool) { + self.domain = domain + self.useDPoP = useDPoP + } + + private func createClient(accessToken: String) -> any MyAccount { + var client = Auth0.myAccount(token: accessToken, domain: self.domain) + if self.useDPoP { + client = client.useDPoP() + } + return client + } + + private func rejectWithMyAccountError(reject: @escaping RCTPromiseRejectBlock, error: MyAccountError) { + let code = error.code + let info: [String: Any] = [ + "type": error.code, + "title": error.title, + "detail": error.detail, + "statusCode": error.statusCode + ] + let message: String + if let data = try? JSONSerialization.data(withJSONObject: info), + let json = String(data: data, encoding: .utf8) { + message = json + } else { + message = error.localizedDescription + } + reject(code, message, error) + } + + @objc public func passkeyEnrollmentChallenge(accessToken: String, userIdentity: String?, connection: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + guard #available(iOS 16.6, *) else { + reject("PASSKEYS_NOT_SUPPORTED", "Passkeys require iOS 16.6 or later", nil) + return + } + + let myAccount = createClient(accessToken: accessToken) + + let userIdentityValue = userIdentity?.isEmpty == true ? nil : userIdentity + let connectionValue = connection?.isEmpty == true ? nil : connection + + myAccount.authenticationMethods.passkeyEnrollmentChallenge( + userIdentityId: userIdentityValue, + connection: connectionValue + ).start { result in + switch result { + case .success(let challenge): + let authParamsPublicKey: [String: Any] = [ + "rp": ["id": challenge.relyingPartyId], + "challenge": challenge.challengeData.base64URLEncodedString(), + "user": [ + "id": challenge.userId.base64URLEncodedString(), + "name": challenge.userName + ] + ] + let response: [String: Any] = [ + "authenticationMethodId": challenge.authenticationMethodId, + "authSession": challenge.authenticationSession, + "authParamsPublicKey": authParamsPublicKey + ] + resolve(response) + case .failure(let error): + self.rejectWithMyAccountError(reject: reject, error: error) + } + } + } + + @objc public func enrollPasskey(accessToken: String, authenticationMethodId: String, authSession: String, authResponse: String, authParamsPublicKey: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + guard #available(iOS 16.6, *) else { + reject("PASSKEYS_NOT_SUPPORTED", "Passkeys require iOS 16.6 or later", nil) + return + } + + let myAccount = createClient(accessToken: accessToken) + + guard let responseData = authResponse.data(using: .utf8), + let json = try? JSONSerialization.jsonObject(with: responseData) as? [String: Any], + let responseDict = json["response"] as? [String: Any], + let idString = json["id"] as? String, + let credentialID = Data(base64URLEncoded: idString), + let clientDataJSONString = responseDict["clientDataJSON"] as? String, + let clientDataJSON = Data(base64URLEncoded: clientDataJSONString) else { + reject("MY_ACCOUNT_ERROR", "Invalid authResponse JSON", nil) + return + } + + let attachmentString = json["authenticatorAttachment"] as? String ?? "platform" + let attachment: ASAuthorizationPublicKeyCredentialAttachment = attachmentString == "cross-platform" ? .crossPlatform : .platform + + let attestationObjectString = responseDict["attestationObject"] as? String + let attestationObject = attestationObjectString.flatMap { Data(base64URLEncoded: $0) } + + let passkey = BridgeSignupPasskey( + credentialID: credentialID, + attachment: attachment, + rawClientDataJSON: clientDataJSON, + rawAttestationObject: attestationObject + ) + + guard let authParamsData = authParamsPublicKey.data(using: .utf8), + let authParams = try? JSONSerialization.jsonObject(with: authParamsData) as? [String: Any] else { + reject("MY_ACCOUNT_ERROR", "Invalid authParamsPublicKey JSON", nil) + return + } + + let relyingPartyId = (authParams["rp"] as? [String: Any])?["id"] as? String ?? self.domain + var challengeData = Data() + var userId = Data() + var userName = "" + + if let challengeStr = authParams["challenge"] as? String, let data = Data(base64URLEncoded: challengeStr) { + challengeData = data + } + if let user = authParams["user"] as? [String: Any] { + if let userIdStr = user["id"] as? String, let data = Data(base64URLEncoded: userIdStr) { + userId = data + } + if let name = user["name"] as? String { + userName = name + } + } + + let challenge = PasskeyEnrollmentChallenge( + authenticationMethodId: authenticationMethodId, + authenticationSession: authSession, + relyingPartyId: relyingPartyId, + userId: userId, + userName: userName, + challengeData: challengeData + ) + + myAccount.authenticationMethods.enroll(passkey: passkey, challenge: challenge).start { result in + switch result { + case .success(let method): + let response: [String: Any] = [ + "id": method.id, + "type": method.type, + "userIdentityId": method.userIdentityId, + "userAgent": method.userAgent as Any, + "keyId": method.credential.id, + "publicKey": method.credential.publicKey.base64EncodedString(), + "userHandle": method.credential.userHandle.base64URLEncodedString(), + "credentialDeviceType": method.credential.deviceType.rawValue, + "credentialBackedUp": method.credential.isBackedUp, + "createdAt": ISO8601DateFormatter().string(from: method.createdAt), + "aaguid": method.aaguid, + "relyingPartyId": method.relyingPartyIdentifier + ] + resolve(response) + case .failure(let error): + self.rejectWithMyAccountError(reject: reject, error: error) + } + } + } + + @objc public func getAuthenticationMethods(accessToken: String, type: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + let myAccount = createClient(accessToken: accessToken) + + var methodType: AuthenticationMethodType? = nil + if let typeStr = type, !typeStr.isEmpty { + guard let parsed = AuthenticationMethodType(rawValue: typeStr) else { + reject("MY_ACCOUNT_ERROR", "Invalid authentication method type: \(typeStr)", nil) + return + } + methodType = parsed + } + + myAccount.authenticationMethods.getAuthenticationMethods(type: methodType).start { result in + switch result { + case .success(let methods): + let response = methods.map { self.authenticationMethodToDict($0) } + resolve(response) + case .failure(let error): + self.rejectWithMyAccountError(reject: reject, error: error) + } + } + } + + @objc public func getAuthenticationMethodById(accessToken: String, id: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + let myAccount = createClient(accessToken: accessToken) + + myAccount.authenticationMethods.getAuthenticationMethod(by: id).start { result in + switch result { + case .success(let method): + resolve(self.authenticationMethodToDict(method)) + case .failure(let error): + self.rejectWithMyAccountError(reject: reject, error: error) + } + } + } + + @objc public func updateAuthenticationMethodById(accessToken: String, id: String, name: String?, preferredAuthenticationMethod: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + let myAccount = createClient(accessToken: accessToken) + + let nameValue = name?.isEmpty == true ? nil : name + var preferredMethod: PreferredAuthenticationMethod? = nil + if let methodStr = preferredAuthenticationMethod, !methodStr.isEmpty { + guard let parsed = PreferredAuthenticationMethod(rawValue: methodStr) else { + reject("MY_ACCOUNT_ERROR", "Invalid preferred authentication method: \(methodStr)", nil) + return + } + preferredMethod = parsed + } + + myAccount.authenticationMethods.updateAuthenticationMethod(by: id, name: nameValue, preferredAuthenticationMethod: preferredMethod).start { result in + switch result { + case .success(let method): + resolve(self.authenticationMethodToDict(method)) + case .failure(let error): + self.rejectWithMyAccountError(reject: reject, error: error) + } + } + } + + @objc public func deleteAuthenticationMethodById(accessToken: String, id: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + let myAccount = createClient(accessToken: accessToken) + + myAccount.authenticationMethods.deleteAuthenticationMethod(by: id).start { result in + switch result { + case .success: + resolve(nil) + case .failure(let error): + self.rejectWithMyAccountError(reject: reject, error: error) + } + } + } + + // MARK: - Factor Enrollment + + @objc public func enrollPhone(accessToken: String, phoneNumber: String, preferredAuthenticationMethod: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + let myAccount = createClient(accessToken: accessToken) + + var preferredMethod: PreferredAuthenticationMethod? = nil + if let methodStr = preferredAuthenticationMethod, !methodStr.isEmpty { + guard let parsed = PreferredAuthenticationMethod(rawValue: methodStr) else { + reject("MY_ACCOUNT_ENROLLMENT_FAILED", "Invalid preferred authentication method: \(methodStr)", nil) + return + } + preferredMethod = parsed + } + + myAccount.authenticationMethods.enrollPhone(phoneNumber: phoneNumber, preferredAuthenticationMethod: preferredMethod).start { result in + switch result { + case .success(let challenge): + let response: [String: Any] = [ + "id": challenge.authenticationId, + "authSession": challenge.authenticationSession + ] + resolve(response) + case .failure(let error): + self.rejectWithMyAccountError(reject: reject, error: error) + } + } + } + + @objc public func enrollEmail(accessToken: String, emailAddress: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + let myAccount = createClient(accessToken: accessToken) + + myAccount.authenticationMethods.enrollEmail(emailAddress: emailAddress).start { result in + switch result { + case .success(let challenge): + let response: [String: Any] = [ + "id": challenge.authenticationId, + "authSession": challenge.authenticationSession + ] + resolve(response) + case .failure(let error): + self.rejectWithMyAccountError(reject: reject, error: error) + } + } + } + + @objc public func enrollTOTP(accessToken: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + let myAccount = createClient(accessToken: accessToken) + + myAccount.authenticationMethods.enrollTOTP().start { result in + switch result { + case .success(let challenge): + var response: [String: Any] = [ + "id": challenge.authenticationId, + "authSession": challenge.authenticationSession, + "barcodeUri": challenge.authenticatorQRCodeURI + ] + if let manualCode = challenge.authenticatorManualInputCode { + response["manualInputCode"] = manualCode + } + resolve(response) + case .failure(let error): + self.rejectWithMyAccountError(reject: reject, error: error) + } + } + } + + @objc public func enrollPushNotification(accessToken: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + let myAccount = createClient(accessToken: accessToken) + + myAccount.authenticationMethods.enrollPushNotification().start { result in + switch result { + case .success(let challenge): + var response: [String: Any] = [ + "id": challenge.authenticationId, + "authSession": challenge.authenticationSession, + "barcodeUri": challenge.authenticatorQRCodeURI + ] + if let manualCode = challenge.authenticatorManualInputCode { + response["manualInputCode"] = manualCode + } + resolve(response) + case .failure(let error): + self.rejectWithMyAccountError(reject: reject, error: error) + } + } + } + + @objc public func enrollRecoveryCode(accessToken: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + let myAccount = createClient(accessToken: accessToken) + + myAccount.authenticationMethods.enrollRecoveryCode().start { result in + switch result { + case .success(let challenge): + let response: [String: Any] = [ + "id": challenge.authenticationId, + "authSession": challenge.authenticationSession, + "recoveryCode": challenge.recoveryCode + ] + resolve(response) + case .failure(let error): + self.rejectWithMyAccountError(reject: reject, error: error) + } + } + } + + // MARK: - Enrollment Confirmation + + @objc public func confirmEnrollmentWithOtp(accessToken: String, id: String, authSession: String, otpCode: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + let myAccount = createClient(accessToken: accessToken) + + myAccount.authenticationMethods.confirmPhoneEnrollment(id: id, authSession: authSession, otpCode: otpCode).start { result in + switch result { + case .success(let method): + resolve(self.authenticationMethodToDict(method)) + case .failure(let error): + self.rejectWithMyAccountError(reject: reject, error: error) + } + } + } + + @objc public func confirmEnrollment(accessToken: String, id: String, authSession: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + let myAccount = createClient(accessToken: accessToken) + + myAccount.authenticationMethods.confirmRecoveryCodeEnrollment(id: id, authSession: authSession).start { result in + switch result { + case .success(let method): + resolve(self.authenticationMethodToDict(method)) + case .failure(let error): + self.rejectWithMyAccountError(reject: reject, error: error) + } + } + } + + // MARK: - Factors + + @objc public func getFactors(accessToken: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + let myAccount = createClient(accessToken: accessToken) + + myAccount.authenticationMethods.getFactors().start { result in + switch result { + case .success(let factors): + let response = factors.map { factor -> [String: Any?] in + return [ + "type": factor.type, + "usage": factor.usage + ] + } + resolve(response) + case .failure(let error): + self.rejectWithMyAccountError(reject: reject, error: error) + } + } + } + + // MARK: - Helpers + + private func authenticationMethodToDict(_ method: Auth0.AuthenticationMethod) -> [String: Any?] { + return [ + "id": method.id, + "type": method.type, + "createdAt": method.createdAt, + "usage": method.usage, + "confirmed": method.confirmed, + "name": method.name, + "keyId": method.keyId, + "publicKey": method.publicKey, + "userHandle": method.userHandle, + "credentialDeviceType": method.credentialDeviceType, + "credentialBackedUp": method.credentialBackedUp, + "userAgent": method.userAgent, + "identityUserId": method.identityUserId, + "transports": method.transports, + "phoneNumber": method.phoneNumber, + "preferredAuthenticationMethod": method.preferredAuthenticationMethod, + "lastPasswordReset": method.lastPasswordReset, + ] + } +} diff --git a/ios/NativeBridge.swift b/ios/NativeBridge.swift index 78266c46..efcc4cae 100644 --- a/ios/NativeBridge.swift +++ b/ios/NativeBridge.swift @@ -606,8 +606,6 @@ public class NativeBridge: NSObject { } } - - @objc public func getClientId() -> String { return clientId } diff --git a/src/Auth0.ts b/src/Auth0.ts index e0d96d42..d2826074 100644 --- a/src/Auth0.ts +++ b/src/Auth0.ts @@ -65,6 +65,19 @@ class Auth0 { return this.client.auth; } + /** + * Provides access to the My Account API for managing authentication methods. + * + * @example + * ```typescript + * const methods = await auth0.myAccount.getAuthenticationMethods({ accessToken }); + * await auth0.myAccount.deleteAuthenticationMethodById({ accessToken, id: 'auth_method_123' }); + * ``` + */ + get myAccount() { + return this.client.myAccount; + } + /** * Provides access to the Management API (e.g., for user patching). * @param token An access token with the required permissions for the management operations. diff --git a/src/core/interfaces/IAuth0Client.ts b/src/core/interfaces/IAuth0Client.ts index d246b7a8..02926370 100644 --- a/src/core/interfaces/IAuth0Client.ts +++ b/src/core/interfaces/IAuth0Client.ts @@ -1,6 +1,7 @@ import type { IWebAuthProvider } from './IWebAuthProvider'; import type { ICredentialsManager } from './ICredentialsManager'; import type { IAuthenticationProvider } from './IAuthenticationProvider'; +import type { IMyAccountClient } from './IMyAccountClient'; import type { IUsersClient } from './IUsersClient'; import type { DPoPHeadersParams, @@ -36,6 +37,11 @@ export interface IAuth0Client { */ readonly auth: IAuthenticationProvider; + /** + * Provides access to methods for interacting with the My Account API for managing authentication methods. + */ + readonly myAccount: IMyAccountClient; + /** * Creates a client for interacting with the Auth0 Management API's user endpoints. * diff --git a/src/core/interfaces/IMyAccountClient.ts b/src/core/interfaces/IMyAccountClient.ts new file mode 100644 index 00000000..dcbcab2a --- /dev/null +++ b/src/core/interfaces/IMyAccountClient.ts @@ -0,0 +1,98 @@ +import type { + PasskeyEnrollmentChallengeParameters, + PasskeyEnrollmentChallengeResponse, + EnrollPasskeyParameters, + PasskeyAuthenticationMethod, + GetAuthenticationMethodsParameters, + GetAuthenticationMethodByIdParameters, + UpdateAuthenticationMethodByIdParameters, + DeleteAuthenticationMethodByIdParameters, + AuthenticationMethod, + EnrollPhoneParameters, + EnrollEmailParameters, + EnrollTOTPParameters, + EnrollPushNotificationParameters, + EnrollRecoveryCodeParameters, + ConfirmOTPEnrollmentParameters, + ConfirmRecoveryCodeEnrollmentParameters, + ConfirmPushNotificationEnrollmentParameters, + GetFactorsParameters, + EnrollmentChallenge, + TOTPEnrollmentChallenge, + RecoveryCodeEnrollmentChallenge, + Factor, +} from '../../types'; + +export interface IMyAccountClient { + // --- Passkey Enrollment --- + + passkeyEnrollmentChallenge( + parameters: PasskeyEnrollmentChallengeParameters + ): Promise; + + enrollPasskey( + parameters: EnrollPasskeyParameters + ): Promise; + + // --- Factor Enrollment --- + + enrollPhone(parameters: EnrollPhoneParameters): Promise; + + enrollEmail(parameters: EnrollEmailParameters): Promise; + + enrollTOTP( + parameters: EnrollTOTPParameters + ): Promise; + + enrollPushNotification( + parameters: EnrollPushNotificationParameters + ): Promise; + + enrollRecoveryCode( + parameters: EnrollRecoveryCodeParameters + ): Promise; + + // --- Enrollment Confirmation --- + + confirmPhoneEnrollment( + parameters: ConfirmOTPEnrollmentParameters + ): Promise; + + confirmEmailEnrollment( + parameters: ConfirmOTPEnrollmentParameters + ): Promise; + + confirmTOTPEnrollment( + parameters: ConfirmOTPEnrollmentParameters + ): Promise; + + confirmPushNotificationEnrollment( + parameters: ConfirmPushNotificationEnrollmentParameters + ): Promise; + + confirmRecoveryCodeEnrollment( + parameters: ConfirmRecoveryCodeEnrollmentParameters + ): Promise; + + // --- Authentication Method Management --- + + getAuthenticationMethods( + parameters: GetAuthenticationMethodsParameters + ): Promise; + + getAuthenticationMethodById( + parameters: GetAuthenticationMethodByIdParameters + ): Promise; + + updateAuthenticationMethodById( + parameters: UpdateAuthenticationMethodByIdParameters + ): Promise; + + deleteAuthenticationMethodById( + parameters: DeleteAuthenticationMethodByIdParameters + ): Promise; + + // --- Factors --- + + getFactors(parameters: GetFactorsParameters): Promise; +} diff --git a/src/core/interfaces/index.ts b/src/core/interfaces/index.ts index 4cdbbd99..db2b1489 100644 --- a/src/core/interfaces/index.ts +++ b/src/core/interfaces/index.ts @@ -2,5 +2,6 @@ export * from './common'; export * from './IAuth0Client'; export * from './IAuthenticationProvider'; export * from './ICredentialsManager'; +export * from './IMyAccountClient'; export * from './IWebAuthProvider'; export * from './IUsersClient'; diff --git a/src/core/models/MyAccountError.ts b/src/core/models/MyAccountError.ts new file mode 100644 index 00000000..16f703cd --- /dev/null +++ b/src/core/models/MyAccountError.ts @@ -0,0 +1,52 @@ +import { AuthError } from './AuthError'; + +/** + * Represents an error from the My Account API, mirroring the properties + * exposed by the native Auth0 SDKs (Auth0.swift and Auth0.Android). + * + * @example + * ```typescript + * import { MyAccountError } from 'react-native-auth0'; + * + * try { + * await myAccount.enrollPhone({ accessToken, phoneNumber: '+1234567890' }); + * } catch (error) { + * if (error instanceof MyAccountError) { + * console.log(error.type); // e.g. "https://auth0.com/api-errors/A0E-401-0001" + * console.log(error.statusCode); // e.g. 401 + * console.log(error.title); // e.g. "Unauthorized" + * console.log(error.detail); // e.g. "The access token is invalid or has expired" + * } + * } + * ``` + */ +export class MyAccountError extends AuthError { + /** Error type URI from the API (e.g., "https://auth0.com/api-errors/A0E-401-0001") */ + public readonly type: string; + /** Human-readable error title (e.g., "Unauthorized", "Bad Request") */ + public readonly title: string; + /** Detailed error description from the API */ + public readonly detail: string; + /** HTTP status code of the error response */ + public readonly statusCode: number; + + constructor(originalError: AuthError) { + super(originalError.name, originalError.message, { + status: originalError.status, + code: originalError.code, + json: originalError.json, + }); + + let parsed: Record | undefined; + try { + parsed = JSON.parse(originalError.message); + } catch { + // message is not JSON — fall back to raw values + } + + this.type = (parsed?.type as string) ?? originalError.code; + this.title = (parsed?.title as string) ?? ''; + this.detail = (parsed?.detail as string) ?? originalError.message; + this.statusCode = (parsed?.statusCode as number) ?? originalError.status; + } +} diff --git a/src/core/models/index.ts b/src/core/models/index.ts index c88811fe..d7f67911 100644 --- a/src/core/models/index.ts +++ b/src/core/models/index.ts @@ -10,3 +10,4 @@ export { export { WebAuthError, WebAuthErrorCodes } from './WebAuthError'; export { DPoPError, DPoPErrorCodes } from './DPoPError'; export { PasskeyError, PasskeyErrorCodes } from './PasskeyError'; +export { MyAccountError } from './MyAccountError'; diff --git a/src/hooks/Auth0Context.ts b/src/hooks/Auth0Context.ts index bc32608f..a550a163 100644 --- a/src/hooks/Auth0Context.ts +++ b/src/hooks/Auth0Context.ts @@ -28,6 +28,7 @@ import type { DPoPHeadersParams, SessionTransferCredentials, } from '../types'; +import type { IMyAccountClient } from '../core/interfaces'; import type { ApiCredentials } from '../core/models'; import type { NativeAuthorizeOptions, @@ -259,6 +260,17 @@ export interface Auth0ContextInterface extends AuthState { parameters: GetTokenByPasskeyParameters ) => Promise; + /** + * Provides access to the My Account API for managing authentication methods. + * + * @example + * ```typescript + * const { myAccount } = useAuth0(); + * const methods = await myAccount.getAuthenticationMethods({ accessToken }); + * ``` + */ + myAccount: IMyAccountClient; + /** * Sends a verification code to the user's email. * @param parameters The parameters for sending the email code. @@ -451,6 +463,25 @@ const initialContext: Auth0ContextInterface = { passkeySignupChallenge: stub, passkeyLoginChallenge: stub, getTokenByPasskey: stub, + myAccount: { + passkeyEnrollmentChallenge: stub, + enrollPasskey: stub, + enrollPhone: stub, + enrollEmail: stub, + enrollTOTP: stub, + enrollPushNotification: stub, + enrollRecoveryCode: stub, + confirmPhoneEnrollment: stub, + confirmEmailEnrollment: stub, + confirmTOTPEnrollment: stub, + confirmPushNotificationEnrollment: stub, + confirmRecoveryCodeEnrollment: stub, + getAuthenticationMethods: stub, + getAuthenticationMethodById: stub, + updateAuthenticationMethodById: stub, + deleteAuthenticationMethodById: stub, + getFactors: stub, + }, sendEmailCode: stub, sendSMSCode: stub, authorizeWithEmail: stub, diff --git a/src/hooks/Auth0Provider.tsx b/src/hooks/Auth0Provider.tsx index 8babcfb3..313d7c9e 100644 --- a/src/hooks/Auth0Provider.tsx +++ b/src/hooks/Auth0Provider.tsx @@ -1,4 +1,4 @@ -import { useEffect, useReducer, useMemo, useCallback } from 'react'; +import { useEffect, useReducer, useCallback, useMemo } from 'react'; import type { PropsWithChildren } from 'react'; import type { ApiCredentials } from '../core/models'; import { Auth0Context, type Auth0ContextInterface } from './Auth0Context'; @@ -342,6 +342,8 @@ export const Auth0Provider = ({ [client, loginFlow] ); + const myAccount = useMemo(() => client.myAccount, [client]); + const sendEmailCode = useCallback( (parameters: PasswordlessEmailParameters) => voidFlow(client.auth.passwordlessWithEmail(parameters)), @@ -454,6 +456,7 @@ export const Auth0Provider = ({ passkeySignupChallenge, passkeyLoginChallenge, getTokenByPasskey, + myAccount, sendEmailCode, authorizeWithEmail, sendSMSCode, @@ -487,6 +490,7 @@ export const Auth0Provider = ({ passkeySignupChallenge, passkeyLoginChallenge, getTokenByPasskey, + myAccount, sendEmailCode, authorizeWithEmail, sendSMSCode, diff --git a/src/index.ts b/src/index.ts index 8cd12c56..ba44e035 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ export { DPoPErrorCodes, PasskeyError, PasskeyErrorCodes, + MyAccountError, } from './core/models'; export { TimeoutError } from './core/utils/fetchWithTimeout'; export { TokenType } from './types/common'; diff --git a/src/platforms/native/adapters/NativeAuth0Client.ts b/src/platforms/native/adapters/NativeAuth0Client.ts index d8559260..a89479c7 100644 --- a/src/platforms/native/adapters/NativeAuth0Client.ts +++ b/src/platforms/native/adapters/NativeAuth0Client.ts @@ -1,6 +1,7 @@ import type { IAuth0Client, IAuthenticationProvider, + IMyAccountClient, IUsersClient, } from '../../../core/interfaces'; import type { NativeAuth0Options } from '../../../types/platform-specific'; @@ -15,6 +16,7 @@ import type { } from '../../../types'; import { NativeWebAuthProvider } from './NativeWebAuthProvider'; import { NativeCredentialsManager } from './NativeCredentialsManager'; +import { NativeMyAccountClient } from './NativeMyAccountClient'; import { type INativeBridge, NativeBridgeManager } from '../bridge'; import { AuthenticationOrchestrator, @@ -80,6 +82,7 @@ export class NativeAuth0Client implements IAuth0Client { this.webAuth = new NativeWebAuthProvider(guardedBridge, options.domain); this.credentialsManager = new NativeCredentialsManager(guardedBridge); + this.myAccount = new NativeMyAccountClient(guardedBridge); } private async initialize( @@ -123,6 +126,8 @@ export class NativeAuth0Client implements IAuth0Client { }); } + readonly myAccount: IMyAccountClient; + async getDPoPHeaders( params: DPoPHeadersParams ): Promise> { diff --git a/src/platforms/native/adapters/NativeMyAccountClient.ts b/src/platforms/native/adapters/NativeMyAccountClient.ts new file mode 100644 index 00000000..2a202fa1 --- /dev/null +++ b/src/platforms/native/adapters/NativeMyAccountClient.ts @@ -0,0 +1,345 @@ +import type { IMyAccountClient } from '../../../core/interfaces'; +import type { + PasskeyEnrollmentChallengeParameters, + PasskeyEnrollmentChallengeResponse, + EnrollPasskeyParameters, + PasskeyAuthenticationMethod, + GetAuthenticationMethodsParameters, + GetAuthenticationMethodByIdParameters, + UpdateAuthenticationMethodByIdParameters, + DeleteAuthenticationMethodByIdParameters, + AuthenticationMethod, + EnrollPhoneParameters, + EnrollEmailParameters, + EnrollTOTPParameters, + EnrollPushNotificationParameters, + EnrollRecoveryCodeParameters, + ConfirmOTPEnrollmentParameters, + ConfirmRecoveryCodeEnrollmentParameters, + ConfirmPushNotificationEnrollmentParameters, + GetFactorsParameters, + EnrollmentChallenge, + TOTPEnrollmentChallenge, + RecoveryCodeEnrollmentChallenge, + Factor, +} from '../../../types'; +import type { INativeBridge } from '../bridge'; +import { AuthError, PasskeyError, MyAccountError } from '../../../core/models'; + +export class NativeMyAccountClient implements IMyAccountClient { + private readonly bridge: INativeBridge; + + constructor(bridge: INativeBridge) { + this.bridge = bridge; + } + + // --- Passkey Enrollment (uses PasskeyError) --- + + async passkeyEnrollmentChallenge( + parameters: PasskeyEnrollmentChallengeParameters + ): Promise { + const { accessToken, userIdentity, connection } = parameters; + try { + return await this.bridge.passkeyEnrollmentChallenge( + accessToken, + userIdentity || undefined, + connection || undefined + ); + } catch (e) { + if (e instanceof AuthError) { + throw new PasskeyError(e); + } + throw e; + } + } + + async enrollPasskey( + parameters: EnrollPasskeyParameters + ): Promise { + const { + accessToken, + authenticationMethodId, + authSession, + authResponse, + authParamsPublicKey, + } = parameters; + try { + return (await this.bridge.enrollPasskey( + accessToken, + authenticationMethodId, + authSession, + authResponse, + JSON.stringify(authParamsPublicKey) + )) as PasskeyAuthenticationMethod; + } catch (e) { + if (e instanceof AuthError) { + throw new PasskeyError(e); + } + throw e; + } + } + + // --- Factor Enrollment (uses MyAccountError) --- + + async enrollPhone( + parameters: EnrollPhoneParameters + ): Promise { + const { accessToken, phoneNumber, preferredAuthenticationMethod } = + parameters; + try { + return (await this.bridge.enrollPhone( + accessToken, + phoneNumber, + preferredAuthenticationMethod || undefined + )) as EnrollmentChallenge; + } catch (e) { + if (e instanceof AuthError) { + throw new MyAccountError(e); + } + throw e; + } + } + + async enrollEmail( + parameters: EnrollEmailParameters + ): Promise { + const { accessToken, emailAddress } = parameters; + try { + return (await this.bridge.enrollEmail( + accessToken, + emailAddress + )) as EnrollmentChallenge; + } catch (e) { + if (e instanceof AuthError) { + throw new MyAccountError(e); + } + throw e; + } + } + + async enrollTOTP( + parameters: EnrollTOTPParameters + ): Promise { + const { accessToken } = parameters; + try { + return (await this.bridge.enrollTOTP( + accessToken + )) as TOTPEnrollmentChallenge; + } catch (e) { + if (e instanceof AuthError) { + throw new MyAccountError(e); + } + throw e; + } + } + + async enrollPushNotification( + parameters: EnrollPushNotificationParameters + ): Promise { + const { accessToken } = parameters; + try { + return (await this.bridge.enrollPushNotification( + accessToken + )) as TOTPEnrollmentChallenge; + } catch (e) { + if (e instanceof AuthError) { + throw new MyAccountError(e); + } + throw e; + } + } + + async enrollRecoveryCode( + parameters: EnrollRecoveryCodeParameters + ): Promise { + const { accessToken } = parameters; + try { + return (await this.bridge.enrollRecoveryCode( + accessToken + )) as RecoveryCodeEnrollmentChallenge; + } catch (e) { + if (e instanceof AuthError) { + throw new MyAccountError(e); + } + throw e; + } + } + + // --- Enrollment Confirmation (uses MyAccountError) --- + + async confirmPhoneEnrollment( + parameters: ConfirmOTPEnrollmentParameters + ): Promise { + const { accessToken, id, authSession, otpCode } = parameters; + try { + return (await this.bridge.confirmEnrollmentWithOtp( + accessToken, + id, + authSession, + otpCode + )) as AuthenticationMethod; + } catch (e) { + if (e instanceof AuthError) { + throw new MyAccountError(e); + } + throw e; + } + } + + async confirmEmailEnrollment( + parameters: ConfirmOTPEnrollmentParameters + ): Promise { + const { accessToken, id, authSession, otpCode } = parameters; + try { + return (await this.bridge.confirmEnrollmentWithOtp( + accessToken, + id, + authSession, + otpCode + )) as AuthenticationMethod; + } catch (e) { + if (e instanceof AuthError) { + throw new MyAccountError(e); + } + throw e; + } + } + + async confirmTOTPEnrollment( + parameters: ConfirmOTPEnrollmentParameters + ): Promise { + const { accessToken, id, authSession, otpCode } = parameters; + try { + return (await this.bridge.confirmEnrollmentWithOtp( + accessToken, + id, + authSession, + otpCode + )) as AuthenticationMethod; + } catch (e) { + if (e instanceof AuthError) { + throw new MyAccountError(e); + } + throw e; + } + } + + async confirmPushNotificationEnrollment( + parameters: ConfirmPushNotificationEnrollmentParameters + ): Promise { + const { accessToken, id, authSession } = parameters; + try { + return (await this.bridge.confirmEnrollment( + accessToken, + id, + authSession + )) as AuthenticationMethod; + } catch (e) { + if (e instanceof AuthError) { + throw new MyAccountError(e); + } + throw e; + } + } + + async confirmRecoveryCodeEnrollment( + parameters: ConfirmRecoveryCodeEnrollmentParameters + ): Promise { + const { accessToken, id, authSession } = parameters; + try { + return (await this.bridge.confirmEnrollment( + accessToken, + id, + authSession + )) as AuthenticationMethod; + } catch (e) { + if (e instanceof AuthError) { + throw new MyAccountError(e); + } + throw e; + } + } + + // --- Authentication Method Management (uses MyAccountError) --- + + async getAuthenticationMethods( + parameters: GetAuthenticationMethodsParameters + ): Promise { + const { accessToken, type } = parameters; + try { + return (await this.bridge.getAuthenticationMethods( + accessToken, + type || undefined + )) as AuthenticationMethod[]; + } catch (e) { + if (e instanceof AuthError) { + throw new MyAccountError(e); + } + throw e; + } + } + + async getAuthenticationMethodById( + parameters: GetAuthenticationMethodByIdParameters + ): Promise { + const { accessToken, id } = parameters; + try { + return (await this.bridge.getAuthenticationMethodById( + accessToken, + id + )) as AuthenticationMethod; + } catch (e) { + if (e instanceof AuthError) { + throw new MyAccountError(e); + } + throw e; + } + } + + async updateAuthenticationMethodById( + parameters: UpdateAuthenticationMethodByIdParameters + ): Promise { + const { accessToken, id, name, preferredAuthenticationMethod } = parameters; + try { + return (await this.bridge.updateAuthenticationMethodById( + accessToken, + id, + name || null, + preferredAuthenticationMethod || null + )) as AuthenticationMethod; + } catch (e) { + if (e instanceof AuthError) { + throw new MyAccountError(e); + } + throw e; + } + } + + async deleteAuthenticationMethodById( + parameters: DeleteAuthenticationMethodByIdParameters + ): Promise { + const { accessToken, id } = parameters; + try { + await this.bridge.deleteAuthenticationMethodById(accessToken, id); + } catch (e) { + if (e instanceof AuthError) { + throw new MyAccountError(e); + } + throw e; + } + } + + // --- Factors (uses MyAccountError) --- + + async getFactors(parameters: GetFactorsParameters): Promise { + const { accessToken } = parameters; + try { + return (await this.bridge.getFactors(accessToken)) as Factor[]; + } catch (e) { + if (e instanceof AuthError) { + throw new MyAccountError(e); + } + throw e; + } + } +} diff --git a/src/platforms/native/adapters/index.ts b/src/platforms/native/adapters/index.ts index 3df27790..d25e3446 100644 --- a/src/platforms/native/adapters/index.ts +++ b/src/platforms/native/adapters/index.ts @@ -1,3 +1,4 @@ export * from './NativeAuth0Client'; export * from './NativeWebAuthProvider'; export * from './NativeCredentialsManager'; +export * from './NativeMyAccountClient'; diff --git a/src/platforms/native/bridge/INativeBridge.ts b/src/platforms/native/bridge/INativeBridge.ts index 33050fd0..9661e7da 100644 --- a/src/platforms/native/bridge/INativeBridge.ts +++ b/src/platforms/native/bridge/INativeBridge.ts @@ -272,4 +272,129 @@ export interface INativeBridge { scope?: string, organization?: string ): Promise; + + /** + * Request a passkey enrollment challenge from the My Account API. + * + * @param accessToken Access token for My Account API. + * @param userIdentity Optional user identity ID (for linked accounts). + * @param connection Optional database connection name. + * @returns A promise that resolves with the enrollment challenge response. + */ + passkeyEnrollmentChallenge( + accessToken: string, + userIdentity?: string, + connection?: string + ): Promise<{ + authenticationMethodId: string; + authSession: string; + authParamsPublicKey: Record; + }>; + + /** + * Verify a passkey enrollment with the My Account API. + * + * @param accessToken Access token for My Account API. + * @param authenticationMethodId The authentication method ID from the challenge. + * @param authSession The auth session from the challenge. + * @param authResponse JSON string of the PublicKeyCredential response. + * @returns A promise that resolves with the enrolled authentication method. + */ + enrollPasskey( + accessToken: string, + authenticationMethodId: string, + authSession: string, + authResponse: string, + authParamsPublicKey: string + ): Promise>; + + /** + * Get all authentication methods for the authenticated user. + */ + getAuthenticationMethods( + accessToken: string, + type?: string + ): Promise[]>; + + /** + * Get a single authentication method by ID. + */ + getAuthenticationMethodById( + accessToken: string, + id: string + ): Promise>; + + /** + * Update an authentication method by ID. + */ + updateAuthenticationMethodById( + accessToken: string, + id: string, + name?: string | null, + preferredAuthenticationMethod?: string | null + ): Promise>; + + /** + * Delete an authentication method by ID. + */ + deleteAuthenticationMethodById( + accessToken: string, + id: string + ): Promise; + + /** + * Enroll a phone number as an authentication method. + */ + enrollPhone( + accessToken: string, + phoneNumber: string, + preferredAuthenticationMethod?: string + ): Promise>; + + /** + * Enroll an email address as an authentication method. + */ + enrollEmail( + accessToken: string, + emailAddress: string + ): Promise>; + + /** + * Enroll TOTP as an authentication method. + */ + enrollTOTP(accessToken: string): Promise>; + + /** + * Enroll push notification as an authentication method. + */ + enrollPushNotification(accessToken: string): Promise>; + + /** + * Enroll a recovery code as an authentication method. + */ + enrollRecoveryCode(accessToken: string): Promise>; + + /** + * Confirm an enrollment that requires an OTP code (phone, email, TOTP). + */ + confirmEnrollmentWithOtp( + accessToken: string, + id: string, + authSession: string, + otpCode: string + ): Promise>; + + /** + * Confirm an enrollment that does not require an OTP code (recovery code, push notification). + */ + confirmEnrollment( + accessToken: string, + id: string, + authSession: string + ): Promise>; + + /** + * Get available authentication factors. + */ + getFactors(accessToken: string): Promise[]>; } diff --git a/src/platforms/native/bridge/NativeBridgeManager.ts b/src/platforms/native/bridge/NativeBridgeManager.ts index b56355e2..d10a4c77 100644 --- a/src/platforms/native/bridge/NativeBridgeManager.ts +++ b/src/platforms/native/bridge/NativeBridgeManager.ts @@ -314,4 +314,168 @@ export class NativeBridgeManager implements INativeBridge { ); return new CredentialsModel(credential); } + + async passkeyEnrollmentChallenge( + accessToken: string, + userIdentity?: string, + connection?: string + ): Promise<{ + authenticationMethodId: string; + authSession: string; + authParamsPublicKey: Record; + }> { + return this.a0_call( + Auth0NativeModule.passkeyEnrollmentChallenge.bind(Auth0NativeModule), + accessToken, + userIdentity, + connection + ); + } + + async enrollPasskey( + accessToken: string, + authenticationMethodId: string, + authSession: string, + authResponse: string, + authParamsPublicKey: string + ): Promise> { + return this.a0_call( + Auth0NativeModule.enrollPasskey.bind(Auth0NativeModule), + accessToken, + authenticationMethodId, + authSession, + authResponse, + authParamsPublicKey + ); + } + + async getAuthenticationMethods( + accessToken: string, + type?: string + ): Promise[]> { + return this.a0_call( + Auth0NativeModule.getAuthenticationMethods.bind(Auth0NativeModule), + accessToken, + type + ); + } + + async getAuthenticationMethodById( + accessToken: string, + id: string + ): Promise> { + return this.a0_call( + Auth0NativeModule.getAuthenticationMethodById.bind(Auth0NativeModule), + accessToken, + id + ); + } + + async updateAuthenticationMethodById( + accessToken: string, + id: string, + name?: string | null, + preferredAuthenticationMethod?: string | null + ): Promise> { + return this.a0_call( + Auth0NativeModule.updateAuthenticationMethodById.bind(Auth0NativeModule), + accessToken, + id, + name, + preferredAuthenticationMethod + ); + } + + async deleteAuthenticationMethodById( + accessToken: string, + id: string + ): Promise { + return this.a0_call( + Auth0NativeModule.deleteAuthenticationMethodById.bind(Auth0NativeModule), + accessToken, + id + ); + } + + async enrollPhone( + accessToken: string, + phoneNumber: string, + preferredAuthenticationMethod?: string + ): Promise> { + return this.a0_call( + Auth0NativeModule.enrollPhone.bind(Auth0NativeModule), + accessToken, + phoneNumber, + preferredAuthenticationMethod + ); + } + + async enrollEmail( + accessToken: string, + emailAddress: string + ): Promise> { + return this.a0_call( + Auth0NativeModule.enrollEmail.bind(Auth0NativeModule), + accessToken, + emailAddress + ); + } + + async enrollTOTP(accessToken: string): Promise> { + return this.a0_call( + Auth0NativeModule.enrollTOTP.bind(Auth0NativeModule), + accessToken + ); + } + + async enrollPushNotification( + accessToken: string + ): Promise> { + return this.a0_call( + Auth0NativeModule.enrollPushNotification.bind(Auth0NativeModule), + accessToken + ); + } + + async enrollRecoveryCode(accessToken: string): Promise> { + return this.a0_call( + Auth0NativeModule.enrollRecoveryCode.bind(Auth0NativeModule), + accessToken + ); + } + + async confirmEnrollmentWithOtp( + accessToken: string, + id: string, + authSession: string, + otpCode: string + ): Promise> { + return this.a0_call( + Auth0NativeModule.confirmEnrollmentWithOtp.bind(Auth0NativeModule), + accessToken, + id, + authSession, + otpCode + ); + } + + async confirmEnrollment( + accessToken: string, + id: string, + authSession: string + ): Promise> { + return this.a0_call( + Auth0NativeModule.confirmEnrollment.bind(Auth0NativeModule), + accessToken, + id, + authSession + ); + } + + async getFactors(accessToken: string): Promise[]> { + return this.a0_call( + Auth0NativeModule.getFactors.bind(Auth0NativeModule), + accessToken + ); + } } diff --git a/src/platforms/web/adapters/WebAuth0Client.ts b/src/platforms/web/adapters/WebAuth0Client.ts index b7acbbd8..5b75199c 100644 --- a/src/platforms/web/adapters/WebAuth0Client.ts +++ b/src/platforms/web/adapters/WebAuth0Client.ts @@ -6,6 +6,7 @@ import { import type { IAuth0Client, IAuthenticationProvider, + IMyAccountClient, IUsersClient, } from '../../../core/interfaces'; import type { WebAuth0Options } from '../../../types/platform-specific'; @@ -20,6 +21,7 @@ import type { } from '../../../types'; import { WebWebAuthProvider } from './WebWebAuthProvider'; import { WebCredentialsManager } from './WebCredentialsManager'; +import { WebMyAccountClient } from './WebMyAccountClient'; import { ssoExchangeNotSupported } from './WebAuthenticationProvider'; import { AuthenticationOrchestrator, @@ -143,6 +145,8 @@ export class WebAuth0Client implements IAuth0Client { }); } + readonly myAccount: IMyAccountClient = new WebMyAccountClient(); + public async logout(options?: LogoutOptions): Promise { // If a logout process has already started, do nothing. if (this.logoutInProgress) { diff --git a/src/platforms/web/adapters/WebMyAccountClient.ts b/src/platforms/web/adapters/WebMyAccountClient.ts new file mode 100644 index 00000000..4ecdcc1d --- /dev/null +++ b/src/platforms/web/adapters/WebMyAccountClient.ts @@ -0,0 +1,213 @@ +import type { IMyAccountClient } from '../../../core/interfaces'; +import type { + PasskeyEnrollmentChallengeParameters, + PasskeyEnrollmentChallengeResponse, + EnrollPasskeyParameters, + PasskeyAuthenticationMethod, + GetAuthenticationMethodsParameters, + GetAuthenticationMethodByIdParameters, + UpdateAuthenticationMethodByIdParameters, + DeleteAuthenticationMethodByIdParameters, + AuthenticationMethod, + EnrollPhoneParameters, + EnrollEmailParameters, + EnrollTOTPParameters, + EnrollPushNotificationParameters, + EnrollRecoveryCodeParameters, + ConfirmOTPEnrollmentParameters, + ConfirmRecoveryCodeEnrollmentParameters, + ConfirmPushNotificationEnrollmentParameters, + GetFactorsParameters, + EnrollmentChallenge, + TOTPEnrollmentChallenge, + RecoveryCodeEnrollmentChallenge, + Factor, +} from '../../../types'; +import { AuthError, PasskeyError, MyAccountError } from '../../../core/models'; + +export class WebMyAccountClient implements IMyAccountClient { + async passkeyEnrollmentChallenge( + _parameters: PasskeyEnrollmentChallengeParameters + ): Promise { + throw new PasskeyError( + new AuthError( + 'UnsupportedOperation', + 'My Account API is not supported on the web platform' + ) + ); + } + + async enrollPasskey( + _parameters: EnrollPasskeyParameters + ): Promise { + throw new PasskeyError( + new AuthError( + 'UnsupportedOperation', + 'My Account API is not supported on the web platform' + ) + ); + } + + async enrollPhone( + _parameters: EnrollPhoneParameters + ): Promise { + throw new MyAccountError( + new AuthError( + 'UnsupportedOperation', + 'My Account API is not supported on the web platform' + ) + ); + } + + async enrollEmail( + _parameters: EnrollEmailParameters + ): Promise { + throw new MyAccountError( + new AuthError( + 'UnsupportedOperation', + 'My Account API is not supported on the web platform' + ) + ); + } + + async enrollTOTP( + _parameters: EnrollTOTPParameters + ): Promise { + throw new MyAccountError( + new AuthError( + 'UnsupportedOperation', + 'My Account API is not supported on the web platform' + ) + ); + } + + async enrollPushNotification( + _parameters: EnrollPushNotificationParameters + ): Promise { + throw new MyAccountError( + new AuthError( + 'UnsupportedOperation', + 'My Account API is not supported on the web platform' + ) + ); + } + + async enrollRecoveryCode( + _parameters: EnrollRecoveryCodeParameters + ): Promise { + throw new MyAccountError( + new AuthError( + 'UnsupportedOperation', + 'My Account API is not supported on the web platform' + ) + ); + } + + async confirmPhoneEnrollment( + _parameters: ConfirmOTPEnrollmentParameters + ): Promise { + throw new MyAccountError( + new AuthError( + 'UnsupportedOperation', + 'My Account API is not supported on the web platform' + ) + ); + } + + async confirmEmailEnrollment( + _parameters: ConfirmOTPEnrollmentParameters + ): Promise { + throw new MyAccountError( + new AuthError( + 'UnsupportedOperation', + 'My Account API is not supported on the web platform' + ) + ); + } + + async confirmTOTPEnrollment( + _parameters: ConfirmOTPEnrollmentParameters + ): Promise { + throw new MyAccountError( + new AuthError( + 'UnsupportedOperation', + 'My Account API is not supported on the web platform' + ) + ); + } + + async confirmPushNotificationEnrollment( + _parameters: ConfirmPushNotificationEnrollmentParameters + ): Promise { + throw new MyAccountError( + new AuthError( + 'UnsupportedOperation', + 'My Account API is not supported on the web platform' + ) + ); + } + + async confirmRecoveryCodeEnrollment( + _parameters: ConfirmRecoveryCodeEnrollmentParameters + ): Promise { + throw new MyAccountError( + new AuthError( + 'UnsupportedOperation', + 'My Account API is not supported on the web platform' + ) + ); + } + + async getAuthenticationMethods( + _parameters: GetAuthenticationMethodsParameters + ): Promise { + throw new MyAccountError( + new AuthError( + 'UnsupportedOperation', + 'My Account API is not supported on the web platform' + ) + ); + } + + async getAuthenticationMethodById( + _parameters: GetAuthenticationMethodByIdParameters + ): Promise { + throw new MyAccountError( + new AuthError( + 'UnsupportedOperation', + 'My Account API is not supported on the web platform' + ) + ); + } + + async updateAuthenticationMethodById( + _parameters: UpdateAuthenticationMethodByIdParameters + ): Promise { + throw new MyAccountError( + new AuthError( + 'UnsupportedOperation', + 'My Account API is not supported on the web platform' + ) + ); + } + + async deleteAuthenticationMethodById( + _parameters: DeleteAuthenticationMethodByIdParameters + ): Promise { + throw new MyAccountError( + new AuthError( + 'UnsupportedOperation', + 'My Account API is not supported on the web platform' + ) + ); + } + + async getFactors(_parameters: GetFactorsParameters): Promise { + throw new MyAccountError( + new AuthError( + 'UnsupportedOperation', + 'My Account API is not supported on the web platform' + ) + ); + } +} diff --git a/src/platforms/web/adapters/index.ts b/src/platforms/web/adapters/index.ts index 1c697be4..e0f07b37 100644 --- a/src/platforms/web/adapters/index.ts +++ b/src/platforms/web/adapters/index.ts @@ -1,3 +1,4 @@ export * from './WebAuth0Client'; export * from './WebWebAuthProvider'; export * from './WebCredentialsManager'; +export * from './WebMyAccountClient'; diff --git a/src/specs/NativeA0Auth0.ts b/src/specs/NativeA0Auth0.ts index 4ffeb7c9..8e04a6e1 100644 --- a/src/specs/NativeA0Auth0.ts +++ b/src/specs/NativeA0Auth0.ts @@ -196,6 +196,114 @@ export interface Spec extends TurboModule { scope: string | undefined, organization: string | undefined ): Promise; + + /** + * Request a passkey enrollment challenge from the My Account API. + */ + passkeyEnrollmentChallenge( + accessToken: string, + userIdentity: string | undefined, + connection: string | undefined + ): Promise<{ + authenticationMethodId: string; + authSession: string; + authParamsPublicKey: Object; + }>; + + /** + * Verify a passkey enrollment with the My Account API. + */ + enrollPasskey( + accessToken: string, + authenticationMethodId: string, + authSession: string, + authResponse: string, + authParamsPublicKey: string + ): Promise; + + /** + * Get all authentication methods for the authenticated user. + */ + getAuthenticationMethods( + accessToken: string, + type: string | undefined + ): Promise; + + /** + * Get a single authentication method by ID. + */ + getAuthenticationMethodById(accessToken: string, id: string): Promise; + + /** + * Update an authentication method by ID. + */ + updateAuthenticationMethodById( + accessToken: string, + id: string, + name: string | null | undefined, + preferredAuthenticationMethod: string | null | undefined + ): Promise; + + /** + * Delete an authentication method by ID. + */ + deleteAuthenticationMethodById( + accessToken: string, + id: string + ): Promise; + + /** + * Enroll a phone number as an authentication method. + */ + enrollPhone( + accessToken: string, + phoneNumber: string, + preferredAuthenticationMethod: string | undefined + ): Promise; + + /** + * Enroll an email address as an authentication method. + */ + enrollEmail(accessToken: string, emailAddress: string): Promise; + + /** + * Enroll TOTP as an authentication method. + */ + enrollTOTP(accessToken: string): Promise; + + /** + * Enroll push notification as an authentication method. + */ + enrollPushNotification(accessToken: string): Promise; + + /** + * Enroll a recovery code as an authentication method. + */ + enrollRecoveryCode(accessToken: string): Promise; + + /** + * Confirm an enrollment that requires an OTP code. + */ + confirmEnrollmentWithOtp( + accessToken: string, + id: string, + authSession: string, + otpCode: string + ): Promise; + + /** + * Confirm an enrollment without an OTP code (recovery code, push notification). + */ + confirmEnrollment( + accessToken: string, + id: string, + authSession: string + ): Promise; + + /** + * Get available authentication factors. + */ + getFactors(accessToken: string): Promise; } export default TurboModuleRegistry.getEnforcing('A0Auth0'); diff --git a/src/types/parameters.ts b/src/types/parameters.ts index 7c86817f..78b5531e 100644 --- a/src/types/parameters.ts +++ b/src/types/parameters.ts @@ -402,6 +402,398 @@ export interface GetTokenByPasskeyParameters { organization?: string; } +// ========= Passkey Enrollment Parameters ========= + +/** + * Parameters for requesting a passkey enrollment challenge from the My Account API. + * + * Passkey enrollment adds a passkey to an already-authenticated user's account. + * Requires an access token with audience `https://{domain}/me/v1` and + * scope `create:me:authentication_methods`. + * + * @see https://auth0.com/docs/authenticate/database-connections/passkeys + */ +export interface PasskeyEnrollmentChallengeParameters { + /** Access token with My Account API scopes. */ + accessToken: string; + /** Unique identifier of user's identity (for linked accounts). */ + userIdentity?: string; + /** Database connection name. */ + connection?: string; +} + +/** + * Response from a passkey enrollment challenge request. + * Contains the authentication method ID, auth session, and WebAuthn creation options. + */ +export interface PasskeyEnrollmentChallengeResponse { + /** Authentication method ID (needed for the verify step). */ + authenticationMethodId: string; + /** Auth session identifier for the verify step. */ + authSession: string; + /** WebAuthn PublicKeyCredentialCreationOptions for the platform credential manager. */ + authParamsPublicKey: Record; +} + +/** + * Parameters for verifying a passkey enrollment with the My Account API. + * + * After the platform credential manager returns the credential registration, + * pass the auth session and the credential JSON response to this method + * to complete the enrollment. + */ +export interface EnrollPasskeyParameters { + /** Access token with My Account API scopes. */ + accessToken: string; + /** Authentication method ID from the challenge response. */ + authenticationMethodId: string; + /** Auth session from the challenge response. */ + authSession: string; + /** JSON string of the PublicKeyCredential response from the platform credential manager. */ + authResponse: string; + /** The public key parameters from the enrollment challenge response. */ + authParamsPublicKey: Record; +} + +/** + * Represents an enrolled passkey authentication method. + * Returned after successful enrollment verification. + */ +export interface PasskeyAuthenticationMethod { + /** Authentication method ID. */ + id: string; + /** Type (always "passkey"). */ + type: string; + /** User identity ID. */ + userIdentityId: string; + /** User agent of the device used to enroll. */ + userAgent?: string; + /** Credential key ID. */ + keyId: string; + /** Public key (base64-encoded). */ + publicKey: string; + /** User handle (base64url-encoded). */ + userHandle: string; + /** Device type: "single_device" or "multi_device". */ + credentialDeviceType: string; + /** Whether the credential is backed up. */ + credentialBackedUp: boolean; + /** Creation timestamp (ISO 8601). */ + createdAt: string; + /** Authenticator Attestation GUID. */ + aaguid: string; + /** Relying party identifier. */ + relyingPartyId: string; +} + +// ========= My Account — Authentication Method Management ========= + +/** + * Supported authentication method types for filtering. + */ +export type AuthenticationMethodType = + | 'passkey' + | 'phone' + | 'email' + | 'totp' + | 'push-notification' + | 'recovery-code' + | 'webauthn-platform' + | 'webauthn-roaming' + | 'password'; + +/** + * Runtime constants for authentication method types. + * + * @example + * ```typescript + * import { AuthenticationMethodTypes } from 'react-native-auth0'; + * + * const methods = await myAccount.getAuthenticationMethods({ + * accessToken, + * type: AuthenticationMethodTypes.PASSKEY, + * }); + * ``` + */ +export const AuthenticationMethodTypes = { + PASSKEY: 'passkey', + PHONE: 'phone', + EMAIL: 'email', + TOTP: 'totp', + PUSH_NOTIFICATION: 'push-notification', + RECOVERY_CODE: 'recovery-code', + WEBAUTHN_PLATFORM: 'webauthn-platform', + WEBAUTHN_ROAMING: 'webauthn-roaming', + PASSWORD: 'password', +} as const; + +/** + * Runtime constants for preferred phone authentication methods. + * + * @example + * ```typescript + * import { PreferredAuthenticationMethods } from 'react-native-auth0'; + * + * await myAccount.enrollPhone({ + * accessToken, + * phoneNumber: '+1234567890', + * preferredAuthenticationMethod: PreferredAuthenticationMethods.SMS, + * }); + * ``` + */ +export const PreferredAuthenticationMethods = { + SMS: 'sms', + VOICE: 'voice', +} as const; + +/** + * Parameters for listing authentication methods from the My Account API. + */ +export interface GetAuthenticationMethodsParameters { + /** Access token with My Account API scopes. */ + accessToken: string; + /** Optional type filter to retrieve only methods of a specific type. */ + type?: AuthenticationMethodType; +} + +/** + * Parameters for retrieving a single authentication method by ID. + */ +export interface GetAuthenticationMethodByIdParameters { + /** Access token with My Account API scopes. */ + accessToken: string; + /** The ID of the authentication method to retrieve. */ + id: string; +} + +/** + * Parameters for updating an authentication method by ID. + */ +export interface UpdateAuthenticationMethodByIdParameters { + /** Access token with My Account API scopes. */ + accessToken: string; + /** The ID of the authentication method to update. */ + id: string; + /** The friendly name of the authentication method. */ + name?: string; + /** The preferred communication method for phone authenticators. */ + preferredAuthenticationMethod?: 'sms' | 'voice'; +} + +/** + * Parameters for deleting an authentication method by ID. + */ +export interface DeleteAuthenticationMethodByIdParameters { + /** Access token with My Account API scopes. */ + accessToken: string; + /** The ID of the authentication method to delete. */ + id: string; +} + +/** + * Represents a generic authentication method returned by the My Account API. + */ +export interface AuthenticationMethod { + /** Authentication method ID. */ + id: string; + /** The type of authentication method (e.g., "passkey", "phone", "email", "totp"). */ + type: string; + /** Creation timestamp (ISO 8601). */ + createdAt: string; + /** Usage classification (e.g., "mfa", "first_factor"). */ + usage: string[]; + /** Whether the method has been confirmed/verified. */ + confirmed?: boolean; + /** Friendly name of the authentication method. */ + name?: string; + /** Credential key ID (passkey/webauthn). */ + keyId?: string; + /** Public key (passkey/webauthn). */ + publicKey?: string; + /** User handle (passkey). */ + userHandle?: string; + /** Device type: "single_device" or "multi_device" (passkey). */ + credentialDeviceType?: string; + /** Whether the credential is backed up (passkey). */ + credentialBackedUp?: boolean; + /** User agent of the device used to enroll (passkey). */ + userAgent?: string; + /** Authenticator Attestation GUID (passkey). */ + aaguid?: string; + /** Relying party identifier (passkey/webauthn). */ + relyingPartyId?: string; + /** User identity ID. */ + identityUserId?: string; + /** Transports used by the authenticator (passkey). */ + transports?: string[]; + /** Phone number (phone method). */ + phoneNumber?: string; + /** Preferred communication method (phone method). */ + preferredAuthenticationMethod?: string; + /** Email address (email method). */ + email?: string; + /** Date of last password reset (password method). */ + lastPasswordReset?: string; +} + +// ========= My Account — Factor Enrollment Parameters ========= + +/** + * Parameters for enrolling a phone number as an authentication method. + * After enrollment, an OTP code will be sent to the phone number which must + * be confirmed via `confirmPhoneEnrollment`. + */ +export interface EnrollPhoneParameters { + /** Access token with My Account API scopes. */ + accessToken: string; + /** The phone number to enroll (E.164 format, e.g., "+1234567890"). */ + phoneNumber: string; + /** The preferred communication method for sending OTP codes. */ + preferredAuthenticationMethod?: 'sms' | 'voice'; +} + +/** + * Parameters for enrolling an email address as an authentication method. + * After enrollment, an OTP code will be sent to the email which must + * be confirmed via `confirmEmailEnrollment`. + */ +export interface EnrollEmailParameters { + /** Access token with My Account API scopes. */ + accessToken: string; + /** The email address to enroll. */ + emailAddress: string; +} + +/** + * Parameters for enrolling a TOTP authenticator app. + * Returns a QR code URI and optional manual input code. + * Must be confirmed with an OTP code from the authenticator app via `confirmTOTPEnrollment`. + */ +export interface EnrollTOTPParameters { + /** Access token with My Account API scopes. */ + accessToken: string; +} + +/** + * Parameters for enrolling push notification as an authentication method. + * Returns a QR code URI for pairing with a push notification app. + * Must be confirmed via `confirmPushNotificationEnrollment`. + */ +export interface EnrollPushNotificationParameters { + /** Access token with My Account API scopes. */ + accessToken: string; +} + +/** + * Parameters for enrolling a recovery code as an authentication method. + * Returns a recovery code that the user should store securely. + * Must be confirmed via `confirmRecoveryCodeEnrollment`. + */ +export interface EnrollRecoveryCodeParameters { + /** Access token with My Account API scopes. */ + accessToken: string; +} + +/** + * Parameters for confirming an enrollment that requires an OTP code. + * Used for phone, email, and TOTP enrollments. + */ +export interface ConfirmOTPEnrollmentParameters { + /** Access token with My Account API scopes. */ + accessToken: string; + /** The authentication method ID from the enrollment challenge response. */ + id: string; + /** The auth session from the enrollment challenge response. */ + authSession: string; + /** The one-time password code sent to the user or generated by the authenticator app. */ + otpCode: string; +} + +/** + * Parameters for confirming a recovery code enrollment. + * Does not require an OTP code. + */ +export interface ConfirmRecoveryCodeEnrollmentParameters { + /** Access token with My Account API scopes. */ + accessToken: string; + /** The authentication method ID from the enrollment challenge response. */ + id: string; + /** The auth session from the enrollment challenge response. */ + authSession: string; +} + +/** + * Parameters for confirming a push notification enrollment. + * Does not require an OTP code. + */ +export interface ConfirmPushNotificationEnrollmentParameters { + /** Access token with My Account API scopes. */ + accessToken: string; + /** The authentication method ID from the enrollment challenge response. */ + id: string; + /** The auth session from the enrollment challenge response. */ + authSession: string; +} + +/** + * Parameters for retrieving available factors from the My Account API. + */ +export interface GetFactorsParameters { + /** Access token with My Account API scopes. */ + accessToken: string; +} + +// ========= My Account — Factor Enrollment Responses ========= + +/** + * Response from a phone or email enrollment challenge. + * Contains the authentication method ID and auth session needed for confirmation. + */ +export interface EnrollmentChallenge { + /** The unique identifier for the authentication method. */ + id: string; + /** The unique session identifier for the enrollment (needed for confirmation). */ + authSession: string; +} + +/** + * Response from a TOTP or push notification enrollment challenge. + * Contains the QR code URI for pairing with an authenticator app. + */ +export interface TOTPEnrollmentChallenge { + /** The unique identifier for the authentication method. */ + id: string; + /** The unique session identifier for the enrollment (needed for confirmation). */ + authSession: string; + /** The URI for the QR code to be scanned by the authenticator app. */ + barcodeUri: string; + /** The manual input code as an alternative to scanning the QR code. */ + manualInputCode?: string; +} + +/** + * Response from a recovery code enrollment challenge. + * Contains the recovery code that the user must store securely. + */ +export interface RecoveryCodeEnrollmentChallenge { + /** The unique identifier for the authentication method. */ + id: string; + /** The unique session identifier for the enrollment (needed for confirmation). */ + authSession: string; + /** The recovery code value that the user should store securely. */ + recoveryCode: string; +} + +/** + * Represents an available authentication factor from the My Account API. + */ +export interface Factor { + /** The authentication method type (e.g., "phone", "email", "totp", "push-notification", "recovery-code"). */ + type: string; + /** Usage classification (e.g., ["mfa"], ["first_factor", "mfa"]). */ + usage?: string[]; +} + // ========= User Management & Profile Parameters ========= /**