Cómo mostrar un diálogo de autenticación biométrica

Un método para proteger la información sensible o el contenido premium de tu app es solicitar autenticación biométrica, por ejemplo, con reconocimiento facial o de huella digital. En esta guía, se explica cómo admitir flujos de acceso biométrico en tu app.

Como regla general, debes usar Credential Manager para el acceso inicial en un dispositivo. Las reautorizaciones posteriores se pueden realizar con el mensaje biométrico o el Administrador de credenciales. La ventaja de usar el mensaje biométrico es que ofrece más opciones de personalización, mientras que el Administrador de credenciales ofrece una sola implementación en ambos flujos.

Declara los tipos de autenticación que admite tu app

Para definir los tipos de autenticación que admite tu app, usa la interfaz de BiometricManager.Authenticators. El sistema te permite declarar los siguientes tipos de autenticación:

BIOMETRIC_STRONG
Autenticación con un método biométrico de clase 3, como se define en la página Definición de compatibilidad con Android
BIOMETRIC_WEAK
Autenticación con un método biométrico de clase 2, como se define en la página Definición de compatibilidad con Android
DEVICE_CREDENTIAL
Autenticación a través de una credencial de bloqueo de pantalla: el PIN, el patrón o la contraseña del usuario

Para comenzar usando un autenticador, el usuario debe crear un PIN, un patrón o una contraseña. Si el usuario todavía no tiene uno, el flujo de inscripción biométrica le pedirá que lo cree.

Para definir los tipos de autenticación biométrica que acepta tu app, pasa un tipo de autenticación o una combinación de bits de tipos al método setAllowedAuthenticators(). En el siguiente fragmento de código, se muestra cómo admitir la autenticación con un método biométrico de clase 3 o una credencial de bloqueo de pantalla.

Kotlin

// Lets the user authenticate using either a Class 3 biometric or
// their lock screen credential (PIN, pattern, or password).
promptInfo = BiometricPrompt.PromptInfo.Builder()
        .setTitle("Biometric login for my app")
        .setSubtitle("Log in using your biometric credential")
        .setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
        .build()

Java

// Lets user authenticate using either a Class 3 biometric or
// their lock screen credential (PIN, pattern, or password).
promptInfo = new BiometricPrompt.PromptInfo.Builder()
        .setTitle("Biometric login for my app")
        .setSubtitle("Log in using your biometric credential")
        .setAllowedAuthenticators(BIOMETRIC_STRONG | DEVICE_CREDENTIAL)
        .build();

Las siguientes combinaciones de tipos de autenticador no son compatibles con Android 10 (nivel de API 29) y versiones anteriores: DEVICE_CREDENTIAL y BIOMETRIC_STRONG | DEVICE_CREDENTIAL. Para verificar la presencia de un PIN, un patrón o una contraseña en Android 10 y versiones anteriores, usa el método KeyguardManager.isDeviceSecure().

Comprueba que la autenticación biométrica esté disponible

Después de decidir qué elementos de autenticación admite tu app, verifica si esos elementos están disponibles. Para ello, pasa la misma combinación a nivel de bits de tipos que declaraste mediante el método setAllowedAuthenticators() al método canAuthenticate(). Si es necesario, invoca la acción de intent ACTION_BIOMETRIC_ENROLL. En el intent adicional, proporciona el conjunto de autenticadores que acepta tu app. Este intent solicita al usuario que registre las credenciales de un autenticador que acepte tu app.

Kotlin

val biometricManager = BiometricManager.from(this)
when (biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)) {
    BiometricManager.BIOMETRIC_SUCCESS ->
        Log.d("MY_APP_TAG", "App can authenticate using biometrics.")
    BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE ->
        Log.e("MY_APP_TAG", "No biometric features available on this device.")
    BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE ->
        Log.e("MY_APP_TAG", "Biometric features are currently unavailable.")
    BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
        // Prompts the user to create credentials that your app accepts.
        val enrollIntent = Intent(Settings.ACTION_BIOMETRIC_ENROLL).apply {
            putExtra(Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
                BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
        }
        startActivityForResult(enrollIntent, REQUEST_CODE)
    }
}

Java

