Skip to content
~/sph.sh

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:

json
{  "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:

typescript
// types/AuthState.tsexport 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:

  1. Access Token (15 minutes): Used for API authorization
  2. Refresh Token (30 days): Used to obtain new access tokens
  3. ID Token (1 hour): Contains user profile information

Step 2: Setting Up Auth0 Integration

Create a robust Auth0 service that handles all authentication operations:

typescript
// services/auth/AuthService.tsimport 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:

typescript
// services/storage/SecureStorage.tsimport * 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:

typescript
// services/auth/BiometricService.tsimport 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:

typescript
// services/auth/TokenManager.tsimport 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:

typescript
// hooks/useAppStateAuth.tsimport { 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:

typescript
// contexts/AuthContext.tsximport 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:

typescript
// navigation/AuthNavigator.tsximport 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 screensimport 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:

typescript
// In TokenManagerif (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:

typescript
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:

typescript
// 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:

typescript
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):

typescript
// 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>
xml
<!-- 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:

typescript
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:

javascript
// Auth0 Dashboard > Applications > Settings > Refresh Token Rotation{  "rotation": {    "enabled": true,    "leeway": 60  }}

5. Secure Communication

Use encrypted channels for all token operations:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

  1. Multi-Factor Authentication: Add TOTP or SMS-based 2FA
  2. Device Trust: Implement device fingerprinting and trusted device management
  3. Session Analytics: Track session duration, refresh patterns, and security events
  4. Offline Support: Implement offline token validation and sync when online
  5. Advanced Biometrics: Add fallback mechanisms and biometric change detection

For production deployment:

  1. Configure Auth0 tenant settings for mobile optimization
  2. Set up monitoring for token refresh failures
  3. Implement proper error tracking and alerting
  4. Add comprehensive logging for security events
  5. 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