diff --git a/A0Auth0.podspec b/A0Auth0.podspec
index 42e42311..34af539b 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.14'
+ s.dependency 'Auth0', '2.16'
install_modules_dependencies(s)
end
diff --git a/EXAMPLES.md b/EXAMPLES.md
index 5f56aa2b..ff6341cd 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -13,6 +13,12 @@
- [Set global headers during initialization](#set-global-headers-during-initialization)
- [Using custom headers with Auth0Provider component](#using-custom-headers-with-auth0provider-component)
- [Set request-specific headers](#set-request-specific-headers)
+- [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)
+ - [Migration from Previous Behavior](#migration-from-previous-behavior)
- [Management API (Users)](#management-api-users)
- [Patch user with user_metadata](#patch-user-with-user_metadata)
- [Get full user profile](#get-full-user-profile)
@@ -253,6 +259,116 @@ auth0.auth
.catch(console.error);
```
+## Biometric Authentication
+
+> **Platform Support:** Native only (iOS/Android)
+
+Configure biometric authentication to protect credential access. The SDK supports four biometric policies that control when biometric prompts are shown.
+
+### Biometric Policy Types
+
+- **`BiometricPolicy.default`**: System-managed behavior. Reuses the same `LAContext` on iOS, allowing the system to optimize prompt frequency. May skip the biometric prompt if authentication was recently successful.
+
+- **`BiometricPolicy.always`**: Always requires biometric authentication on every credential access. Creates a fresh `LAContext` on iOS and uses the "Always" policy on Android to ensure a new prompt is shown.
+
+- **`BiometricPolicy.session`**: Requires biometric authentication only once per session. After successful authentication, credentials can be accessed without prompting for the specified timeout duration.
+
+- **`BiometricPolicy.appLifecycle`**: Similar to session policy, but persists for the app's lifecycle. Session remains valid until the app restarts or `clearCredentials()` is called. Default timeout is 1 hour (3600 seconds).
+
+### Using with Auth0Provider (Hooks)
+
+```jsx
+import {
+ Auth0Provider,
+ BiometricPolicy,
+ LocalAuthenticationStrategy,
+ LocalAuthenticationLevel,
+} from 'react-native-auth0';
+
+function App() {
+ return (
+
+
+
+ );
+}
+```
+
+### Using with Auth0 Class
+
+```js
+import Auth0, {
+ BiometricPolicy,
+ LocalAuthenticationStrategy,
+ LocalAuthenticationLevel,
+} from 'react-native-auth0';
+
+const auth0 = new Auth0({
+ domain: 'YOUR_AUTH0_DOMAIN',
+ clientId: 'YOUR_AUTH0_CLIENT_ID',
+ localAuthenticationOptions: {
+ title: 'Authenticate to access credentials',
+ subtitle: 'Please authenticate to continue',
+ description: 'We need to authenticate you to retrieve your credentials',
+ cancelTitle: 'Cancel',
+ evaluationPolicy: LocalAuthenticationStrategy.deviceOwnerWithBiometrics,
+ fallbackTitle: 'Use Passcode',
+ authenticationLevel: LocalAuthenticationLevel.strong,
+ deviceCredentialFallback: true,
+ biometricPolicy: BiometricPolicy.session,
+ biometricTimeout: 300, // 5 minutes
+ },
+});
+
+// Get credentials - will prompt for biometric authentication based on policy
+const credentials = await auth0.credentialsManager.getCredentials();
+```
+
+### Platform-Specific Behavior
+
+#### Android
+
+- `BiometricPolicy.default` and `BiometricPolicy.always` both map to the Android SDK's "Always" policy
+- Uses `BiometricPrompt` for authentication
+- Session state is stored in memory and cleared on app restart
+
+#### iOS
+
+- `BiometricPolicy.default` reuses the same `LAContext`, allowing the system to manage prompt frequency
+- `BiometricPolicy.always`, `session`, and `appLifecycle` create a fresh `LAContext` to ensure reliable prompts
+- Uses Face ID or Touch ID based on device capabilities
+- Session state is thread-safe and managed in memory
+
+### Migration from Previous Behavior
+
+If you were not explicitly configuring biometric authentication before, the new `BiometricPolicy.default` maintains backward-compatible behavior. To enforce stricter biometric requirements, switch to `BiometricPolicy.always`.
+
## Management API (Users)
### Patch user with user_metadata
diff --git a/android/build.gradle b/android/build.gradle
index ffcf375f..73bbe866 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.11.0'
+ implementation 'com.auth0.android:auth0:3.12.0'
}
if (isNewArchitectureEnabled()) {
diff --git a/android/src/main/java/com/auth0/react/LocalAuthenticationOptionsParser.kt b/android/src/main/java/com/auth0/react/LocalAuthenticationOptionsParser.kt
index 5c7299fb..d0ca17c7 100644
--- a/android/src/main/java/com/auth0/react/LocalAuthenticationOptionsParser.kt
+++ b/android/src/main/java/com/auth0/react/LocalAuthenticationOptionsParser.kt
@@ -1,6 +1,7 @@
package com.auth0.react
import com.auth0.android.authentication.storage.AuthenticationLevel
+import com.auth0.android.authentication.storage.BiometricPolicy
import com.auth0.android.authentication.storage.LocalAuthenticationOptions
import com.facebook.react.bridge.ReadableMap
@@ -11,16 +12,18 @@ object LocalAuthenticationOptionsParser {
private const val CANCEL_TITLE_KEY = "cancel"
private const val AUTHENTICATION_LEVEL_KEY = "authenticationLevel"
private const val DEVICE_CREDENTIAL_FALLBACK_KEY = "deviceCredentialFallback"
+ private const val BIOMETRIC_POLICY_KEY = "biometricPolicy"
+ private const val BIOMETRIC_TIMEOUT_KEY = "biometricTimeout"
fun fromMap(map: ReadableMap): LocalAuthenticationOptions {
val title = map.getString(TITLE_KEY)
?: throw IllegalArgumentException("LocalAuthenticationOptionsParser: fromMap: The 'title' field is required")
-
+
val subtitle = map.getString(SUBTITLE_KEY)
val description = map.getString(DESCRIPTION_KEY)
val cancelTitle = map.getString(CANCEL_TITLE_KEY)
val deviceCredentialFallback = map.getBoolean(DEVICE_CREDENTIAL_FALLBACK_KEY)
-
+
val builder = LocalAuthenticationOptions.Builder()
.setTitle(title)
.setSubTitle(subtitle)
@@ -33,9 +36,22 @@ object LocalAuthenticationOptionsParser {
val level = getAuthenticationLevelFromInt(map.getInt(AUTHENTICATION_LEVEL_KEY))
builder.setAuthenticationLevel(level)
}
-
+
cancelTitle?.let { builder.setNegativeButtonText(it) }
-
+
+ // Parse biometric policy
+ if (map.hasKey(BIOMETRIC_POLICY_KEY)) {
+ val policyString = map.getString(BIOMETRIC_POLICY_KEY)
+ val timeout = if (map.hasKey(BIOMETRIC_TIMEOUT_KEY)) {
+ map.getInt(BIOMETRIC_TIMEOUT_KEY)
+ } else {
+ 3600 // Default 1 hour
+ }
+
+ val policy = getBiometricPolicyFromString(policyString, timeout)
+ builder.setPolicy(policy)
+ }
+
return builder.build()
}
@@ -46,5 +62,14 @@ object LocalAuthenticationOptionsParser {
else -> AuthenticationLevel.DEVICE_CREDENTIAL
}
}
+
+ private fun getBiometricPolicyFromString(policy: String?, timeout: Int): BiometricPolicy {
+ return when (policy) {
+ "default", "always", null -> BiometricPolicy.Always // Map both 'default' and 'always' to Always
+ "session" -> BiometricPolicy.Session(timeout)
+ "appLifecycle" -> BiometricPolicy.AppLifecycle(timeout)
+ else -> BiometricPolicy.Always // Default to Always
+ }
+ }
}
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index cec6f8d7..b6533c10 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -1,6 +1,6 @@
PODS:
- A0Auth0 (5.2.1):
- - Auth0 (= 2.14)
+ - Auth0 (= 2.16)
- boost
- DoubleConversion
- fast_float
@@ -28,7 +28,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- - Auth0 (2.14.0):
+ - Auth0 (2.16.0):
- JWTDecode (= 3.3.0)
- SimpleKeychain (= 1.3.0)
- boost (1.84.0)
@@ -2778,8 +2778,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
- A0Auth0: 9253099fae9372f663f89abbf5d02a6b36faf45c
- Auth0: 022dda235af8a664a4faf9e7b60b063b5bc08373
+ A0Auth0: 936bf2484d314ca9ae7c3285bcc53ebe92b5e1cd
+ Auth0: 6db7cf0801a5201b3c6c376f07b8dfecfac21ebe
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6
diff --git a/example/src/navigation/HooksDemoNavigator.tsx b/example/src/navigation/HooksDemoNavigator.tsx
index 587c8d76..d7205cd4 100644
--- a/example/src/navigation/HooksDemoNavigator.tsx
+++ b/example/src/navigation/HooksDemoNavigator.tsx
@@ -1,5 +1,11 @@
import React from 'react';
-import { Auth0Provider, useAuth0 } from 'react-native-auth0';
+import {
+ Auth0Provider,
+ useAuth0,
+ BiometricPolicy,
+ LocalAuthenticationStrategy,
+ LocalAuthenticationLevel,
+} from 'react-native-auth0';
import AuthStackNavigator from './AuthStackNavigator';
import MainTabNavigator from './MainTabNavigator';
import { ActivityIndicator, View, StyleSheet } from 'react-native';
@@ -31,10 +37,45 @@ const AppContent = () => {
/**
* This component wraps the entire Hooks-based demo flow with the Auth0Provider,
* making the authentication context available to all its child screens.
+ *
+ * Biometric Policy Examples:
+ * - BiometricPolicy.default: System-managed, may skip prompt if recently authenticated
+ * - BiometricPolicy.always: Always shows biometric prompt on every credential access
+ * - BiometricPolicy.session: Shows prompt once, then caches for specified timeout
+ * - BiometricPolicy.appLifecycle: Shows prompt once per app lifecycle
+ *
+ * Uncomment different policies below to test them.
*/
const HooksDemoNavigator = () => {
return (
-
+
);
diff --git a/example/src/navigation/MainTabNavigator.tsx b/example/src/navigation/MainTabNavigator.tsx
index 0a9c866f..af4632ea 100644
--- a/example/src/navigation/MainTabNavigator.tsx
+++ b/example/src/navigation/MainTabNavigator.tsx
@@ -3,7 +3,6 @@
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import ProfileScreen from '../screens/hooks/Profile';
-import ApiScreen from '../screens/hooks/Api';
import MoreScreen from '../screens/hooks/More';
import CredentialsScreen from '../screens/hooks/CredentialsScreen';
@@ -34,7 +33,6 @@ const MainTabNavigator = () => {
// You can add icons here if desired
/>
-
);
diff --git a/example/src/navigation/RootNavigator.tsx b/example/src/navigation/RootNavigator.tsx
index eb84bbeb..00b53c7e 100644
--- a/example/src/navigation/RootNavigator.tsx
+++ b/example/src/navigation/RootNavigator.tsx
@@ -1,10 +1,14 @@
// example/src/navigation/RootNavigator.tsx
-import React from 'react';
+import React, { Suspense } from 'react';
+import { ActivityIndicator, View, StyleSheet } from 'react-native';
import { createStackNavigator } from '@react-navigation/stack';
import SelectionScreen from '../screens/SelectionScreen';
-import HooksDemoNavigator from './HooksDemoNavigator';
-import ClassDemoNavigator from './ClassDemoNavigator';
+
+// Lazy load the demo navigators to prevent Auth0Provider from initializing
+// until the user actually navigates to those screens.
+const HooksDemoNavigator = React.lazy(() => import('./HooksDemoNavigator'));
+const ClassDemoNavigator = React.lazy(() => import('./ClassDemoNavigator'));
// Define the parameter list for type safety
export type RootStackParamList = {
@@ -15,6 +19,13 @@ export type RootStackParamList = {
const Stack = createStackNavigator();
+// Loading fallback component
+const LoadingFallback = () => (
+
+
+
+);
+
/**
* The top-level navigator that allows the user to select which
* demo they want to see: the recommended Hooks-based approach or
@@ -33,16 +44,35 @@ const RootNavigator = () => {
/>
+ >
+ {() => (
+ }>
+
+
+ )}
+
+ >
+ {() => (
+ }>
+
+
+ )}
+
);
};
+const styles = StyleSheet.create({
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: '#FFFFFF',
+ },
+});
+
export default RootNavigator;
diff --git a/example/src/screens/hooks/Api.tsx b/example/src/screens/hooks/Api.tsx
deleted file mode 100644
index 8f97a5a9..00000000
--- a/example/src/screens/hooks/Api.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import React, { useState } from 'react';
-import { SafeAreaView, ScrollView, StyleSheet } from 'react-native';
-import Auth0, { useAuth0, User } from 'react-native-auth0';
-import Button from '../../components/Button';
-import Header from '../../components/Header';
-import Result from '../../components/Result';
-import config from '../../auth0-configuration';
-
-const AUTH0_DOMAIN = config.domain;
-const AUTH0_CLIENT_ID = config.clientId;
-
-const auth0 = new Auth0({ domain: AUTH0_DOMAIN, clientId: AUTH0_CLIENT_ID });
-
-const ApiScreen = () => {
- const { user, getCredentials } = useAuth0();
- const [apiResult, setApiResult] = useState(null);
- const [apiError, setApiError] = useState(null);
-
- const onCallApi = async () => {
- try {
- const credentials = await getCredentials(
- 'openid profile email read:current_user'
- );
- if (!credentials || !user?.sub) {
- throw new Error('Could not get credentials or user ID.');
- }
-
- const managementClient = auth0.users(credentials.accessToken);
- const fullProfile = await managementClient.getUser({ id: user.sub });
-
- setApiResult(fullProfile);
- setApiError(null);
- } catch (e) {
- setApiError(e as Error);
- }
- };
-
- return (
-
-
-
-
-
-
-
- );
-};
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#FFFFFF',
- },
- content: {
- alignItems: 'center',
- padding: 16,
- },
-});
-
-export default ApiScreen;
diff --git a/ios/NativeBridge.swift b/ios/NativeBridge.swift
index c95e19f5..e0d67eaf 100644
--- a/ios/NativeBridge.swift
+++ b/ios/NativeBridge.swift
@@ -61,7 +61,15 @@ public class NativeBridge: NSObject {
if let evaluationPolicyInt = localAuthenticationOptions["evaluationPolicy"] as? Int {
evaluationPolicy = convert(policyInt: evaluationPolicyInt)
}
- self.credentialsManager.enableBiometrics(withTitle: title, cancelTitle: localAuthenticationOptions["cancelTitle"] as? String, fallbackTitle: localAuthenticationOptions["fallbackTitle"] as? String, evaluationPolicy: evaluationPolicy)
+
+ // Parse biometric policy
+ var biometricPolicy = BiometricPolicy.default
+ if let policyString = localAuthenticationOptions["biometricPolicy"] as? String {
+ let timeout = localAuthenticationOptions["biometricTimeout"] as? Int ?? 3600
+ biometricPolicy = convert(policyString: policyString, timeout: timeout)
+ }
+
+ self.credentialsManager.enableBiometrics(withTitle: title, cancelTitle: localAuthenticationOptions["cancelTitle"] as? String, fallbackTitle: localAuthenticationOptions["fallbackTitle"] as? String, evaluationPolicy: evaluationPolicy, policy: biometricPolicy)
resolve(true)
return
} else {
@@ -384,6 +392,21 @@ public class NativeBridge: NSObject {
}
return LAPolicy.deviceOwnerAuthenticationWithBiometrics
}
+
+ func convert(policyString: String, timeout: Int) -> BiometricPolicy {
+ switch policyString {
+ case "default":
+ return .default
+ case "always":
+ return .always
+ case "session":
+ return .session(timeoutInSeconds: timeout)
+ case "appLifecycle":
+ return .appLifecycle(timeoutInSeconds: timeout)
+ default:
+ return .default
+ }
+ }
}
diff --git a/src/exports/enums.ts b/src/exports/enums.ts
index ea849e6c..5b4987c8 100644
--- a/src/exports/enums.ts
+++ b/src/exports/enums.ts
@@ -2,4 +2,5 @@ export { SafariViewControllerPresentationStyle } from '../index';
export {
LocalAuthenticationLevel,
LocalAuthenticationStrategy,
+ BiometricPolicy,
} from '../types/platform-specific';
diff --git a/src/index.ts b/src/index.ts
index 442d3c5d..aaf6d89b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -9,11 +9,12 @@ export { TokenType } from './types/common';
export { Auth0Provider } from './hooks/Auth0Provider';
export { useAuth0 } from './hooks/useAuth0';
export * from './types';
-export type {
+export {
+ BiometricPolicy,
LocalAuthenticationLevel,
- LocalAuthenticationOptions,
LocalAuthenticationStrategy,
} from './types/platform-specific';
+export type { LocalAuthenticationOptions } from './types/platform-specific';
// Re-export Auth0 as default
export { default } from './Auth0';
diff --git a/src/specs/NativeA0Auth0.ts b/src/specs/NativeA0Auth0.ts
index 882fcb78..a0da007c 100644
--- a/src/specs/NativeA0Auth0.ts
+++ b/src/specs/NativeA0Auth0.ts
@@ -175,4 +175,12 @@ export interface LocalAuthenticationOptions {
* Should the user be given the option to authenticate with their device PIN, pattern, or password instead of a biometric. **Applicable for Android only.**
*/
deviceCredentialFallback: boolean | undefined;
+ /**
+ * Controls when biometric authentication prompts are shown. **Applicable for both Android and iOS.**
+ */
+ biometricPolicy: string | undefined;
+ /**
+ * Timeout in seconds for session and appLifecycle policies. Defaults to 3600 seconds (1 hour). **Applicable for both Android and iOS.**
+ */
+ biometricTimeout: Int32 | undefined;
}
diff --git a/src/types/platform-specific.ts b/src/types/platform-specific.ts
index 1741fff5..75dc306b 100644
--- a/src/types/platform-specific.ts
+++ b/src/types/platform-specific.ts
@@ -42,6 +42,23 @@ export enum LocalAuthenticationStrategy {
deviceOwner,
}
+/**
+ * @remarks
+ * **Platform specific:** Native only (iOS/Android).
+ * Controls when biometric authentication prompts are shown when accessing stored credentials.
+ *
+ * - `default`: System-managed behavior (preserves backward compatibility, reuses LAContext on iOS; maps to Always policy on Android)
+ * - `always`: Always prompts for biometric authentication (creates fresh LAContext on iOS, maps to 'always' on Android)
+ * - `session`: Prompts once, then caches for the specified timeout in seconds
+ * - `appLifecycle`: Prompts once until app restart or manual clear, with optional timeout
+ */
+export enum BiometricPolicy {
+ default = 'default',
+ always = 'always',
+ session = 'session',
+ appLifecycle = 'appLifecycle',
+}
+
// ========= Native-Specific Options =========
/**
@@ -58,6 +75,18 @@ export interface LocalAuthenticationOptions {
authenticationLevel?: LocalAuthenticationLevel;
fallbackTitle?: string;
deviceCredentialFallback?: boolean;
+ /**
+ * Controls when biometric authentication prompts are shown when accessing stored credentials.
+ * @default BiometricPolicy.default
+ */
+ biometricPolicy?: BiometricPolicy;
+ /**
+ * Timeout in seconds for session and appLifecycle policies.
+ * - For `session` policy: credentials are cached for this duration after successful biometric authentication
+ * - For `appLifecycle` policy: optional timeout, defaults to 3600 seconds (1 hour) if not specified
+ * @default 3600 (1 hour)
+ */
+ biometricTimeout?: number;
}
/**