BiometricManager biometricManager = BiometricManager.from(this);
switch (biometricManager.canAuthenticate(BIOMETRIC_STRONG | DEVICE_CREDENTIAL)) {
    case BiometricManager.BIOMETRIC_SUCCESS:
        Log.d("MY_APP_TAG", "App can authenticate using biometrics.");
        break;
    case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:
        Log.e("MY_APP_TAG", "No biometric features available on this device.");
        break;
    case BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE:
        Log.e("MY_APP_TAG", "Biometric features are currently unavailable.");
        break;
    case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:
        // Prompts the user to create credentials that your app accepts.
        final Intent enrollIntent = new Intent(Settings.ACTION_BIOMETRIC_ENROLL);
        enrollIntent.putExtra(Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
                BIOMETRIC_STRONG | DEVICE_CREDENTIAL);
        startActivityForResult(enrollIntent, REQUEST_CODE);
        break;
}

Determina cómo se autenticó el usuario

Después de que el usuario se autentique, podrás verificar si lo hizo usando una credencial de dispositivo o una credencial biométrica llamando al método getAuthenticationType().

Muestra la solicitud de acceso

Para mostrar un mensaje del sistema que solicite al usuario autenticarse mediante credenciales biométricas, utiliza la biblioteca biométrica. Este diálogo proporcionado por el sistema es coherente en todas las apps que lo usan, lo que crea una experiencia del usuario más confiable. En la Figura 1, aparece un ejemplo de este diálogo.

Captura de pantalla que muestra un diálogo
Figura 1: Diálogo del sistema que solicita una autenticación biométrica

Para agregar una autenticación biométrica a tu app con la biblioteca Biometric, sigue estos pasos:

  1. En el archivo build.gradle del módulo de tu app, agrega una dependencia a la biblioteca androidx.biometric.

  2. En la actividad o el fragmento que aloja el diálogo de acceso biométrico, muestra el diálogo con la lógica indicada en el siguiente fragmento de código:

    Kotlin

    private lateinit var executor: Executor
    private lateinit var biometricPrompt: BiometricPrompt
    private lateinit var promptInfo: BiometricPrompt.PromptInfo
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        executor = ContextCompat.getMainExecutor(this)
        biometricPrompt = BiometricPrompt(this, executor,
                object : BiometricPrompt.AuthenticationCallback() {
            override fun onAuthenticationError(errorCode: Int,
                    errString: CharSequence) {
                super.onAuthenticationError(errorCode, errString)
                Toast.makeText(applicationContext,
                    "Authentication error: $errString", Toast.LENGTH_SHORT)
                    .show()
            }
    
            override fun onAuthenticationSucceeded(
                    result: BiometricPrompt.AuthenticationResult) {
                super.onAuthenticationSucceeded(result)
                Toast.makeText(applicationContext,
                    "Authentication succeeded!", Toast.LENGTH_SHORT)
                    .show()
            }
    
            override fun onAuthenticationFailed() {
                super.onAuthenticationFailed()
                Toast.makeText(applicationContext, "Authentication failed",
                    Toast.LENGTH_SHORT)
                    .show()
            }
        })
    
        promptInfo = BiometricPrompt.PromptInfo.Builder()
                .setTitle("Biometric login for my app")
                .setSubtitle("Log in using your biometric credential")
                .setNegativeButtonText("Use account password")
                .build()
    
        // Prompt appears when user clicks "Log in".
        // Consider integrating with the keystore to unlock cryptographic operations,
        // if needed by your app.
        val biometricLoginButton =
                findViewById<Button>(R.id.biometric_login)
        biometricLoginButton.setOnClickListener {
            biometricPrompt.authenticate(promptInfo)
        }
    }

    Java

    private Executor executor;
    private BiometricPrompt biometricPrompt;
    private BiometricPrompt.PromptInfo promptInfo;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        executor = ContextCompat.getMainExecutor(this);
        biometricPrompt = new BiometricPrompt(MainActivity.this,
                executor, new BiometricPrompt.AuthenticationCallback() {
            @Override
            public void onAuthenticationError(int errorCode,
                    @NonNull CharSequence errString) {
                super.onAuthenticationError(errorCode, errString);
                Toast.makeText(getApplicationContext(),
                    "Authentication error: " + errString, Toast.LENGTH_SHORT)
                    .show();
            }
    
            @Override
            public void onAuthenticationSucceeded(
                    @NonNull BiometricPrompt.AuthenticationResult result) {
                super.onAuthenticationSucceeded(result);
                Toast.makeText(getApplicationContext(),
                    "Authentication succeeded!", Toast.LENGTH_SHORT).show();
            }
    
            @Override
            public void onAuthenticationFailed() {
                super.onAuthenticationFailed();
                Toast.makeText(getApplicationContext(), "Authentication failed",
                    Toast.LENGTH_SHORT)
                    .show();
            }
        });
    
        promptInfo = new BiometricPrompt.PromptInfo.Builder()
                .setTitle("Biometric login for my app")
                .setSubtitle("Log in using your biometric credential")
                .setNegativeButtonText("Use account password")
                .build();
    
        // Prompt appears when user clicks "Log in".
        // Consider integrating with the keystore to unlock cryptographic operations,
        // if needed by your app.
        Button biometricLoginButton = findViewById(R.id.biometric_login);
        biometricLoginButton.setOnClickListener(view -> {
                biometricPrompt.authenticate(promptInfo);
        });
    }

