Skip to content

Commit 35fb0b4

Browse files
feat: add configurable biometric authentication policies (#1411)
1 parent 58a6025 commit 35fb0b4

File tree

14 files changed

+296
-88
lines changed

14 files changed

+296
-88
lines changed

A0Auth0.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
1616
s.source_files = 'ios/**/*.{h,m,mm,swift}'
1717
s.requires_arc = true
1818

19-
s.dependency 'Auth0', '2.14'
19+
s.dependency 'Auth0', '2.16'
2020

2121
install_modules_dependencies(s)
2222
end

EXAMPLES.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313
- [Set global headers during initialization](#set-global-headers-during-initialization)
1414
- [Using custom headers with Auth0Provider component](#using-custom-headers-with-auth0provider-component)
1515
- [Set request-specific headers](#set-request-specific-headers)
16+
- [Biometric Authentication](#biometric-authentication)
17+
- [Biometric Policy Types](#biometric-policy-types)
18+
- [Using with Auth0Provider (Hooks)](#using-with-auth0provider-hooks)
19+
- [Using with Auth0 Class](#using-with-auth0-class)
20+
- [Platform-Specific Behavior](#platform-specific-behavior)
21+
- [Migration from Previous Behavior](#migration-from-previous-behavior)
1622
- [Management API (Users)](#management-api-users)
1723
- [Patch user with user_metadata](#patch-user-with-user_metadata)
1824
- [Get full user profile](#get-full-user-profile)
@@ -253,6 +259,116 @@ auth0.auth
253259
.catch(console.error);
254260
```
255261

262+
## Biometric Authentication
263+
264+
> **Platform Support:** Native only (iOS/Android)
265+
266+
Configure biometric authentication to protect credential access. The SDK supports four biometric policies that control when biometric prompts are shown.
267+
268+
### Biometric Policy Types
269+
270+
- **`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.
271+
272+
- **`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.
273+
274+
- **`BiometricPolicy.session`**: Requires biometric authentication only once per session. After successful authentication, credentials can be accessed without prompting for the specified timeout duration.
275+
276+
- **`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).
277+
278+
### Using with Auth0Provider (Hooks)
279+
280+
```jsx
281+
import {
282+
Auth0Provider,
283+
BiometricPolicy,
284+
LocalAuthenticationStrategy,
285+
LocalAuthenticationLevel,
286+
} from 'react-native-auth0';
287+
288+
function App() {
289+
return (
290+
<Auth0Provider
291+
domain="YOUR_AUTH0_DOMAIN"
292+
clientId="YOUR_CLIENT_ID"
293+
localAuthenticationOptions={{
294+
title: 'Authenticate to access credentials',
295+
subtitle: 'Please authenticate to continue',
296+
description: 'We need to authenticate you to retrieve your credentials',
297+
cancelTitle: 'Cancel',
298+
evaluationPolicy: LocalAuthenticationStrategy.deviceOwnerWithBiometrics,
299+
fallbackTitle: 'Use Passcode',
300+
authenticationLevel: LocalAuthenticationLevel.strong,
301+
deviceCredentialFallback: true,
302+
// Option 1: Default policy (system-managed, backward compatible)
303+
biometricPolicy: BiometricPolicy.default,
304+
305+
// Option 2: Always require biometric authentication
306+
// biometricPolicy: BiometricPolicy.always,
307+
308+
// Option 3: Session-based (5 minutes)
309+
// biometricPolicy: BiometricPolicy.session,
310+
// biometricTimeout: 300,
311+
312+
// Option 4: App lifecycle (1 hour)
313+
// biometricPolicy: BiometricPolicy.appLifecycle,
314+
// biometricTimeout: 3600,
315+
}}
316+
>
317+
<YourApp />
318+
</Auth0Provider>
319+
);
320+
}
321+
```
322+
323+
### Using with Auth0 Class
324+
325+
```js
326+
import Auth0, {
327+
BiometricPolicy,
328+
LocalAuthenticationStrategy,
329+
LocalAuthenticationLevel,
330+
} from 'react-native-auth0';
331+
332+
const auth0 = new Auth0({
333+
domain: 'YOUR_AUTH0_DOMAIN',
334+
clientId: 'YOUR_AUTH0_CLIENT_ID',
335+
localAuthenticationOptions: {
336+
title: 'Authenticate to access credentials',
337+
subtitle: 'Please authenticate to continue',
338+
description: 'We need to authenticate you to retrieve your credentials',
339+
cancelTitle: 'Cancel',
340+
evaluationPolicy: LocalAuthenticationStrategy.deviceOwnerWithBiometrics,
341+
fallbackTitle: 'Use Passcode',
342+
authenticationLevel: LocalAuthenticationLevel.strong,
343+
deviceCredentialFallback: true,
344+
biometricPolicy: BiometricPolicy.session,
345+
biometricTimeout: 300, // 5 minutes
346+
},
347+
});
348+
349+
// Get credentials - will prompt for biometric authentication based on policy
350+
const credentials = await auth0.credentialsManager.getCredentials();
351+
```
352+
353+
### Platform-Specific Behavior
354+
355+
#### Android
356+
357+
- `BiometricPolicy.default` and `BiometricPolicy.always` both map to the Android SDK's "Always" policy
358+
- Uses `BiometricPrompt` for authentication
359+
- Session state is stored in memory and cleared on app restart
360+
361+
#### iOS
362+
363+
- `BiometricPolicy.default` reuses the same `LAContext`, allowing the system to manage prompt frequency
364+
- `BiometricPolicy.always`, `session`, and `appLifecycle` create a fresh `LAContext` to ensure reliable prompts
365+
- Uses Face ID or Touch ID based on device capabilities
366+
- Session state is thread-safe and managed in memory
367+
368+
### Migration from Previous Behavior
369+
370+
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`.
371+
256372
## Management API (Users)
257373

258374
### Patch user with user_metadata

android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ dependencies {
9696
implementation "com.facebook.react:react-android"
9797
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
9898
implementation "androidx.browser:browser:1.2.0"
99-
implementation 'com.auth0.android:auth0:3.11.0'
99+
implementation 'com.auth0.android:auth0:3.12.0'
100100
}
101101

102102
if (isNewArchitectureEnabled()) {

android/src/main/java/com/auth0/react/LocalAuthenticationOptionsParser.kt

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.auth0.react
22

33
import com.auth0.android.authentication.storage.AuthenticationLevel
4+
import com.auth0.android.authentication.storage.BiometricPolicy
45
import com.auth0.android.authentication.storage.LocalAuthenticationOptions
56
import com.facebook.react.bridge.ReadableMap
67

@@ -11,16 +12,18 @@ object LocalAuthenticationOptionsParser {
1112
private const val CANCEL_TITLE_KEY = "cancel"
1213
private const val AUTHENTICATION_LEVEL_KEY = "authenticationLevel"
1314
private const val DEVICE_CREDENTIAL_FALLBACK_KEY = "deviceCredentialFallback"
15+
private const val BIOMETRIC_POLICY_KEY = "biometricPolicy"
16+
private const val BIOMETRIC_TIMEOUT_KEY = "biometricTimeout"
1417

1518
fun fromMap(map: ReadableMap): LocalAuthenticationOptions {
1619
val title = map.getString(TITLE_KEY)
1720
?: throw IllegalArgumentException("LocalAuthenticationOptionsParser: fromMap: The 'title' field is required")
18-
21+
1922
val subtitle = map.getString(SUBTITLE_KEY)
2023
val description = map.getString(DESCRIPTION_KEY)
2124
val cancelTitle = map.getString(CANCEL_TITLE_KEY)
2225
val deviceCredentialFallback = map.getBoolean(DEVICE_CREDENTIAL_FALLBACK_KEY)
23-
26+
2427
val builder = LocalAuthenticationOptions.Builder()
2528
.setTitle(title)
2629
.setSubTitle(subtitle)
@@ -33,9 +36,22 @@ object LocalAuthenticationOptionsParser {
3336
val level = getAuthenticationLevelFromInt(map.getInt(AUTHENTICATION_LEVEL_KEY))
3437
builder.setAuthenticationLevel(level)
3538
}
36-
39+
3740
cancelTitle?.let { builder.setNegativeButtonText(it) }
38-
41+
42+
// Parse biometric policy
43+
if (map.hasKey(BIOMETRIC_POLICY_KEY)) {
44+
val policyString = map.getString(BIOMETRIC_POLICY_KEY)
45+
val timeout = if (map.hasKey(BIOMETRIC_TIMEOUT_KEY)) {
46+
map.getInt(BIOMETRIC_TIMEOUT_KEY)
47+
} else {
48+
3600 // Default 1 hour
49+
}
50+
51+
val policy = getBiometricPolicyFromString(policyString, timeout)
52+
builder.setPolicy(policy)
53+
}
54+
3955
return builder.build()
4056
}
4157

@@ -46,5 +62,14 @@ object LocalAuthenticationOptionsParser {
4662
else -> AuthenticationLevel.DEVICE_CREDENTIAL
4763
}
4864
}
65+
66+
private fun getBiometricPolicyFromString(policy: String?, timeout: Int): BiometricPolicy {
67+
return when (policy) {
68+
"default", "always", null -> BiometricPolicy.Always // Map both 'default' and 'always' to Always
69+
"session" -> BiometricPolicy.Session(timeout)
70+
"appLifecycle" -> BiometricPolicy.AppLifecycle(timeout)
71+
else -> BiometricPolicy.Always // Default to Always
72+
}
73+
}
4974
}
5075

example/ios/Podfile.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
PODS:
22
- A0Auth0 (5.2.1):
3-
- Auth0 (= 2.14)
3+
- Auth0 (= 2.16)
44
- boost
55
- DoubleConversion
66
- fast_float
@@ -28,7 +28,7 @@ PODS:
2828
- ReactCommon/turbomodule/core
2929
- SocketRocket
3030
- Yoga
31-
- Auth0 (2.14.0):
31+
- Auth0 (2.16.0):
3232
- JWTDecode (= 3.3.0)
3333
- SimpleKeychain (= 1.3.0)
3434
- boost (1.84.0)
@@ -2778,8 +2778,8 @@ EXTERNAL SOURCES:
27782778
:path: "../node_modules/react-native/ReactCommon/yoga"
27792779

27802780
SPEC CHECKSUMS:
2781-
A0Auth0: 9253099fae9372f663f89abbf5d02a6b36faf45c
2782-
Auth0: 022dda235af8a664a4faf9e7b60b063b5bc08373
2781+
A0Auth0: 936bf2484d314ca9ae7c3285bcc53ebe92b5e1cd
2782+
Auth0: 6db7cf0801a5201b3c6c376f07b8dfecfac21ebe
27832783
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
27842784
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
27852785
fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6

example/src/navigation/HooksDemoNavigator.tsx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import React from 'react';
2-
import { Auth0Provider, useAuth0 } from 'react-native-auth0';
2+
import {
3+
Auth0Provider,
4+
useAuth0,
5+
BiometricPolicy,
6+
LocalAuthenticationStrategy,
7+
LocalAuthenticationLevel,
8+
} from 'react-native-auth0';
39
import AuthStackNavigator from './AuthStackNavigator';
410
import MainTabNavigator from './MainTabNavigator';
511
import { ActivityIndicator, View, StyleSheet } from 'react-native';
@@ -31,10 +37,45 @@ const AppContent = () => {
3137
/**
3238
* This component wraps the entire Hooks-based demo flow with the Auth0Provider,
3339
* making the authentication context available to all its child screens.
40+
*
41+
* Biometric Policy Examples:
42+
* - BiometricPolicy.default: System-managed, may skip prompt if recently authenticated
43+
* - BiometricPolicy.always: Always shows biometric prompt on every credential access
44+
* - BiometricPolicy.session: Shows prompt once, then caches for specified timeout
45+
* - BiometricPolicy.appLifecycle: Shows prompt once per app lifecycle
46+
*
47+
* Uncomment different policies below to test them.
3448
*/
3549
const HooksDemoNavigator = () => {
3650
return (
37-
<Auth0Provider domain={AUTH0_DOMAIN} clientId={AUTH0_CLIENT_ID}>
51+
<Auth0Provider
52+
domain={AUTH0_DOMAIN}
53+
clientId={AUTH0_CLIENT_ID}
54+
// Example: Enable biometric authentication with different policies
55+
localAuthenticationOptions={{
56+
title: 'Authenticate to retrieve your credentials',
57+
subtitle: 'Please authenticate to continue',
58+
description: 'We need to authenticate you to retrieve your credentials',
59+
cancelTitle: 'Cancel',
60+
evaluationPolicy: LocalAuthenticationStrategy.deviceOwnerWithBiometrics,
61+
fallbackTitle: 'Use Passcode',
62+
authenticationLevel: LocalAuthenticationLevel.strong,
63+
deviceCredentialFallback: true,
64+
// Option 1: Default policy (system-managed)
65+
biometricPolicy: BiometricPolicy.default,
66+
67+
// Option 2: Always require biometric authentication
68+
// biometricPolicy: BiometricPolicy.always,
69+
70+
// Option 3: Session-based (5 minutes)
71+
// biometricPolicy: BiometricPolicy.session,
72+
// biometricTimeout: 300,
73+
74+
// Option 4: App lifecycle (1 hour)
75+
// biometricPolicy: BiometricPolicy.appLifecycle,
76+
// biometricTimeout: 3600,
77+
}}
78+
>
3879
<AppContent />
3980
</Auth0Provider>
4081
);

example/src/navigation/MainTabNavigator.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import React from 'react';
44
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
55
import ProfileScreen from '../screens/hooks/Profile';
6-
import ApiScreen from '../screens/hooks/Api';
76
import MoreScreen from '../screens/hooks/More';
87
import CredentialsScreen from '../screens/hooks/CredentialsScreen';
98

@@ -34,7 +33,6 @@ const MainTabNavigator = () => {
3433
// You can add icons here if desired
3534
/>
3635
<Tab.Screen name="Credentials" component={CredentialsScreen} />
37-
<Tab.Screen name="Api" component={ApiScreen} />
3836
<Tab.Screen name="More" component={MoreScreen} />
3937
</Tab.Navigator>
4038
);

example/src/navigation/RootNavigator.tsx

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
// example/src/navigation/RootNavigator.tsx
22

3-
import React from 'react';
3+
import React, { Suspense } from 'react';
4+
import { ActivityIndicator, View, StyleSheet } from 'react-native';
45
import { createStackNavigator } from '@react-navigation/stack';
56
import SelectionScreen from '../screens/SelectionScreen';
6-
import HooksDemoNavigator from './HooksDemoNavigator';
7-
import ClassDemoNavigator from './ClassDemoNavigator';
7+
8+
// Lazy load the demo navigators to prevent Auth0Provider from initializing
9+
// until the user actually navigates to those screens.
10+
const HooksDemoNavigator = React.lazy(() => import('./HooksDemoNavigator'));
11+
const ClassDemoNavigator = React.lazy(() => import('./ClassDemoNavigator'));
812

913
// Define the parameter list for type safety
1014
export type RootStackParamList = {
@@ -15,6 +19,13 @@ export type RootStackParamList = {
1519

1620
const Stack = createStackNavigator<RootStackParamList>();
1721

22+
// Loading fallback component
23+
const LoadingFallback = () => (
24+
<View style={styles.loadingContainer}>
25+
<ActivityIndicator size="large" color="#E53935" />
26+
</View>
27+
);
28+
1829
/**
1930
* The top-level navigator that allows the user to select which
2031
* demo they want to see: the recommended Hooks-based approach or
@@ -33,16 +44,35 @@ const RootNavigator = () => {
3344
/>
3445
<Stack.Screen
3546
name="HooksDemo"
36-
component={HooksDemoNavigator}
3747
options={{ headerShown: false }} // The hooks demo will manage its own UI
38-
/>
48+
>
49+
{() => (
50+
<Suspense fallback={<LoadingFallback />}>
51+
<HooksDemoNavigator />
52+
</Suspense>
53+
)}
54+
</Stack.Screen>
3955
<Stack.Screen
4056
name="ClassDemo"
41-
component={ClassDemoNavigator}
4257
options={{ headerShown: false }} // The class demo will manage its own UI
43-
/>
58+
>
59+
{() => (
60+
<Suspense fallback={<LoadingFallback />}>
61+
<ClassDemoNavigator />
62+
</Suspense>
63+
)}
64+
</Stack.Screen>
4465
</Stack.Navigator>
4566
);
4667
};
4768

69+
const styles = StyleSheet.create({
70+
loadingContainer: {
71+
flex: 1,
72+
justifyContent: 'center',
73+
alignItems: 'center',
74+
backgroundColor: '#FFFFFF',
75+
},
76+
});
77+
4878
export default RootNavigator;

0 commit comments

Comments
 (0)