Skip to content

Commit 1a01a89

Browse files
feat: implement biometric authentication support with configurable policies
1 parent 58a6025 commit 1a01a89

File tree

10 files changed

+271
-15
lines changed

10 files changed

+271
-15
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: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@
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+
- [Additional Configuration Options](#additional-configuration-options)
22+
- [Migration from Previous Behavior](#migration-from-previous-behavior)
1623
- [Management API (Users)](#management-api-users)
1724
- [Patch user with user_metadata](#patch-user-with-user_metadata)
1825
- [Get full user profile](#get-full-user-profile)
@@ -253,6 +260,128 @@ auth0.auth
253260
.catch(console.error);
254261
```
255262

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

258387
### 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" -> 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 retreive 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
);

ios/NativeBridge.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,15 @@ public class NativeBridge: NSObject {
6161
if let evaluationPolicyInt = localAuthenticationOptions["evaluationPolicy"] as? Int {
6262
evaluationPolicy = convert(policyInt: evaluationPolicyInt)
6363
}
64-
self.credentialsManager.enableBiometrics(withTitle: title, cancelTitle: localAuthenticationOptions["cancelTitle"] as? String, fallbackTitle: localAuthenticationOptions["fallbackTitle"] as? String, evaluationPolicy: evaluationPolicy)
64+
65+
// Parse biometric policy
66+
var biometricPolicy = BiometricPolicy.default
67+
if let policyString = localAuthenticationOptions["biometricPolicy"] as? String {
68+
let timeout = localAuthenticationOptions["biometricTimeout"] as? Int ?? 3600
69+
biometricPolicy = convert(policyString: policyString, timeout: timeout)
70+
}
71+
72+
self.credentialsManager.enableBiometrics(withTitle: title, cancelTitle: localAuthenticationOptions["cancelTitle"] as? String, fallbackTitle: localAuthenticationOptions["fallbackTitle"] as? String, evaluationPolicy: evaluationPolicy, policy: biometricPolicy)
6573
resolve(true)
6674
return
6775
} else {
@@ -384,6 +392,21 @@ public class NativeBridge: NSObject {
384392
}
385393
return LAPolicy.deviceOwnerAuthenticationWithBiometrics
386394
}
395+
396+
func convert(policyString: String, timeout: Int) -> BiometricPolicy {
397+
switch policyString {
398+
case "default":
399+
return .default
400+
case "always":
401+
return .always
402+
case "session":
403+
return .session(timeoutInSeconds: timeout)
404+
case "appLifecycle":
405+
return .appLifecycle(timeoutInSeconds: timeout)
406+
default:
407+
return .default
408+
}
409+
}
387410
}
388411

389412

src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ export { TokenType } from './types/common';
99
export { Auth0Provider } from './hooks/Auth0Provider';
1010
export { useAuth0 } from './hooks/useAuth0';
1111
export * from './types';
12-
export type {
12+
export {
13+
BiometricPolicy,
1314
LocalAuthenticationLevel,
14-
LocalAuthenticationOptions,
1515
LocalAuthenticationStrategy,
1616
} from './types/platform-specific';
17+
export type { LocalAuthenticationOptions } from './types/platform-specific';
1718

1819
// Re-export Auth0 as default
1920
export { default } from './Auth0';

src/specs/NativeA0Auth0.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,12 @@ export interface LocalAuthenticationOptions {
175175
* Should the user be given the option to authenticate with their device PIN, pattern, or password instead of a biometric. **Applicable for Android only.**
176176
*/
177177
deviceCredentialFallback: boolean | undefined;
178+
/**
179+
* Controls when biometric authentication prompts are shown. **Applicable for both Android and iOS.**
180+
*/
181+
biometricPolicy: string | undefined;
182+
/**
183+
* Timeout in seconds for session and appLifecycle policies. Defaults to 3600 seconds (1 hour). **Applicable for both Android and iOS.**
184+
*/
185+
biometricTimeout: Int32 | undefined;
178186
}

0 commit comments

Comments
 (0)