Usa una solución criptográfica que dependa de la autenticación

Para proteger aún más la información sensible de tu app, puedes incorporar criptografía en tu flujo de trabajo de autenticación biométrica con una instancia de CryptoObject. El marco de trabajo admite los siguientes objetos criptográficos: Signature, Cipher y Mac.

Una vez que se autentica el usuario correctamente mediante un mensaje biométrico, tu app puede realizar una operación criptográfica. Por ejemplo, si autenticas con un objeto Cipher, tu app puede realizar una encriptación y una desencriptación con un objeto SecretKey.

En las siguientes secciones, se muestran ejemplos de cómo usar un objeto Cipher y un objeto SecretKey para encriptar datos. Cada ejemplo utiliza los siguientes métodos:

Kotlin

private fun generateSecretKey(keyGenParameterSpec: KeyGenParameterSpec) {
    val keyGenerator = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
    keyGenerator.init(keyGenParameterSpec)
    keyGenerator.generateKey()
}

private fun getSecretKey(): SecretKey {
    val keyStore = KeyStore.getInstance("AndroidKeyStore")

    // Before the keystore can be accessed, it must be loaded.
    keyStore.load(null)
    return keyStore.getKey(KEY_NAME, null) as SecretKey
}

private fun getCipher(): Cipher {
    return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
            + KeyProperties.BLOCK_MODE_CBC + "/"
            + KeyProperties.ENCRYPTION_PADDING_PKCS7)
}

Java

private void generateSecretKey(KeyGenParameterSpec keyGenParameterSpec) {
    KeyGenerator keyGenerator = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
    keyGenerator.init(keyGenParameterSpec);
    keyGenerator.generateKey();
}

private SecretKey getSecretKey() {
    KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");

    // Before the keystore can be accessed, it must be loaded.
    keyStore.load(null);
    return ((SecretKey)keyStore.getKey(KEY_NAME, null));
}

private Cipher getCipher() {
    return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
            + KeyProperties.BLOCK_MODE_CBC + "/"
            + KeyProperties.ENCRYPTION_PADDING_PKCS7);
}

Cómo autenticar solo con credenciales biométricas

Si tu app usa una clave secreta que requiere credenciales biométricas para desbloquearse, el usuario debe autenticar sus credenciales biométricas cada vez antes de que tu app acceda a la clave.

