Autenticación Biométrica y Seguridad en Apps Móviles con React Native y Expo

En aplicaciones móviles bancarias, fintech, de salud o gestión empresarial, la autenticación biométrica (Face ID y huella dactilar) proporciona el equilibrio ideal entre máxima seguridad y acceso instantáneo para el usuario. En este artículo aprenderás a integrar un flujo completo de autenticación biométrica en React Native y Expo utilizando
expo-local-authenticationyexpo-secure-store.

¿Por qué no basta con AsyncStorage para credenciales?#
Un error crítico común en el desarrollo de apps móviles es almacenar tokens JWT o claves de sesión en AsyncStorage. En dispositivos Android rooteados o iOS con jailbreak, los datos de AsyncStorage se guardan en texto plano en el sistema de archivos de la app, permitiendo su extracción por actores maliciosos.
Para garantizar seguridad de nivel bancario:
- Keychain (iOS) y Keystore (Android): Debemos utilizar almacenamiento en el enclave seguro del dispositivo mediante
expo-secure-store. - Autenticación Biométrica: Validar la presencia física del usuario antes de desbloquear la clave criptográfica o refrescar el token de sesión.
- Manejo de Fallbacks: Permitir ingreso por PIN o contraseña en caso de fallos de hardware o desconfiguración biométrica.
1. Instalación de Dependencias#
Ejecutamos el comando de instalación en nuestro proyecto Expo:
npx expo install expo-local-authentication expo-secure-store
Y en app.json configuramos los permisos requeridos para iOS y Android:
{
"expo": {
"plugins": [
[
"expo-local-authentication",
{
"faceIDPermission": "Permite a la aplicación utilizar Face ID para acceder de forma rápida y segura a tu cuenta."
}
]
]
}
}
2. Servicio de Gestión Criptográfica y Almacenamiento Seguro#
Creamos un módulo de utilidades src/services/security.service.ts para interactuar de forma segura con el hardware del dispositivo:
// src/services/security.service.ts
import * as LocalAuthentication from 'expo-local-authentication';
import * as SecureStore from 'expo-secure-store';
const AUTH_TOKEN_KEY = 'user_secure_refresh_token';
const BIOMETRIC_ENABLED_KEY = 'biometric_auth_enabled';
export const SecurityService = {
// Verificar si el dispositivo cuenta con sensores biométricos
async checkBiometricCapabilities() {
const hasHardware = await LocalAuthentication.hasHardwareAsync();
const isEnrolled = await LocalAuthentication.isEnrolledAsync();
const supportedTypes = await LocalAuthentication.supportedAuthenticationTypesAsync();
return {
hasHardware,
isEnrolled,
supportsFaceID: supportedTypes.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION),
supportsFingerprint: supportedTypes.includes(LocalAuthentication.AuthenticationType.FINGERPRINT),
};
},
// Solicitar autenticación biométrica al usuario
async promptBiometricAuth(promptMessage = 'Confirma tu identidad para continuar'): Promise<boolean> {
try {
const { hasHardware, isEnrolled } = await this.checkBiometricCapabilities();
if (!hasHardware || !isEnrolled) {
return false;
}
const result = await LocalAuthentication.authenticateAsync({
promptMessage,
fallbackLabel: 'Ingresar con contraseña',
disableDeviceFallback: false,
cancelLabel: 'Cancelar',
});
return result.success;
} catch (error) {
console.error('Error durante la autenticación biométrica:', error);
return false;
}
},
// Guardar token cifrado
async saveSecureToken(token: string) {
await SecureStore.setItemAsync(AUTH_TOKEN_KEY, token, {
keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
},
// Obtener token previa autenticación
async getSecureToken(): Promise<string | null> {
return await SecureStore.getItemAsync(AUTH_TOKEN_KEY);
},
// Eliminar token en cierre de sesión
async removeSecureToken() {
await SecureStore.deleteItemAsync(AUTH_TOKEN_KEY);
},
};
3. Hook React para Autenticación Biométrica#
Creamos un custom hook useBiometrics para gestionar el estado y la interfaz reactiva:
// src/hooks/useBiometrics.ts
import { useState, useEffect, useCallback } from 'react';
import { SecurityService } from '../services/security.service';
export function useBiometrics() {
const [isCompatible, setIsCompatible] = useState(false);
const [biometryType, setBiometryType] = useState<'FaceID' | 'Fingerprint' | 'None'>('None');
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function initBiometrics() {
try {
const capabilities = await SecurityService.checkBiometricCapabilities();
setIsCompatible(capabilities.hasHardware && capabilities.isEnrolled);
if (capabilities.supportsFaceID) {
setBiometryType('FaceID');
} else if (capabilities.supportsFingerprint) {
setBiometryType('Fingerprint');
} else {
setBiometryType('None');
}
} finally {
setIsLoading(false);
}
}
initBiometrics();
}, []);
const authenticate = useCallback(async (customMessage?: string) => {
return await SecurityService.promptBiometricAuth(customMessage);
}, []);
return {
isCompatible,
biometryType,
isLoading,
authenticate,
};
}
4. Componente de Pantalla de Bloqueo / Desbloqueo#
// src/components/BiometricLockScreen.tsx
import React, { useEffect } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { useBiometrics } from '../hooks/useBiometrics';
interface LockScreenProps {
onUnlockSuccess: () => void;
onFallbackToPassword: () => void;
}
export function BiometricLockScreen({ onUnlockSuccess, onFallbackToPassword }: LockScreenProps) {
const { isCompatible, biometryType, authenticate } = useBiometrics();
const handleBiometricLogin = async () => {
const success = await authenticate('Desbloquea tu billetera digital');
if (success) {
onUnlockSuccess();
}
};
useEffect(() => {
if (isCompatible) {
handleBiometricLogin();
}
}, [isCompatible]);
return (
<View style={styles.container}>
<Text style={styles.title}>Billetera Protegida</Text>
<Text style={styles.subtitle}>
Autentícate con {biometryType === 'FaceID' ? 'Face ID' : 'tu huella dactilar'} para acceder.
</Text>
<TouchableOpacity style={styles.primaryButton} onPress={handleBiometricLogin}>
<Text style={styles.buttonText}>
Usar {biometryType === 'FaceID' ? 'Face ID' : 'Biometría'}
</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.secondaryButton} onPress={onFallbackToPassword}>
<Text style={styles.secondaryButtonText}>Ingresar con Contraseña</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#090D16',
justifyContent: 'center',
alignItems: 'center',
padding: 24,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#FFFFFF',
marginBottom: 8,
},
subtitle: {
fontSize: 15,
color: '#94A3B8',
textAlign: 'center',
marginBottom: 32,
},
primaryButton: {
backgroundColor: '#3B82F6',
paddingVertical: 16,
paddingHorizontal: 32,
borderRadius: 12,
width: '100%',
alignItems: 'center',
marginBottom: 12,
},
buttonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: 'bold',
},
secondaryButton: {
paddingVertical: 14,
},
secondaryButtonText: {
color: '#60A5FA',
fontSize: 15,
},
});
Recomendaciones para Producción#
- Rotación de tokens de refresco: Al autenticar biométricamente, solicita un nuevo token de acceso a tu backend para invalidar sesiones antiguas.
- Detección de cambios de biometría: Si un usuario agrega una nueva huella en los ajustes del sistema operativo, invalida el token almacenado y solicita re-autenticación con contraseña para evitar accesos no autorizados.
¿Estás desarrollando una aplicación móvil en iOS o Android y necesitas seguridad robusta, sincronización offline o pasarelas de pago? Explora nuestros proyectos de Apps Móviles o solicita una cotización aquí.
¿Te gustaría profundizar en estos temas?
Aprende sobre desarrollo de software, apps a medida, automatizaciones con N8N, Next.js y Cloud con casos reales.


