2025-09-04
Real-World Session Management in React Native with Auth0 and Biometrics
Step-by-step guide to implementing secure session management with Auth0, biometric authentication, and proper token lifecycle handling in production React Native applications
Abstract
Session management in mobile applications presents unique challenges that web developers rarely encounter. Mobile apps must handle background states, network interruptions, biometric authentication, and platform-specific security constraints while maintaining a seamless user experience. This guide provides a proven approach to implementing robust session management in React Native applications using Auth0 and biometric authentication.
In this tutorial, you’ll build a complete session management system that handles token lifecycle, implements biometric authentication, manages background operations, and gracefully recovers from edge cases. The implementation focuses on real-world scenarios and provides effective patterns that scale from startup MVPs to enterprise applications.
Prerequisites
Before starting this implementation, ensure you have:
- React Native development environment configured (React Native 0.78+)
- Auth0 account with a configured application
- iOS and Android development environments set up
- Basic understanding of OAuth 2.0 and JWT tokens
- Familiarity with React Native navigation and state management
Required packages:
{
"react-native": "^0.78.0",
"react-native-auth0": "^4.6.0",
"react-native-biometrics": "^3.0.1",
"react-native-keychain": "^10.0.0",
"@react-native-async-storage/async-storage": "^1.21.0",
"@react-native-community/netinfo": "^11.2.0",
"react-native-background-fetch": "^4.2.0"
}
Compatibility Notes:
- React Native New Architecture: All packages are compatible with the New Architecture (Fabric/TurboModules)
- Expo: Compatible with Expo dev builds (requires custom development client for native dependencies)
- iOS Background Limitations: Background fetch is limited to system-scheduled intervals and may not work reliably for token refresh
Step 1: Understanding Session Management Architecture
Before implementing code, let’s establish the core concepts and architecture that will guide our implementation.
Session States and Lifecycle
Mobile applications require sophisticated state management to handle various authentication scenarios:
// types/AuthState.ts
export enum UserState {
UNAUTHENTICATED = 'unauthenticated',
AUTHENTICATING = 'authenticating',
AUTHENTICATED = 'authenticated',
SESSION_EXPIRED = 'session_expired',
REQUIRES_VERIFICATION = 'requires_verification',
CHECKING_AUTH = 'checking_auth',
LOGGING_OUT = 'logging_out'
}
export interface AuthState {
userState: UserState;
user: User | null;
tokens: {
accessToken: string | null;
refreshToken: string | null;
idToken: string | null;
expiresAt: number | null;
};
lastActivity: number;
biometricEnabled: boolean;
sessionStartTime: number;
}
export interface User {
id: string;
email: string;
name: string;
picture?: string;
emailVerified: boolean;
}
Token Architecture
Understanding the three-token system is crucial:
- Access Token (15 minutes): Used for API authorization
- Refresh Token (30 days): Used to obtain new access tokens
- ID Token (1 hour): Contains user profile information
Step 2: Setting Up Auth0 Integration
Create a robust Auth0 service that handles all authentication operations:
// services/auth/AuthService.ts
import Auth0 from 'react-native-auth0';
import Config from 'react-native-config';
class AuthService {
private auth0: Auth0;
private readonly AUTH0_SCOPE = 'openid profile email offline_access';
constructor() {
this.auth0 = new Auth0({
domain: Config.AUTH0_DOMAIN,
clientId: Config.AUTH0_CLIENT_ID,
});
}
async login(): Promise<Credentials> {
try {
const credentials = await this.auth0.webAuth.authorize({
scope: this.AUTH0_SCOPE,
audience: Config.AUTH0_AUDIENCE,
prompt: 'login',
// Additional parameters for mobile optimization
parameters: {
device: 'mobile'
}
});
return this.processCredentials(credentials);
} catch (error) {
if (error.error === 'a0.session.user_cancelled') {
throw new Error('User cancelled login');
}
throw error;
}
}
async refreshTokens(refreshToken: string): Promise<Credentials> {
try {
// Auth0 SDK v4+ uses renew method for token refresh
const credentials = await this.auth0.auth.renew({
refreshToken,
scope: this.AUTH0_SCOPE
});
return this.processCredentials(credentials);
} catch (error) {
if (error.error === 'invalid_grant') {
throw new Error('Refresh token expired or revoked');
}
throw error;
}
}
async logout(): Promise<void> {
// Clear Auth0 session and cookies
await this.auth0.webAuth.clearSession();
}
async getUserInfo(accessToken: string): Promise<User> {
const userInfo = await this.auth0.auth.userInfo({ token: accessToken });
return {
id: userInfo.sub,
email: userInfo.email,
name: userInfo.name || userInfo.nickname,
picture: userInfo.picture,
emailVerified: userInfo.email_verified
};
}
private processCredentials(credentials: any): Credentials {
return {
accessToken: credentials.accessToken,
refreshToken: credentials.refreshToken,
idToken: credentials.idToken,
expiresIn: credentials.expiresIn,
expiresAt: Date.now() + (credentials.expiresIn * 1000),
tokenType: credentials.tokenType,
scope: credentials.scope
};
}
}
export default new AuthService();
Step 3: Implementing Secure Token Storage
Store tokens securely using platform-specific secure storage:
// services/storage/SecureStorage.ts
import * as Keychain from 'react-native-keychain';
import AsyncStorage from '@react-native-async-storage/async-storage';
import CryptoJS from 'crypto-js';
class SecureStorage {
private readonly KEYCHAIN_SERVICE = 'com.yourapp.oauth';
private readonly KEYCHAIN_ACCESS_GROUP = 'group.com.yourapp';
async storeTokens(tokens: TokenSet): Promise<void> {
try {
// Store sensitive tokens in Keychain
await Keychain.setInternetCredentials(
this.KEYCHAIN_SERVICE,
'tokens',
JSON.stringify({
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresAt: tokens.expiresAt
}),
{
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
accessGroup: this.KEYCHAIN_ACCESS_GROUP,
authenticatePrompt: 'Authenticate to access your account',
authenticationPromptType: Keychain.AUTHENTICATION_TYPE.BIOMETRICS
}
);
// Store non-sensitive data in AsyncStorage
await AsyncStorage.setItem('@auth_metadata', JSON.stringify({
expiresAt: tokens.expiresAt,
scope: tokens.scope,
tokenType: tokens.tokenType
}));
} catch (error) {
console.error('Failed to store tokens securely:', error);
throw new Error('Secure storage failed');
}
}
async getTokens(): Promise<TokenSet | null> {
try {
const credentials = await Keychain.getInternetCredentials(
this.KEYCHAIN_SERVICE
);
if (!credentials) {
return null;
}
const tokens = JSON.parse(credentials.password);
const metadata = await AsyncStorage.getItem('@auth_metadata');
return {
...tokens,
...(metadata ? JSON.parse(metadata) : {})
};
} catch (error) {
console.error('Failed to retrieve tokens:', error);
return null;
}
}
async clearTokens(): Promise<void> {
await Keychain.resetInternetCredentials(this.KEYCHAIN_SERVICE);
await AsyncStorage.removeItem('@auth_metadata');
}
async hasStoredCredentials(): Promise<boolean> {
try {
const credentials = await Keychain.hasInternetCredentials(
this.KEYCHAIN_SERVICE
);
return credentials;
} catch {
return false;
}
}
}
export default new SecureStorage();
Step 4: Implementing Biometric Authentication
Use the modern react-native-biometrics library for biometric authentication:
// services/auth/BiometricService.ts
import ReactNativeBiometrics, { BiometryTypes } from 'react-native-biometrics';
import SecureStorage from '../storage/SecureStorage';
class BiometricService {
private rnBiometrics: ReactNativeBiometrics;
constructor() {
this.rnBiometrics = new ReactNativeBiometrics({
allowDeviceCredentials: true
});
}
async isBiometricAvailable(): Promise<{
available: boolean;
biometryType: BiometryTypes | null;
}> {
try {
const { available, biometryType } = await this.rnBiometrics.isSensorAvailable();
return { available, biometryType };
} catch (error) {
console.error('Biometric check failed:', error);
return { available: false, biometryType: null };
}
}
async authenticateWithBiometrics(): Promise<AuthenticationResult> {
try {
const { available, biometryType } = await this.isBiometricAvailable();
if (!available) {
return {
success: false,
error: 'Biometric authentication not available'
};
}
// Create signature for authentication
const { success, signature } = await this.rnBiometrics.createSignature({
promptMessage: 'Authenticate to access your account',
payload: Date.now().toString(),
cancelButtonText: 'Cancel'
});
if (!success) {
return {
success: false,
error: 'Biometric authentication failed'
};
}
// Retrieve stored tokens after successful authentication
const tokens = await SecureStorage.getTokens();
if (!tokens) {
return {
success: false,
error: 'No stored credentials found'
};
}
// Check if tokens need refresh
if (this.shouldRefreshTokens(tokens)) {
const refreshedTokens = await AuthService.refreshTokens(tokens.refreshToken);
await SecureStorage.storeTokens(refreshedTokens);
return { success: true, tokens: refreshedTokens };
}
return { success: true, tokens };
} catch (error) {
console.error('Biometric authentication error:', error);
return {
success: false,
error: error.message || 'Unknown error occurred'
};
}
}
async enrollBiometrics(): Promise<boolean> {
try {
const { available } = await this.isBiometricAvailable();
if (!available) {
return false;
}
// Generate key pair for biometric enrollment
const { publicKey } = await this.rnBiometrics.createKeys();
// Store public key for future verification
await AsyncStorage.setItem('@biometric_public_key', publicKey);
return true;
} catch (error) {
console.error('Biometric enrollment failed:', error);
return false;
}
}
async deleteBiometricKeys(): Promise<void> {
await this.rnBiometrics.deleteKeys();
await AsyncStorage.removeItem('@biometric_public_key');
}
private shouldRefreshTokens(tokens: TokenSet): boolean {
const REFRESH_THRESHOLD = 5 * 60 * 1000; // 5 minutes
const timeUntilExpiry = tokens.expiresAt - Date.now();
return timeUntilExpiry < REFRESH_THRESHOLD;
}
}
export default new BiometricService();
Step 5: Building the Token Manager
Implement automatic token refresh with retry logic and network awareness:
// services/auth/TokenManager.ts
import NetInfo from '@react-native-community/netinfo';
import BackgroundFetch from 'react-native-background-fetch';
import AuthService from './AuthService';
import SecureStorage from '../storage/SecureStorage';
class TokenManager {
private refreshTimer: NodeJS.Timeout | null = null;
private refreshPromise: Promise<void> | null = null;
private readonly REFRESH_THRESHOLD = 5 * 60 * 1000; // 5 minutes
private readonly MAX_RETRY_ATTEMPTS = 3;
private readonly RETRY_DELAY_BASE = 1000; // 1 second
async initialize(): Promise<void> {
// Set up automatic token refresh
await this.scheduleTokenRefresh();
// Configure background fetch for token refresh
await this.configureBackgroundRefresh();
// Listen for network changes
NetInfo.addEventListener(this.handleNetworkChange);
}
async scheduleTokenRefresh(): Promise<void> {
// Clear existing timer
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
}
const tokens = await SecureStorage.getTokens();
if (!tokens || !tokens.expiresAt) {
return;
}
const timeUntilExpiry = tokens.expiresAt - Date.now();
const refreshTime = Math.max(0, timeUntilExpiry - this.REFRESH_THRESHOLD);
if (refreshTime > 0) {
this.refreshTimer = setTimeout(() => {
this.performTokenRefresh();
}, refreshTime);
} else {
// Token needs immediate refresh
await this.performTokenRefresh();
}
}
async performTokenRefresh(): Promise<void> {
// Prevent concurrent refresh attempts
if (this.refreshPromise) {
return this.refreshPromise;
}
this.refreshPromise = this.doRefreshWithRetry();
try {
await this.refreshPromise;
} finally {
this.refreshPromise = null;
}
}
private async doRefreshWithRetry(): Promise<void> {
let lastError: Error | null = null;
for (let attempt = 0; attempt < this.MAX_RETRY_ATTEMPTS; attempt++) {
try {
// Check network connectivity
const netInfo = await NetInfo.fetch();
if (!netInfo.isConnected) {
throw new Error('No network connection');
}
// Get current tokens
const tokens = await SecureStorage.getTokens();
if (!tokens || !tokens.refreshToken) {
throw new Error('No refresh token available');
}
// Perform refresh
const newTokens = await AuthService.refreshTokens(tokens.refreshToken);
// Store new tokens
await SecureStorage.storeTokens(newTokens);
// Schedule next refresh
await this.scheduleTokenRefresh();
// Emit success event
this.emitTokenRefreshSuccess();
return;
} catch (error) {
lastError = error;
if (error.message === 'Refresh token expired or revoked') {
// Can't recover from this - need user to log in again
this.handleSessionExpired();
return;
}
if (attempt < this.MAX_RETRY_ATTEMPTS - 1) {
// Calculate exponential backoff delay
const delay = this.RETRY_DELAY_BASE * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// All retries failed
console.error('Token refresh failed after all attempts:', lastError);
this.handleSessionExpired();
}
private async configureBackgroundRefresh(): Promise<void> {
await BackgroundFetch.configure({
minimumFetchInterval: 15, // 15 minutes
forceAlarmManager: false,
stopOnTerminate: false,
enableHeadless: true,
startOnBoot: true,
requiredNetworkType: BackgroundFetch.NETWORK_TYPE_ANY
}, async (taskId) => {
try {
await this.performTokenRefresh();
} catch (error) {
console.error('Background token refresh failed:', error);
} finally {
BackgroundFetch.finish(taskId);
}
}, (taskId) => {
console.error('Background fetch timeout');
BackgroundFetch.finish(taskId);
});
// Note: iOS background fetch is severely limited and may not work reliably
// Consider using silent push notifications for critical token refresh scenarios
}
private handleNetworkChange = async (state: any) => {
if (state.isConnected && this.refreshPromise === null) {
// Network reconnected, check if tokens need refresh
const tokens = await SecureStorage.getTokens();
if (tokens && this.shouldRefreshTokens(tokens)) {
await this.performTokenRefresh();
}
}
};
private shouldRefreshTokens(tokens: TokenSet): boolean {
const timeUntilExpiry = tokens.expiresAt - Date.now();
return timeUntilExpiry < this.REFRESH_THRESHOLD;
}
private handleSessionExpired(): void {
// Clear tokens
SecureStorage.clearTokens();
// Emit session expired event
this.emitSessionExpired();
}
private emitTokenRefreshSuccess(): void {
// Emit event for app to handle
EventEmitter.emit('auth:token-refreshed');
}
private emitSessionExpired(): void {
// Emit event for app to handle
EventEmitter.emit('auth:session-expired');
}
cleanup(): void {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
}
BackgroundFetch.stop();
}
}
export default new TokenManager();
Step 6: Managing App State Transitions
Handle app state changes and implement idle timeout:
// hooks/useAppStateAuth.ts
import { useEffect, useRef } from 'react';
import { AppState, AppStateStatus } from 'react-native';
import BiometricService from '../services/auth/BiometricService';
import TokenManager from '../services/auth/TokenManager';
interface AppStateAuthConfig {
idleTimeout?: number; // milliseconds
requireBiometricOnForeground?: boolean;
onSessionExpired?: () => void;
onAuthRequired?: () => void;
}
export const useAppStateAuth = (config: AppStateAuthConfig = {}) => {
const {
idleTimeout = 30 * 60 * 1000, // 30 minutes default
requireBiometricOnForeground = true,
onSessionExpired,
onAuthRequired
} = config;
const appState = useRef(AppState.currentState);
const backgroundTime = useRef<number>(0);
useEffect(() => {
const handleAppStateChange = async (nextAppState: AppStateStatus) => {
// App coming to foreground
if (
appState.current.match(/inactive|background/) &&
nextAppState === 'active'
) {
const timeInBackground = Date.now() - backgroundTime.current;
if (timeInBackground > idleTimeout) {
// Session timed out
onSessionExpired?.();
} else if (requireBiometricOnForeground && timeInBackground > 60000) {
// Require biometric after 1 minute in background
const result = await BiometricService.authenticateWithBiometrics();
if (!result.success) {
onAuthRequired?.();
} else {
// Refresh tokens if needed
await TokenManager.scheduleTokenRefresh();
}
} else {
// Just ensure tokens are fresh
await TokenManager.scheduleTokenRefresh();
}
}
// App going to background
else if (
appState.current === 'active' &&
nextAppState.match(/inactive|background/)
) {
backgroundTime.current = Date.now();
}
appState.current = nextAppState;
};
const subscription = AppState.addEventListener('change', handleAppStateChange);
return () => {
subscription.remove();
};
}, [idleTimeout, requireBiometricOnForeground, onSessionExpired, onAuthRequired]);
};
Step 7: Implementing the Auth Context
Create a comprehensive auth context that manages all authentication state:
// contexts/AuthContext.tsx
import React, { createContext, useContext, useEffect, useReducer } from 'react';
import AuthService from '../services/auth/AuthService';
import BiometricService from '../services/auth/BiometricService';
import SecureStorage from '../services/storage/SecureStorage';
import TokenManager from '../services/auth/TokenManager';
import { useAppStateAuth } from '../hooks/useAppStateAuth';
interface AuthContextValue {
state: AuthState;
login: () => Promise<void>;
logout: () => Promise<void>;
authenticateWithBiometrics: () => Promise<boolean>;
enableBiometrics: () => Promise<boolean>;
checkAuthStatus: () => Promise<void>;
}
const AuthContext = createContext<AuthContextValue | null>(null);
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
const authReducer = (state: AuthState, action: any): AuthState => {
switch (action.type) {
case 'SET_USER_STATE':
return { ...state, userState: action.payload };
case 'SET_AUTH_DATA':
return {
...state,
userState: UserState.AUTHENTICATED,
user: action.payload.user,
tokens: action.payload.tokens,
sessionStartTime: Date.now(),
lastActivity: Date.now()
};
case 'CLEAR_AUTH':
return {
...state,
userState: UserState.UNAUTHENTICATED,
user: null,
tokens: {
accessToken: null,
refreshToken: null,
idToken: null,
expiresAt: null
},
sessionStartTime: 0,
lastActivity: 0
};
case 'SET_BIOMETRIC_ENABLED':
return { ...state, biometricEnabled: action.payload };
default:
return state;
}
};
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(authReducer, {
userState: UserState.CHECKING_AUTH,
user: null,
tokens: {
accessToken: null,
refreshToken: null,
idToken: null,
expiresAt: null
},
lastActivity: 0,
biometricEnabled: false,
sessionStartTime: 0
});
// Handle app state changes
useAppStateAuth({
idleTimeout: 30 * 60 * 1000,
requireBiometricOnForeground: state.biometricEnabled,
onSessionExpired: () => {
dispatch({ type: 'CLEAR_AUTH' });
},
onAuthRequired: () => {
dispatch({ type: 'SET_USER_STATE', payload: UserState.REQUIRES_VERIFICATION });
}
});
// Check initial auth status
useEffect(() => {
checkAuthStatus();
// Listen for session expired events
const handleSessionExpired = () => {
dispatch({ type: 'CLEAR_AUTH' });
};
EventEmitter.addListener('auth:session-expired', handleSessionExpired);
return () => {
EventEmitter.removeListener('auth:session-expired', handleSessionExpired);
};
}, []);
const checkAuthStatus = async () => {
try {
dispatch({ type: 'SET_USER_STATE', payload: UserState.CHECKING_AUTH });
const tokens = await SecureStorage.getTokens();
if (!tokens || !tokens.accessToken) {
dispatch({ type: 'CLEAR_AUTH' });
return;
}
// Check if tokens are expired
if (tokens.expiresAt && tokens.expiresAt < Date.now()) {
// Try to refresh
try {
await TokenManager.performTokenRefresh();
const newTokens = await SecureStorage.getTokens();
if (newTokens) {
const user = await AuthService.getUserInfo(newTokens.accessToken);
dispatch({
type: 'SET_AUTH_DATA',
payload: { user, tokens: newTokens }
});
// Initialize token manager
await TokenManager.initialize();
}
} catch {
dispatch({ type: 'CLEAR_AUTH' });
}
} else {
// Tokens are still valid
const user = await AuthService.getUserInfo(tokens.accessToken);
dispatch({
type: 'SET_AUTH_DATA',
payload: { user, tokens }
});
// Initialize token manager
await TokenManager.initialize();
}
// Check biometric enrollment
const { available } = await BiometricService.isBiometricAvailable();
if (available) {
const enrolled = await AsyncStorage.getItem('@biometric_enrolled');
dispatch({ type: 'SET_BIOMETRIC_ENABLED', payload: enrolled === 'true' });
}
} catch (error) {
console.error('Auth status check failed:', error);
dispatch({ type: 'CLEAR_AUTH' });
}
};
const login = async () => {
try {
dispatch({ type: 'SET_USER_STATE', payload: UserState.AUTHENTICATING });
const credentials = await AuthService.login();
await SecureStorage.storeTokens(credentials);
const user = await AuthService.getUserInfo(credentials.accessToken);
dispatch({
type: 'SET_AUTH_DATA',
payload: { user, tokens: credentials }
});
// Initialize token manager
await TokenManager.initialize();
// Offer biometric enrollment
const { available } = await BiometricService.isBiometricAvailable();
if (available && !state.biometricEnabled) {
// Prompt user to enable biometrics
// This would typically show a modal or navigate to settings
}
} catch (error) {
dispatch({ type: 'CLEAR_AUTH' });
throw error;
}
};
const logout = async () => {
try {
dispatch({ type: 'SET_USER_STATE', payload: UserState.LOGGING_OUT });
// Cleanup token manager
TokenManager.cleanup();
// Clear Auth0 session
await AuthService.logout();
// Clear stored tokens
await SecureStorage.clearTokens();
// Clear biometric keys if enabled
if (state.biometricEnabled) {
await BiometricService.deleteBiometricKeys();
}
dispatch({ type: 'CLEAR_AUTH' });
} catch (error) {
console.error('Logout error:', error);
// Force logout even if something fails
dispatch({ type: 'CLEAR_AUTH' });
}
};
const authenticateWithBiometrics = async (): Promise<boolean> => {
const result = await BiometricService.authenticateWithBiometrics();
if (result.success && result.tokens) {
const user = await AuthService.getUserInfo(result.tokens.accessToken);
dispatch({
type: 'SET_AUTH_DATA',
payload: { user, tokens: result.tokens }
});
return true;
}
return false;
};
const enableBiometrics = async (): Promise<boolean> => {
const enrolled = await BiometricService.enrollBiometrics();
if (enrolled) {
await AsyncStorage.setItem('@biometric_enrolled', 'true');
dispatch({ type: 'SET_BIOMETRIC_ENABLED', payload: true });
return true;
}
return false;
};
const value = {
state,
login,
logout,
authenticateWithBiometrics,
enableBiometrics,
checkAuthStatus
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
Step 8: Creating Protected Routes
Implement navigation guards that respect authentication state:
// navigation/AuthNavigator.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useAuth } from '../contexts/AuthContext';
import { UserState } from '../types/AuthState';
// Import screens
import LoginScreen from '../screens/LoginScreen';
import BiometricPromptScreen from '../screens/BiometricPromptScreen';
import HomeScreen from '../screens/HomeScreen';
import LoadingScreen from '../screens/LoadingScreen';
const Stack = createNativeStackNavigator();
export const AuthNavigator: React.FC = () => {
const { state } = useAuth();
if (state.userState === UserState.CHECKING_AUTH) {
return <LoadingScreen />;
}
return (
<NavigationContainer>
<Stack.Navigator screenOptions={{ headerShown: false }}>
{state.userState === UserState.AUTHENTICATED ? (
<Stack.Group>
<Stack.Screen name="Home" component={HomeScreen} />
{/* Add other authenticated screens */}
</Stack.Group>
) : state.userState === UserState.REQUIRES_VERIFICATION ? (
<Stack.Screen name="BiometricPrompt" component={BiometricPromptScreen} />
) : (
<Stack.Screen name="Login" component={LoginScreen} />
)}
</Stack.Navigator>
</NavigationContainer>
);
};
Troubleshooting Common Issues
Token Refresh Failures
Problem: Token refresh fails with “invalid_grant” error.
Solution: This typically occurs when the refresh token has been revoked or expired. Implement proper error handling:
// In TokenManager
if (error.error === 'invalid_grant') {
// Clear local tokens
await SecureStorage.clearTokens();
// Force re-authentication
EventEmitter.emit('auth:session-expired');
}
Biometric Authentication Loop
Problem: App continuously prompts for biometric authentication.
Solution: Implement a backoff mechanism:
class BiometricService {
private failureCount = 0;
private readonly MAX_FAILURES = 3;
async authenticateWithBiometrics(): Promise<AuthenticationResult> {
if (this.failureCount >= this.MAX_FAILURES) {
// Fall back to password authentication
return { success: false, error: 'Max attempts exceeded' };
}
const result = await this.performBiometricAuth();
if (!result.success) {
this.failureCount++;
} else {
this.failureCount = 0;
}
return result;
}
}
Background Refresh on iOS
Problem: Background refresh doesn’t work reliably on iOS.
Solution: iOS has strict limitations. Use silent push notifications for critical updates:
// AppDelegate.m
- (void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
// Trigger token refresh
[RNBackgroundFetch performFetchWithCompletionHandler:completionHandler];
}
Network State Handling
Problem: Token refresh fails when network is unstable.
Solution: Implement queue-based retry with network monitoring:
class NetworkAwareTokenManager {
private refreshQueue: Array<() => Promise<void>> = [];
private isOnline = true;
constructor() {
NetInfo.addEventListener(state => {
const wasOffline = !this.isOnline;
this.isOnline = state.isConnected;
if (wasOffline && this.isOnline) {
this.processQueue();
}
});
}
async queueRefresh(): Promise<void> {
return new Promise((resolve, reject) => {
const task = async () => {
try {
await this.performTokenRefresh();
resolve();
} catch (error) {
reject(error);
}
};
if (this.isOnline) {
task();
} else {
this.refreshQueue.push(task);
}
});
}
private async processQueue(): Promise<void> {
while (this.refreshQueue.length > 0) {
const task = this.refreshQueue.shift();
if (task) await task();
}
}
}
Security Best Practices
1. Token Storage Security
Always use platform-specific secure storage:
- iOS: Keychain with
kSecAccessControlBiometryCurrentSet - Android: Android Keystore with user authentication required
2. Certificate Pinning
Implement certificate pinning for Auth0 endpoints (React Native 0.78+ with Network Security Config):
// ios/YourApp/Info.plist
<key>NSAppTransportSecurity</key>
<dict>
<key>NSPinnedDomains</key>
<dict>
<key>your-tenant.auth0.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSPinnedCAIdentities</key>
<array>
<dict>
<key>SPKI-SHA256-BASE64</key>
<string>YOUR_PIN_HERE</string>
</dict>
</array>
</dict>
</dict>
</dict>
<!-- android/app/src/main/res/xml/network_security_config.xml -->
<network-security-config>
<domain-config>
<domain includeSubdomains="true">your-tenant.auth0.com</domain>
<pin-set>
<pin digest="SHA-256">YOUR_PIN_HERE</pin>
</pin-set>
</domain-config>
</network-security-config>
3. Jailbreak/Root Detection
Detect compromised devices and limit functionality:
import JailMonkey from 'jail-monkey';
const checkDeviceSecurity = (): SecurityStatus => {
if (JailMonkey.isJailBroken()) {
return { secure: false, reason: 'Device is jailbroken/rooted' };
}
if (JailMonkey.isDebuggedMode()) {
return { secure: false, reason: 'Debugger detected' };
}
return { secure: true };
};
// Note: With React Native New Architecture, some detection methods may need updates
// Test thoroughly on target devices and OS versions
4. Token Rotation
Enable refresh token rotation in Auth0:
// Auth0 Dashboard > Applications > Settings > Refresh Token Rotation
{
"rotation": {
"enabled": true,
"leeway": 60
}
}
5. Secure Communication
Use encrypted channels for all token operations:
class SecureAPIClient {
private async makeSecureRequest(url: string, options: RequestInit): Promise<Response> {
const tokens = await SecureStorage.getTokens();
if (!tokens || !tokens.accessToken) {
throw new Error('No valid access token');
}
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${tokens.accessToken}`,
'X-Request-ID': generateRequestId(),
'X-App-Version': getAppVersion()
}
});
if (response.status === 401) {
// Token might be expired, try refresh
await TokenManager.performTokenRefresh();
// Retry request with new token
const newTokens = await SecureStorage.getTokens();
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${newTokens.accessToken}`
}
});
}
return response;
}
}
Performance Optimization Tips
1. Token Caching Strategy
Implement memory caching with TTL:
class TokenCache {
private cache: Map<string, { value: any; expires: number }> = new Map();
set(key: string, value: any, ttl: number): void {
this.cache.set(key, {
value,
expires: Date.now() + ttl
});
}
get(key: string): any | null {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() > item.expires) {
this.cache.delete(key);
return null;
}
return item.value;
}
}
2. Batch Token Operations
Reduce Keychain access by batching operations:
class BatchedSecureStorage {
private pendingWrites: Map<string, any> = new Map();
private writeTimer: NodeJS.Timeout | null = null;
async set(key: string, value: any): Promise<void> {
this.pendingWrites.set(key, value);
this.scheduleWrite();
}
private scheduleWrite(): void {
if (this.writeTimer) return;
this.writeTimer = setTimeout(() => {
this.flushWrites();
this.writeTimer = null;
}, 100);
}
private async flushWrites(): Promise<void> {
const writes = Array.from(this.pendingWrites.entries());
this.pendingWrites.clear();
await Promise.all(
writes.map(([key, value]) =>
Keychain.setInternetCredentials(this.SERVICE, key, value)
)
);
}
}
3. Preemptive Token Refresh
Refresh tokens before they expire to avoid delays:
class PreemptiveTokenManager {
private readonly PREEMPTIVE_REFRESH_WINDOW = 10 * 60 * 1000; // 10 minutes
async getValidToken(): Promise<string> {
const tokens = await SecureStorage.getTokens();
if (!tokens) {
throw new Error('No tokens available');
}
const timeUntilExpiry = tokens.expiresAt - Date.now();
// Refresh preemptively if within window
if (timeUntilExpiry < this.PREEMPTIVE_REFRESH_WINDOW) {
// Don't wait for refresh to complete
this.performTokenRefresh().catch(console.error);
}
return tokens.accessToken;
}
}
Next Steps
After implementing this session management system, consider these enhancements:
- Multi-Factor Authentication: Add TOTP or SMS-based 2FA
- Device Trust: Implement device fingerprinting and trusted device management
- Session Analytics: Track session duration, refresh patterns, and security events
- Offline Support: Implement offline token validation and sync when online
- Advanced Biometrics: Add fallback mechanisms and biometric change detection
For production deployment:
- Configure Auth0 tenant settings for mobile optimization
- Set up monitoring for token refresh failures
- Implement proper error tracking and alerting
- Add comprehensive logging for security events
- Test on various devices and OS versions
Conclusion
Implementing robust session management in React Native requires careful consideration of mobile-specific constraints and security requirements. This implementation provides a production-ready foundation that handles the complexities of token lifecycle, biometric authentication, and edge cases that often cause issues in production.
Key insights from this implementation:
- Use Auth0’s built-in token rotation features instead of manual management
- Implement proper retry logic with exponential backoff
- Handle network state changes gracefully
- Use platform-specific secure storage mechanisms
- Implement preemptive token refresh to avoid user disruption
Remember that security is an ongoing process. Regular security audits, dependency updates, and monitoring of authentication patterns will help maintain a secure and reliable authentication system.
Related posts
Step-by-step guide to integrating Sentry error monitoring into a React Native Expo app. Covers SDK initialization, Expo Router instrumentation, session replay, source map uploads for EAS Build and EAS Update, and common pitfalls to avoid.
Real-world implementation of Auth0 multi-audience authentication across micro frontends, token management strategies, and silent authentication in React Native with WebView-based micro frontends
A practical comparison of TypeScript AI SDKs for building AI agents - Vercel AI SDK, OpenAI Agents SDK, and AWS Bedrock integration. Includes code examples, decision frameworks, and production patterns.
A comprehensive comparison of modern TypeScript linting and formatting tools - ESLint, Prettier, Biome, and Oxlint - with performance benchmarks, configuration examples, and migration strategies.
A comprehensive guide to understanding Effect, learning it incrementally, and integrating it with AWS Lambda. Includes real code examples, common pitfalls, and practical patterns from production usage.