Para encriptar información sensible solo después de que el usuario realice la autenticación mediante credenciales biométricas, sigue estos pasos:

  1. Genera una clave que use la siguiente configuración KeyGenParameterSpec:

    Kotlin

    generateSecretKey(KeyGenParameterSpec.Builder(
            KEY_NAME,
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
            .setUserAuthenticationRequired(true)
            // Invalidate the keys if the user has registered a new biometric
            // credential, such as a new fingerprint. Can call this method only
            // on Android 7.0 (API level 24) or higher. The variable
            // "invalidatedByBiometricEnrollment" is true by default.
            .setInvalidatedByBiometricEnrollment(true)
            .build())

    Java

    generateSecretKey(new KeyGenParameterSpec.Builder(
            KEY_NAME,
            KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
            .setUserAuthenticationRequired(true)
            // Invalidate the keys if the user has registered a new biometric
            // credential, such as a new fingerprint. Can call this method only
            // on Android 7.0 (API level 24) or higher. The variable
            // "invalidatedByBiometricEnrollment" is true by default.
            .setInvalidatedByBiometricEnrollment(true)
            .build());
  2. Inicia un flujo de trabajo de autenticación biométrica que incorpore un algoritmo de cifrado:

    Kotlin

    biometricLoginButton.setOnClickListener {
        // Exceptions are unhandled within this snippet.
        val cipher = getCipher()
        val secretKey = getSecretKey()
        cipher.init(Cipher.ENCRYPT_MODE, secretKey)
        biometricPrompt.authenticate(promptInfo,
                BiometricPrompt.CryptoObject(cipher))
    }

    Java

    biometricLoginButton.setOnClickListener(view -> {
        // Exceptions are unhandled within this snippet.
        Cipher cipher = getCipher();
        SecretKey secretKey = getSecretKey();
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        biometricPrompt.authenticate(promptInfo,
                new BiometricPrompt.CryptoObject(cipher));
    });
  3. Dentro de tus devoluciones de llamada de autenticación biométrica, usa la clave secreta para encriptar la información sensible:

    Kotlin

    override fun onAuthenticationSucceeded(
            result: BiometricPrompt.AuthenticationResult) {
        val encryptedInfo: ByteArray = result.cryptoObject.cipher?.doFinal(
            // plaintext-string text is whatever data the developer would like
            // to encrypt. It happens to be plain-text in this example, but it
            // can be anything
                plaintext-string.toByteArray(Charset.defaultCharset())
        )
        Log.d("MY_APP_TAG", "Encrypted information: " +
                Arrays.toString(encryptedInfo))
    }

    Java

    @Override
    public void onAuthenticationSucceeded(
            @NonNull BiometricPrompt.AuthenticationResult result) {
        // NullPointerException is unhandled; use Objects.requireNonNull().
        byte[] encryptedInfo = result.getCryptoObject().getCipher().doFinal(
            // plaintext-string text is whatever data the developer would like
            // to encrypt. It happens to be plain-text in this example, but it
            // can be anything
                plaintext-string.getBytes(Charset.defaultCharset()));
        Log.d("MY_APP_TAG", "Encrypted information: " +
                Arrays.toString(encryptedInfo));
    }

Autentica con credenciales biométricas o de pantalla de bloqueo

Puedes usar una clave secreta que permita la autenticación mediante credenciales biométricas o credenciales de pantalla de bloqueo (PIN, patrón o contraseña). Cuando configures esta clave, especifica un período de validez. Durante este período, tu app puede realizar varias operaciones criptográficas sin que el usuario necesite volver a autenticarse.

Para encriptar información sensible después de que el usuario se autentique con credenciales biométricas o de pantalla de bloqueo, sigue estos pasos:

  1. Genera una clave que use la siguiente configuración KeyGenParameterSpec:

    Kotlin

    generateSecretKey(KeyGenParameterSpec.Builder(
        KEY_NAME,
        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
        .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
        .setUserAuthenticationRequired(true)
        .setUserAuthenticationParameters(VALIDITY_DURATION_SECONDS,
                ALLOWED_AUTHENTICATORS)
        .build())

    Java

    generateSecretKey(new KeyGenParameterSpec.Builder(
        KEY_NAME,
        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
        .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
        .setUserAuthenticationRequired(true)
        .setUserAuthenticationParameters(VALIDITY_DURATION_SECONDS,
                ALLOWED_AUTHENTICATORS)
        .build());
  2. Dentro de un período de VALIDITY_DURATION_SECONDS después de que se autentique el usuario, encripta la información sensible:

    Kotlin

    private fun encryptSecretInformation() {
        // Exceptions are unhandled for getCipher() and getSecretKey().
        val cipher = getCipher()
        val secretKey = getSecretKey()
        try {
            cipher.init(Cipher.ENCRYPT_MODE, secretKey)
            val encryptedInfo: ByteArray = cipher.doFinal(
                // plaintext-string text is whatever data the developer would
                // like to encrypt. It happens to be plain-text in this example,
                // but it can be anything
                    plaintext-string.toByteArray(Charset.defaultCharset()))
            Log.d("MY_APP_TAG", "Encrypted information: " +
                    Arrays.toString(encryptedInfo))
        } catch (e: InvalidKeyException) {
            Log.e("MY_APP_TAG", "Key is invalid.")
        } catch (e: UserNotAuthenticatedException) {
            Log.d("MY_APP_TAG", "The key's validity timed out.")
            biometricPrompt.authenticate(promptInfo)
        }

    Java

    private void encryptSecretInformation() {
        // Exceptions are unhandled for getCipher() and getSecretKey().
        Cipher cipher = getCipher();
        SecretKey secretKey = getSecretKey();
        try {
            // NullPointerException is unhandled; use Objects.requireNonNull().
            ciper.init(Cipher.ENCRYPT_MODE, secretKey);
            byte[] encryptedInfo = cipher.doFinal(
                // plaintext-string text is whatever data the developer would
                // like to encrypt. It happens to be plain-text in this example,
                // but it can be anything
                    plaintext-string.getBytes(Charset.defaultCharset()));
        } catch (InvalidKeyException e) {
            Log.e("MY_APP_TAG", "Key is invalid.");
        } catch (UserNotAuthenticatedException e) {
            Log.d("MY_APP_TAG", "The key's validity timed out.");
            biometricPrompt.authenticate(promptInfo);
        }
    }

Autentica con claves de autenticación según el uso

Puedes proporcionar compatibilidad con las claves de autenticación según el uso dentro de tu instancia de BiometricPrompt. Para esta clave, el usuario deberá presentar una credencial biométrica o una credencial de dispositivo cada vez que tu app necesite acceder a datos protegidos por esa clave. Las claves de autenticación según el uso pueden ser útiles para transacciones importantes, como realizar un pago grande o actualizar los historiales médicos de una persona.

Para asociar un objeto BiometricPrompt con una clave de autenticación por uso, agrega un código similar al siguiente:

Kotlin

val authPerOpKeyGenParameterSpec =
        KeyGenParameterSpec.Builder("myKeystoreAlias", key-purpose)
    // Accept either a biometric credential or a device credential.
    // To accept only one type of credential, include only that type as the
    // second argument.
    .setUserAuthenticationParameters(0 /* duration */,
            KeyProperties.AUTH_BIOMETRIC_STRONG or
            KeyProperties.AUTH_DEVICE_CREDENTIAL)
    .build()

Java

KeyGenParameterSpec authPerOpKeyGenParameterSpec =
        new KeyGenParameterSpec.Builder("myKeystoreAlias", key-purpose)
    // Accept either a biometric credential or a device credential.
    // To accept only one type of credential, include only that type as the
    // second argument.
    .setUserAuthenticationParameters(0 /* duration */,
            KeyProperties.AUTH_BIOMETRIC_STRONG |
            KeyProperties.AUTH_DEVICE_CREDENTIAL)
    .build();

Autentica sin acción explícita del usuario

De forma predeterminada, el sistema requiere que los usuarios realicen una acción específica, como presionar un botón, una vez que se aceptan las credenciales biométricas. Esta configuración es la que se prefiere si tu app muestra el diálogo para confirmar una acción sensible o de alto riesgo, como realizar una compra.

Sin embargo, si tu app muestra un cuadro de diálogo de autenticación biométrica para una acción de menor riesgo, puedes proporcionar una sugerencia al sistema de que el usuario no necesita confirmar la autenticación. Esta sugerencia puede permitir al usuario ver el contenido en tu app más rápidamente después de volver a autenticarse mediante una modalidad pasiva, como el reconocimiento facial o de iris. Para proporcionar esta sugerencia, pasa el objeto false al método setConfirmationRequired().

La Figura 2 muestra dos versiones del mismo diálogo. Una versión requiere una acción del usuario explícita, mientras que la otra no.

Captura de pantalla del diálogo Captura de pantalla del diálogo
Figura 2: Autenticación facial sin confirmación del usuario (arriba) y con confirmación del usuario (abajo).

En el siguiente fragmento de código, se muestra cómo presentar un diálogo que no requiere de una acción explícita del usuario para completar el proceso de autenticación:

Kotlin

// Lets the user authenticate without performing an action, such as pressing a
// button, after their biometric credential is accepted.
promptInfo = BiometricPrompt.PromptInfo.Builder()
        .setTitle("Biometric login for my app")
        .setSubtitle("Log in using your biometric credential")
        .setNegativeButtonText("Use account password")
        .setConfirmationRequired(false)
        .build()

Java

// Lets the user authenticate without performing an action, such as pressing a
// button, after their biometric credential is accepted.
promptInfo = new BiometricPrompt.PromptInfo.Builder()
        .setTitle("Biometric login for my app")
        .setSubtitle("Log in using your biometric credential")
        .setNegativeButtonText("Use account password")
        .setConfirmationRequired(false)
        .build();

Admite el resguardo de credenciales no biométricas

Si deseas que tu app permita la autenticación con credenciales biométricas o de dispositivo, puedes declarar que tu app admita credenciales de dispositivo con la inclusión del objeto DEVICE_CREDENTIAL en el conjunto de valores que pasas al elemento setAllowedAuthenticators().

Si, en la actualidad, tu app usa los objetos createConfirmDeviceCredentialIntent() o setDeviceCredentialAllowed() para proporcionar esta función, cambia al elemento setAllowedAuthenticators().

Recursos adicionales

Para obtener más información sobre la autenticación biométrica en Android, consulta los siguientes recursos.

Entradas de blog