Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion A0Auth0.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
116 changes: 116 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 (
<Auth0Provider
domain="YOUR_AUTH0_DOMAIN"
clientId="YOUR_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,
// Option 1: Default policy (system-managed, backward compatible)
biometricPolicy: BiometricPolicy.default,

// Option 2: Always require biometric authentication
// biometricPolicy: BiometricPolicy.always,

// Option 3: Session-based (5 minutes)
// biometricPolicy: BiometricPolicy.session,
// biometricTimeout: 300,

// Option 4: App lifecycle (1 hour)
// biometricPolicy: BiometricPolicy.appLifecycle,
// biometricTimeout: 3600,
}}
>
<YourApp />
</Auth0Provider>
);
}
```

### 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
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)
Expand All @@ -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()
}

Expand All @@ -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
}
}
}

8 changes: 4 additions & 4 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
PODS:
- A0Auth0 (5.2.1):
- Auth0 (= 2.14)
- Auth0 (= 2.16)
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
45 changes: 43 additions & 2 deletions example/src/navigation/HooksDemoNavigator.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 (
<Auth0Provider domain={AUTH0_DOMAIN} clientId={AUTH0_CLIENT_ID}>
<Auth0Provider
domain={AUTH0_DOMAIN}
clientId={AUTH0_CLIENT_ID}
// Example: Enable biometric authentication with different policies
localAuthenticationOptions={{
title: 'Authenticate to retrieve your 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,
// Option 1: Default policy (system-managed)
biometricPolicy: BiometricPolicy.default,

// Option 2: Always require biometric authentication
// biometricPolicy: BiometricPolicy.always,

// Option 3: Session-based (5 minutes)
// biometricPolicy: BiometricPolicy.session,
// biometricTimeout: 300,

// Option 4: App lifecycle (1 hour)
// biometricPolicy: BiometricPolicy.appLifecycle,
// biometricTimeout: 3600,
}}
>
<AppContent />
</Auth0Provider>
);
Expand Down
2 changes: 0 additions & 2 deletions example/src/navigation/MainTabNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -34,7 +33,6 @@ const MainTabNavigator = () => {
// You can add icons here if desired
/>
<Tab.Screen name="Credentials" component={CredentialsScreen} />
<Tab.Screen name="Api" component={ApiScreen} />
<Tab.Screen name="More" component={MoreScreen} />
</Tab.Navigator>
);
Expand Down
44 changes: 37 additions & 7 deletions example/src/navigation/RootNavigator.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -15,6 +19,13 @@ export type RootStackParamList = {

const Stack = createStackNavigator<RootStackParamList>();

// Loading fallback component
const LoadingFallback = () => (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#E53935" />
</View>
);

/**
* The top-level navigator that allows the user to select which
* demo they want to see: the recommended Hooks-based approach or
Expand All @@ -33,16 +44,35 @@ const RootNavigator = () => {
/>
<Stack.Screen
name="HooksDemo"
component={HooksDemoNavigator}
options={{ headerShown: false }} // The hooks demo will manage its own UI
/>
>
{() => (
<Suspense fallback={<LoadingFallback />}>
<HooksDemoNavigator />
</Suspense>
)}
</Stack.Screen>
<Stack.Screen
name="ClassDemo"
component={ClassDemoNavigator}
options={{ headerShown: false }} // The class demo will manage its own UI
/>
>
{() => (
<Suspense fallback={<LoadingFallback />}>
<ClassDemoNavigator />
</Suspense>
)}
</Stack.Screen>
</Stack.Navigator>
);
};

const styles = StyleSheet.create({
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#FFFFFF',
},
});

export default RootNavigator;
Loading
Loading