¡Abre Puertas! Implementando Autenticación Biométrica y Credenciales en Android
Este tutorial te guiará a través de la implementación de autenticación biométrica (como huella dactilar o reconocimiento facial) y la gestión segura de credenciales en tus aplicaciones Android. Exploraremos el uso de Jetpack Biometric y el nuevo Credential Manager, proporcionando ejemplos prácticos y las mejores prácticas para mejorar la seguridad y la experiencia del usuario.
🚀 Introducción: La Seguridad al Alcance de tu Mano
En el mundo digital actual, la seguridad es paramount. Los usuarios esperan que sus datos estén protegidos y que el acceso a sus aplicaciones sea a la vez seguro y conveniente. La autenticación biométrica, como la huella dactilar o el reconocimiento facial, y la gestión eficiente de credenciales son dos pilares fundamentales para lograr este equilibrio en Android.
Este tutorial te sumergirá en el corazón de estas tecnologías, enseñándote cómo integrar de forma robusta la autenticación biométrica con la biblioteca Jetpack Biometric y cómo aprovechar el potente y simplificado Credential Manager para una gestión de credenciales sin fisuras y segura.
¿Por qué Autenticación Biométrica y Credenciales?
La autenticación tradicional con contraseñas tiene sus limitaciones: son difíciles de recordar, a menudo se reutilizan y pueden ser vulnerables a ataques de fuerza bruta o phishing. La biometría ofrece una alternativa más rápida y segura, ya que es única para cada usuario y más difícil de comprometer.
Por otro lado, la gestión de credenciales se ha vuelto compleja con la proliferación de cuentas. El Credential Manager de Android simplifica este proceso, permitiendo a los usuarios almacenar y acceder de forma segura a sus credenciales (contraseñas, passkeys, etc.) a través de los administradores de credenciales del sistema, mejorando la experiencia de inicio de sesión.
🛠️ Configuración Inicial del Proyecto Android
Antes de sumergirnos en el código, necesitamos configurar nuestro proyecto Android para incluir las dependencias necesarias. Asegúrate de tener la última versión de Android Studio y un proyecto Kotlin configurado.
Dependencias Esenciales
Abre tu archivo build.gradle.kts (Module: app) y añade las siguientes dependencias:
dependencies {
// Autenticación Biométrica
implementation("androidx.biometric:biometric-ktx:1.2.0")
// Credential Manager (androidx.credentials)
implementation("androidx.credentials:credentials:1.2.0-alpha01") // O la versión estable más reciente
implementation("androidx.credentials:credentials-play-services-auth:1.2.0-alpha01") // Para integración con Google Play Services
}
Sincroniza tu proyecto con Gradle después de añadir las dependencias.
Permisos Necesarios
Para la autenticación biométrica, no se requieren permisos explícitos en el AndroidManifest.xml si usas Jetpack Biometric. Sin embargo, para Credential Manager, aunque muchos casos no requieren permisos explícitos, es bueno tener en cuenta que las APIs de auto-relleno pueden necesitar BIND_AUTOFILL_SERVICE si estás implementando un servicio de auto-relleno personalizado (lo cual va más allá del alcance de este tutorial, pero es útil saberlo).
fingerprint Autenticación Biométrica con Jetpack Biometric
Jetpack Biometric es la biblioteca recomendada para implementar la autenticación biométrica en Android. Proporciona una interfaz unificada y gestiona las diferencias entre las distintas versiones y hardware de Android, ofreciendo un flujo de autenticación consistente.
Flujo de Autenticación Biométrica
El proceso general para la autenticación biométrica es el siguiente:
- Verificar la disponibilidad: Comprobar si el dispositivo tiene hardware biométrico y si el usuario ha registrado alguna credencial biométrica.
- Crear un
BiometricPrompt: Configurar el diálogo que se mostrará al usuario. - Configurar un
BiometricPrompt.AuthenticationCallback: Manejar los resultados de la autenticación (éxito, fallo, error). - Iniciar la autenticación: Mostrar el
BiometricPrompt.
1. Comprobando la Disponibilidad Biométrico
Es crucial verificar si el dispositivo soporta biometría y si hay credenciales enrolladas antes de intentar autenticar. Esto mejora la experiencia del usuario y evita errores.
import androidx.biometric.BiometricManager
fun checkBiometricAvailability(activity: AppCompatActivity): Int {
val biometricManager = BiometricManager.from(activity)
return biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.BIOMETRIC_WEAK or BiometricManager.Authenticators.DEVICE_CREDENTIAL)
}
// En tu Activity o Fragment:
// val availability = checkBiometricAvailability(this)
// when (availability) {
// BiometricManager.BIOMETRIC_SUCCESS -> Log.d("MY_APP_TAG", "Dispositivo compatible y biometría registrada")
// BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> Log.e("MY_APP_TAG", "No hay hardware biométrico")
// BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> Log.e("MY_APP_TAG", "Hardware biométrico no disponible")
// BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> Log.e("MY_APP_TAG", "No hay credenciales biométricas registradas")
// BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> Log.e("MY_APP_TAG", "Actualización de seguridad requerida")
// BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> Log.e("MY_APP_TAG", "Funcionalidad biométrica no soportada")
// BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> Log.e("MY_APP_TAG", "Estado biométrico desconocido")
// else -> Log.e("MY_APP_TAG", "Otro error: $availability")
// }
2. Implementando el BiometricPrompt
Aquí creamos el BiometricPrompt y su callback para manejar los resultados.
import android.os.Build
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.appcompat.app.AppCompatActivity
import android.widget.Toast
class BiometricAuthActivity : AppCompatActivity() {
private lateinit var biometricPrompt: BiometricPrompt
private lateinit var promptInfo: BiometricPrompt.PromptInfo
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_biometric_auth)
// 1. Configurar el ejecutor (Executor) para el callback
val executor = ContextCompat.getMainExecutor(this)
// 2. Configurar el callback
biometricPrompt = BiometricPrompt(this, executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Toast.makeText(applicationContext,
"Error de autenticación: $errString ($errorCode)", Toast.LENGTH_SHORT)
.show()
// Manejar el error, quizás con un fallback a password
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
Toast.makeText(applicationContext,
"¡Autenticación biométrica exitosa!", Toast.LENGTH_SHORT)
.show()
// Continuar con la acción que requiere autenticación
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Toast.makeText(applicationContext,
"Autenticación fallida. Intenta de nuevo.", Toast.LENGTH_SHORT)
.show()
// El usuario no pudo autenticarse (ej. huella no reconocida)
}
})
// 3. Configurar la información del diálogo del prompt
promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Inicio de Sesión Biométrico")
.setSubtitle("Usa tu huella dactilar o rostro para acceder")
.setDescription("Mantén tu información segura con autenticación biométrica.")
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL)
.build()
// En algún lugar donde quieras iniciar la autenticación, por ejemplo, un botón
findViewById<Button>(R.id.biometric_button).setOnClickListener {
if (checkBiometricAvailability(this) == BiometricManager.BIOMETRIC_SUCCESS) {
biometricPrompt.authenticate(promptInfo)
} else {
Toast.makeText(this, "Biometría no disponible o no configurada.", Toast.LENGTH_LONG).show()
// Fallback a un método de autenticación alternativo (ej. contraseña)
}
}
}
}
Este código crea una actividad simple con un botón. Al hacer clic, primero verifica la disponibilidad biométrica y luego lanza el BiometricPrompt. Los callbacks manejan los resultados de la autenticación.
Consideraciones Adicionales para Autenticación Biométrica
- Cifrado con clave segura: Para una seguridad avanzada, puedes integrar
BiometricPromptconKeyGenParameterSpecyCipherpara cifrar y descifrar datos solo después de una autenticación biométrica exitosa. Esto asegura que tus datos sensibles (ej. tokens de autenticación) solo puedan ser accesibles cuando el usuario ha sido verificado biométricamente. Este es un tema más avanzado que implica la generación de claves en el Android Keystore y su uso con autenticación de hardware. - Cancelación del usuario: Cuando el usuario cancela el diálogo o se cierra el diálogo debido a un error, se llama a
onAuthenticationError. Asegúrate de manejar este caso de manera elegante. - Modo de debug: En dispositivos emulados o con la huella dactilar, puedes simular diferentes resultados de autenticación (éxito, fallo) desde el menú de configuración de biometría en el emulador.
🔑 Gestión de Credenciales con Credential Manager
El Credential Manager es una API moderna introducida en Android 14 (y disponible para versiones anteriores a través de Jetpack) que unifica la gestión de credenciales. Permite a los usuarios almacenar y acceder fácilmente a diferentes tipos de credenciales (contraseñas, passkeys, credenciales federadas) y ofrece una experiencia de inicio de sesión más fluida y segura.
Tipos de Credenciales Soportadas
El Credential Manager soporta principalmente:
- PasswordCredential: Credenciales de nombre de usuario y contraseña.
- PublicKeyCredential (Passkeys): Un método de autenticación sin contraseña más seguro que usa criptografía de clave pública.
- IdTokenCredential: Credenciales de identidad como las generadas por Google Sign-In u otros proveedores federados.
Flujo Básico del Credential Manager
- Solicitar Credenciales: La aplicación solicita al sistema una credencial específica.
- El Usuario Elige: El sistema muestra una interfaz donde el usuario puede seleccionar una credencial existente o crear una nueva.
- Recibir Credencial: La aplicación recibe la credencial elegida por el usuario.
- Almacenar Credencial: La aplicación puede pedir al sistema que almacene una nueva credencial (ej. después de un registro exitoso).
1. Solicitando una Credencial Existente (Get Credentials)
El primer paso es solicitar al usuario que seleccione una credencial para iniciar sesión. Esto se hace usando GetCredentialRequest.
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.credentials.CredentialManager
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetCredentialResponse
import androidx.credentials.exceptions.GetCredentialException
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
class CredentialManagerActivity : AppCompatActivity() {
private lateinit var credentialManager: CredentialManager
private val getCredentialLauncher: ActivityResultLauncher<IntentSenderRequest> =
registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
lifecycleScope.launch {
try {
val getCredentialResponse = CredentialManager.parseFromIntent(it.data!!)
handleGetCredentialResponse(getCredentialResponse)
} catch (e: GetCredentialException) {
Log.e("CREDENTIAL_MANAGER", "Error al obtener credencial: ${e.errorMessage}", e)
Toast.makeText(applicationContext, "Error al obtener credencial.", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Log.e("CREDENTIAL_MANAGER", "Error inesperado: ${e.message}", e)
Toast.makeText(applicationContext, "Error inesperado.", Toast.LENGTH_SHORT).show()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_credential_manager)
credentialManager = CredentialManager.create(this)
findViewById<Button>(R.id.login_button).setOnClickListener {
requestCredentials()
}
}
private fun requestCredentials() {
lifecycleScope.launch {
try {
// Crear una solicitud para obtener credenciales
val getCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(PasswordCredential.createGetCredentialOption())
// Puedes añadir otras opciones como Passkey, IdTokenCredential
.build()
val response = credentialManager.getCredential(this@CredentialManagerActivity, getCredentialRequest)
if (response.has {
getCredentialLauncher.launch(IntentSenderRequest.Builder(response.credential.toIntentSender()).build())
} else {
handleGetCredentialResponse(response)
}
} catch (e: GetCredentialException) {
Log.e("CREDENTIAL_MANAGER", "Error al solicitar credencial: ${e.errorMessage}", e)
Toast.makeText(applicationContext, "Error al solicitar credencial.", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Log.e("CREDENTIAL_MANAGER", "Error inesperado al solicitar: ${e.message}", e)
Toast.makeText(applicationContext, "Error inesperado al solicitar.", Toast.LENGTH_SHORT).show()
}
}
}
private fun handleGetCredentialResponse(response: GetCredentialResponse) {
when (val credential = response.credential) {
is PasswordCredential -> {
val username = credential.id
val password = credential.password
Toast.makeText(applicationContext, "Credencial de contraseña obtenida. Usuario: $username", Toast.LENGTH_LONG).show()
Log.d("CREDENTIAL_MANAGER", "Usuario: $username, Contraseña: $password")
// Aquí puedes usar username y password para iniciar sesión en tu backend
}
// Puedes añadir más casos para PublicKeyCredential y IdTokenCredential
else -> {
Toast.makeText(applicationContext, "Tipo de credencial no soportado.", Toast.LENGTH_SHORT).show()
Log.w("CREDENTIAL_MANAGER", "Tipo de credencial desconocido: ${credential.type}")
}
}
}
}
Este código muestra cómo:
- Inicializar
CredentialManager. - Crear un
GetCredentialRequestcon las opciones de credenciales deseadas (en este caso, soloPasswordCredential). - Llamar a
credentialManager.getCredential(). Si se necesita interacción del usuario (ej. para seleccionar una credencial), se devuelve unIntentSenderque se lanza a través deActivityResultLauncher. - Procesar la respuesta en
handleGetCredentialResponsepara extraer el nombre de usuario y la contraseña.
2. Almacenando Nuevas Credenciales (Create Credentials)
Después de que un usuario se registra o cambia su contraseña, es una buena práctica ofrecerle guardar la nueva credencial con el sistema. Esto se hace con CreateCredentialRequest.
import androidx.credentials.CreateCredentialRequest
import androidx.credentials.CreateCredentialResponse
import androidx.credentials.PasswordCredential
import androidx.credentials.exceptions.CreateCredentialException
import kotlinx.coroutines.launch
// ... (dentro de CredentialManagerActivity o una clase similar)
private fun saveCredential(username: String, password: String) {
lifecycleScope.launch {
try {
val request = CreateCredentialRequest.Builder()
.addCredentialOption(PasswordCredential.createSetCredentialOption(username, password))
.build()
val response = credentialManager.createCredential(this@CredentialManagerActivity, request)
handleCreateCredentialResponse(response)
} catch (e: CreateCredentialException) {
Log.e("CREDENTIAL_MANAGER", "Error al guardar credencial: ${e.errorMessage}", e)
Toast.makeText(applicationContext, "Error al guardar credencial.", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Log.e("CREDENTIAL_MANAGER", "Error inesperado al guardar: ${e.message}", e)
Toast.makeText(applicationContext, "Error inesperado al guardar.", Toast.LENGTH_SHORT).show()
}
}
}
private fun handleCreateCredentialResponse(response: CreateCredentialResponse) {
// La respuesta de createCredential generalmente solo indica éxito o fallo.
// No hay credenciales para 'parsear' aquí, ya que la credencial se acaba de crear/guardar.
Toast.makeText(applicationContext, "Credencial guardada exitosamente.", Toast.LENGTH_SHORT).show()
}
// Ejemplo de cómo llamar a saveCredential después de un registro exitoso:
// findViewById<Button>(R.id.register_button).setOnClickListener {
// val newUsername = "usuario_nuevo"
// val newPassword = "mi_contraseña_segura"
// // Lógica para registrar al usuario en tu backend...
// // Si el registro es exitoso:
// saveCredential(newUsername, newPassword)
// }
3. Integración con Passkeys (Claves de Acceso)
Las Passkeys son una forma de autenticación sin contraseña basada en estándares FIDO, mucho más seguras que las contraseñas tradicionales. Credential Manager facilita su integración.
Para solicitar Passkeys:
// Dentro de requestCredentials()
// ...
// Requiere configurar tu servidor para soportar Passkeys FIDO2
.addCredentialOption(PublicKeyCredential.createGetCredentialOption(
relyingPartyServerRequestJson = "{\"challenge\":\"base64url_challenge\", \"rpId\":\"example.com\", ...}",
clientDataHash = null
))
// ...
// Dentro de handleGetCredentialResponse()
// ...
is PublicKeyCredential -> {
val responseJson = credential.authenticationResponseJson
Log.d("CREDENTIAL_MANAGER", "Passkey obtenida: $responseJson")
// Envía responseJson a tu servidor para verificación
}
// ...
Para crear/registrar Passkeys (después de un registro o añadir una nueva Passkey):
// Dentro de saveCredential()
// ...
// Requiere configurar tu servidor para soportar Passkeys FIDO2
.addCredentialOption(PublicKeyCredential.createCreateCredentialOption(
relyingPartyServerNewCredentialRequestJson = "{\"challenge\":\"base64url_challenge\", \"rp\":{\"id\":\"example.com\", ...}, \"user\":{\"id\":\"base64url_user_id\", ...}}",
clientDataHash = null
))
// ...
// Dentro de handleCreateCredentialResponse()
// ...
// Si PublicKeyCredential es la respuesta de creación, podrías necesitar extraer algo del IntentSender.parseFromIntent().
// Generalmente, el servidor es quien verifica la creación de la Passkey.
🤝 Combinando Biometría y Credenciales: Un Flujo de Inicio de Sesión Robusto
La verdadera potencia surge al combinar la conveniencia de la autenticación biométrica con la gestión flexible de credenciales. Imagina un flujo donde, al iniciar la aplicación, si el usuario tiene una credencial biométrica, se le pide autenticarse con ella. Si falla o no está disponible, se le ofrece usar una credencial guardada con Credential Manager.
Estrategia de Implementación
- Preferir Biometría: Al abrir la app o acceder a una sección protegida, intentar autenticar con
BiometricPrompt. - Fallback a Credential Manager: Si la biometría no está disponible, falla, o el usuario la cancela, ofrecer la opción de iniciar sesión utilizando
Credential Manager. - Fallback a Formulario Manual: Si ninguna de las opciones anteriores es viable o el usuario prefiere, ofrecer un formulario de inicio de sesión manual.
// Pseudocódigo para ilustrar la lógica en una Activity de inicio de sesión
fun attemptLoginFlow() {
// 1. Intentar biometría
if (checkBiometricAvailability(this) == BiometricManager.BIOMETRIC_SUCCESS) {
// Mostrar BiometricPrompt
biometricPrompt.authenticate(promptInfo)
} else {
// 2. Si no hay biometría, ir al Credential Manager
requestCredentials()
}
}
// En onAuthenticationError o onAuthenticationFailed del BiometricPrompt callback:
// Toast.makeText(applicationContext, "Biometría fallida/cancelada, intentando con Credential Manager.", Toast.LENGTH_SHORT).show()
// requestCredentials()
// En handleGetCredentialResponse (si se obtuvo PasswordCredential):
// Iniciar sesión con username y password, y si falla, pedir un formulario manual.
// Si es un error de Credential Manager, ir al formulario manual.
Esta secuencia ofrece al usuario la forma más rápida y segura de iniciar sesión primero, degradando elegantemente a métodos menos convenientes si es necesario.
📈 Mejores Prácticas y Consideraciones de Seguridad
- Nunca confíes solo en la biometría: La biometría es para la conveniencia y para desbloquear el acceso a credenciales o funcionalidades. La autenticación real de tu usuario debe suceder en tu servidor, validando credenciales o tokens generados. Si el usuario pierde su dispositivo, la biometría no lo protege.
- Usa
BIOMETRIC_STRONGsiempre que sea posible: Para la máxima seguridad, prioriza las biometrías fuertes. - Ofrece opciones de fallback: Siempre ten un plan B (contraseña, PIN) si la biometría o el Credential Manager fallan o no están disponibles.
- Informar al usuario: Explica claramente por qué solicitas la biometría o qué credencial se está pidiendo/guardando.
- Evita solicitudes repetitivas: No bombardees al usuario con peticiones de biometría o Credential Manager si ya ha fallado varias veces o ha cancelado recientemente. Introduce un pequeño retraso o un contador.
- Protege tus claves privadas: Si estás usando biometría para desbloquear claves cifradas en el Android Keystore, asegúrate de que las claves estén marcadas como
setUserAuthenticationRequired(true)y con un tiempo de validez limitado si es necesario. - Auditoría de seguridad: Considera realizar auditorías de seguridad en tus implementaciones críticas de autenticación.
📝 Resumen y Próximos Pasos
En este tutorial, hemos cubierto en profundidad la implementación de la autenticación biométrica utilizando Jetpack Biometric y la gestión de credenciales con el nuevo Credential Manager. Has aprendido a:
- Configurar las dependencias necesarias.
- Verificar la disponibilidad del hardware biométrico.
- Implementar un
BiometricPromptpara la autenticación. - Solicitar y manejar credenciales existentes con
GetCredentialRequest. - Almacenar nuevas credenciales con
CreateCredentialRequest. - Entender la integración con Passkeys.
- Diseñar un flujo de inicio de sesión robusto combinando ambas tecnologías.
- Aplicar las mejores prácticas de seguridad.
La seguridad y la experiencia del usuario son dos caras de la misma moneda en el desarrollo de aplicaciones. Al dominar estas herramientas, puedes construir aplicaciones más seguras, convenientes y agradables para tus usuarios. ¡Sigue explorando y construyendo!
Tutoriales relacionados
- ¡Dominando las Animaciones! Lottie y MotionLayout en Android Nativo para Interfaces Dinámicasintermediate25 min
- ¡Adiós al Caos! Implementando un Patrón MVI Robusto en tu App Android con Kotlin Flowsintermediate15 min
- ¡Poder y Flexibilidad! Desarrollo de Widgets Interactivos en Android con Jetpack Glanceintermediate15 min
- ¡Poder y Flexibilidad! Construyendo Componentes Reutilizables en Android con Jetpack Compose y Modificadores Personalizadosintermediate25 min
- Optimización del Rendimiento en Android: Eliminando AnR y Mejorando la Fluidez de tu Appintermediate18 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!