Aprende a simplificar los recorridos de autenticación con la API de Credential Manager en tu app para Android

1. Antes de comenzar

Las soluciones tradicionales de autenticación plantean una serie de desafíos de seguridad y usabilidad.

Se utilizan mucho las contraseñas, pero…

  • Son fáciles de olvidar.
  • Los usuarios necesitan conocimientos para crear contraseñas seguras.
  • Los atacantes las descifran, recopilan y reproducen fácilmente.

Android ha trabajado en la creación de la API de Credential Manager para simplificar la experiencia de acceso y abordar los riesgos de seguridad a través de la compatibilidad con llaves de acceso, el estándar de nueva generación de la industria en autenticación sin contraseñas.

Credential Manager reúne asistencia para llaves de acceso y la combina con métodos tradicionales de autenticación, como contraseñas, Acceder con Google, etc.

Los usuarios podrán crear llaves de acceso y almacenarlas en el Administrador de contraseñas de Google, que sincronizará esas llaves en todos los dispositivos Android a los que el usuario haya accedido. Para hacerlo, se debe crear una llave de acceso, asociarla a una cuenta de usuario y almacenar la clave pública en un servidor antes de que un usuario pueda acceder con ella.

En este codelab, aprenderás a acceder con llaves y contraseñas utilizando la API del Credential Manager, y a utilizarlas para futuras autenticaciones. Estos son los 2 flujos disponibles:

  • Registrarse con una llave de acceso y una contraseña
  • Registrarse con una llave de acceso y una contraseña guardada

Requisitos previos

  • Conocimientos básicos sobre cómo ejecutar apps en Android Studio
  • Conocimientos básicos del flujo de autenticación en apps para Android
  • Conocimientos básicos sobre las llaves de acceso

Qué aprenderás

  • Cómo crear una llave de acceso
  • Cómo guardar una contraseña en el administrador de contraseñas
  • Cómo autenticar usuarios con una llave de acceso o contraseña guardada

Qué necesitarás

Tener una de las siguientes combinaciones de dispositivos:

  • Un dispositivo Android que ejecute Android 9 o una versión posterior (para llaves de acceso) y Android 4.4 o posterior (para la autenticación con contraseña a través de la API de Credential Manager)
  • Un dispositivo que preferentemente tenga un sensor de datos biométricos
  • Asegurarse de registrar un sistema biométrico (o bloqueo de pantalla)
  • Complemento para Kotlin, versión 1.8.10

2. Prepárate

  1. Clona este repo en tu laptop desde la rama credman_codelab: https://github.com/android/identity-samples/tree/credman_codelab.
  2. Ve al módulo CredentialManager y abre el proyecto en Android Studio.

Veamos el estado inicial de la app

Para saber cómo funciona el estado inicial de la app, sigue estos pasos:

  1. Inicia la app.
  2. Verás una pantalla principal con un botón para registrarte y acceder.
  3. Puedes hacer clic en Registrarte para acceder con una llave de acceso o una contraseña.
  4. Puedes hacer clic en Acceder para ingresar con la llave de acceso y la contraseña guardada.

8c0019ff9011950a.jpeg

Para comprender qué son las llaves de acceso y cómo funcionan, ve a ¿Cómo funcionan las llaves de acceso? .

3. Agrega la opción para registrarse con una llave de acceso

Al registrarse para obtener una nueva cuenta en una app para Android que utilice la API de Credential Manager, los usuarios pueden crear una llave de acceso para su cuenta. Esta llave de acceso se almacenará de forma segura en el proveedor de credenciales que elija el usuario y se utilizará para futuros accesos, sin necesidad de que el usuario introduzca su contraseña cada vez.

Ahora, crearás una llave de acceso y registrarás las credenciales del usuario utilizando datos biométricos o bloqueo de pantalla.

Registro con una llave de acceso

Dentro de Credential Manager -> app -> main -> java -> SignUpFragment.kt, puedes ver un campo de texto "nombre de usuario" y un botón para registrarte con llave de acceso.

dcc5c529b310f2fb.jpeg

Pasa el desafío y otra respuesta json a la llamada createPasskey()

Antes de crear una llave de acceso, debes solicitar al servidor que obtenga la información necesaria para pasarla a la API de Credential Manager durante una llamada createCredential().

Afortunadamente, ya tienes una respuesta simulada en tus assets(RegFromServer.txt) que devuelve esos parámetros a este codelab.

  • En tu app, navega al método SignUpFragment.kt, Find, signUpWithPasskeys, donde escribirás la lógica para crear una llave de acceso y dejar entrar al usuario. Puedes encontrar el método en la misma clase.
  • Comprueba el bloque else con un comentario para llamar a createPasskey() y reemplaza con el siguiente código:

SignUpFragment.kt

//TODO : Call createPasskey() to signup with passkey

val data = createPasskey()

Este método será llamado una vez que tengas un nombre de usuario válido rellenado en la pantalla.

  • Dentro del método createPasskey(), debes crear un CreatePublicKeyCredentialRequest() con los parámetros necesarios devueltos.

SignUpFragment.kt

//TODO create a CreatePublicKeyCredentialRequest() with necessary registration json from server

val request = CreatePublicKeyCredentialRequest(fetchRegistrationJsonFromServer())

Este fetchRegistrationJsonFromServer() es un método que lee la respuesta json de registro de los recursos y devuelve el json de registro para que pase mientras se crea la llave de acceso.

  • Busca el método fetchRegistrationJsonFromServer() y reemplaza TODO por el siguiente código para devolver json y quitar la cadena vacía de la sentencia return:

SignUpFragment.kt

//TODO fetch registration mock response

val response = requireContext().readFromAsset("RegFromServer")

//Update userId,challenge, name and Display name in the mock
return response.replace("<userId>", getEncodedUserId())
   .replace("<userName>", binding.username.text.toString())
   .replace("<userDisplayName>", binding.username.text.toString())
   .replace("<challenge>", getEncodedChallenge())
  • Aquí puedes leer el json de registro de los recursos.
  • Este json tiene 4 campos para reemplazar.
  • UserId debe ser único para que un usuario pueda crear varias llaves de acceso (si es necesario). Se sustituye <userId> por el userId generado.
  • <challenge> también tiene que ser único, por lo que generarás un desafío único aleatorio. El método ya está en tu código.

El siguiente fragmento de código incluye opciones de muestra que recibes del servidor:

{
  "challenge": String,
  "rp": {
    "name": String,
    "id": String
  },
  "user": {
    "id": String,
    "name": String,
    "displayName": String
  },
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    },
    {
      "type": "public-key",
      "alg": -257
    }
  ],
  "timeout": 1800000,
  "attestation": "none",
  "excludeCredentials": [],
  "authenticatorSelection": {
    "authenticatorAttachment": "platform",
    "requireResidentKey": true,
    "residentKey": "required",
    "userVerification": "required"
  }
}

La siguiente tabla no es exhaustiva, pero contiene los parámetros importantes del diccionario PublicKeyCredentialCreationOptions:

Parámetros

Descripciones

challenge

Una cadena aleatoria generada por el servidor que contiene suficiente entropía para que sea imposible adivinarla. Debe tener una longitud mínima de 16 bytes. Es obligatorio, pero no se utiliza durante el registro a menos que se haga una certificación.

user.id

El ID único del usuario. Este valor no debe incluir información de identificación personal, por ejemplo, direcciones de correo electrónico o nombres de usuario. Un valor aleatorio de 16 bytes generado por cuenta funcionará bien.

user.name

Este campo debe contener un identificador único para la cuenta que el usuario reconozca, como su dirección de correo electrónico o nombre de usuario. Se mostrará en el selector de cuentas. (Si utilizas un nombre de usuario, usa el mismo valor que en la autenticación con contraseña).

user.displayName

Este campo corresponde a un nombre opcional y fácil de usar para la cuenta. Se trata de un nombre de cuenta de usuario agradable para las personas, pensado solo para su visualización.

rp.id

La entidad de confianza corresponde a los datos de tu app. Necesita lo siguiente:

  • Un nombre (obligatorio): Es el nombre de tu app.
  • Un ID (opcional): Corresponde al dominio o subdominio. Si no lo tiene, se utiliza el dominio actual.
  • Un ícono (opcional).

pubKeyCredParams

Los parámetros de credencial de clave pública son una lista de algoritmos y tipos de clave permitidos. Esta lista debe contener al menos un elemento.

excludeCredentials

El usuario que intenta registrar un dispositivo puede haber registrado otros dispositivos. Para limitar la creación de varias credenciales para la misma cuenta en un único autenticador, puedes ignorar estos dispositivos. El miembro transports, si está provisto, debe contener el resultado de la llamada getTransports() durante el registro de cada credencial.

authenticatorSelection.authenticatorAttachment

Indica si el dispositivo debe fijarse en la plataforma o no, o si no hay ningún requisito al respecto. Establécelo en "plataforma". Esto indica que queremos un autenticador que esté integrado en el dispositivo de la plataforma, y no se le pedirá al usuario que inserte, por ejemplo, una llave de seguridad USB.

residentKey

Indica un valor "requerido" para crear una llave de acceso.

Cómo crear una credencial

  1. Una vez que creas una CreatePublicKeyCredentialRequest(), es necesario realizar la llamada a createCredential() con la solicitud creada.

SignUpFragment.kt

//TODO call createCredential() with createPublicKeyCredentialRequest

try {
   response = credentialManager.createCredential(
       requireActivity(),
       request
   ) as CreatePublicKeyCredentialResponse
} catch (e: CreateCredentialException) {
   configureProgress(View.INVISIBLE)
   handlePasskeyFailure(e)
}
  • Pasas la información requerida a createCredential().
  • Una vez que la solicitud se haya realizado correctamente, aparecerá una hoja inferior en la pantalla en la que deberás crear una llave de acceso.
  • Ahora los usuarios pueden verificar su identidad a través de datos biométricos o bloqueo de pantalla, etc.
  • Controlas la visibilidad de las vistas renderizadas y las excepciones si la solicitud falla o no tiene éxito por algún motivo. Aquí se registran los mensajes de error y se muestran en la app en un cuadro de diálogo de error. Puedes comprobar los registros de error completos a través de Android Studio o el comando de depuración de adb.

93022cb87c00f1fc.png

  1. Ahora, por último, debes completar el proceso de registro enviando la credencial de clave pública al servidor y permitiendo que el usuario ingrese. La app recibe un objeto de credencial que contiene una clave pública que puedes enviar al servidor para registrar la llave de acceso.

Aquí, usamos un servidor simulado, por lo que solo devolvemos true indicando que el servidor guardó la clave pública registrada para futuros propósitos de autenticación y validación.

Dentro del método signUpWithPasskeys(), encuentra el comentario relevante y reemplázalo con el siguiente código:

SignUpFragment.kt

//TODO : complete the registration process after sending public key credential to your server and let the user in

data?.let {
   registerResponse()
   DataProvider.setSignedInThroughPasskeys(true)
   listener.showHome()
}
  • registerResponse devuelve true indicando que el servidor (simulado) guardó la clave pública para uso futuro.
  • Estableces el parámetro SignedInThroughPasskeys como true, indicando que estás iniciando sesión a través de una llave de acceso.
  • Una vez que el usuario ingresa a la cuenta, lo rediriges a la pantalla principal.

El siguiente fragmento de código contiene un ejemplo de las opciones que deberías recibir:

{
  "id": String,
  "rawId": String,
  "type": "public-key",
  "response": {
    "clientDataJSON": String,
    "attestationObject": String,
  }
}

La siguiente tabla no es exhaustiva, pero contiene los parámetros importantes en PublicKeyCredential:

Parámetros

Descripciones

id

Un ID codificado en Base64URL de la llave de acceso creada. Este ID ayuda al navegador a determinar si una llave de acceso coincide en el dispositivo después de la autenticación. Este valor se debe almacenar en la base de datos del backend.

rawId

Una versión del objeto ArrayBuffer del ID de credencial.

response.clientDataJSON

Datos de cliente codificados en un objeto ArrayBuffer.

response.attestationObject

Un objeto de certificación con codificación ArrayBuffer. Contiene información importante, como un ID de RP, marcas y una clave pública.

Ejecuta la app y podrás hacer clic en el botón Registrarse con llaves de acceso y crear una llave de acceso.

4. Guarda la contraseña en Credential Provider

En esta aplicación, dentro de tu pantalla de registro, ya tienes un registro con nombre de usuario y contraseña implementado para fines de demostración.

Para guardar la credencial de la contraseña del usuario con tu proveedor de contraseñas, implementarás un CreatePasswordRequest para pasarlo a createCredential() para guardar la contraseña.

  • Busca el método signUpWithPassword() y reemplaza TODO por la llamada createPassword:

SignUpFragment.kt

//TODO : Save the user credential password with their password provider

createPassword()
  • Dentro del método createPassword(), debes crear la solicitud de contraseña de este modo y reemplazar TODO por el siguiente código:

SignUpFragment.kt

//TODO : CreatePasswordRequest with entered username and password

val request = CreatePasswordRequest(
   binding.username.text.toString(),
   binding.password.text.toString()
)
  • Luego, dentro del método createPassword(), debes crear la credencial con la solicitud create password, guardar la credencial de la contraseña del usuario con su proveedor de contraseñas y reemplazar TODO por el siguiente código:

SignUpFragment.kt

//TODO : Create credential with created password request


try {
   credentialManager.createCredential(request, requireActivity()) as CreatePasswordResponse
} catch (e: Exception) {
   Log.e("Auth", " Exception Message : " + e.message)
}
  • Ahora guardaste correctamente la credencial de contraseña con el proveedor de contraseñas del usuario para autenticarse a través de contraseña con un solo toque.

5. Agrega la opción para autenticarse con una llave de acceso o contraseña

Ahora puedes usarlo como una forma de autenticarse en tu app de forma segura.

629001f4a778d4fb.png

Obtén el desafío y otras opciones para pasar a la llamada getPasskey()

Antes de pedirle al usuario que se autentique, debes solicitar que los parámetros pasen WebAuthn json desde el servidor, incluido un desafío.

Ya tienes una respuesta simulada en tus recursos (AuthFromServer.txt) que devuelve esos parámetros en este codelab.

  • En tu app, navega hasta SignInFragment.kt, busca el método signInWithSavedCredentials donde escribirás la lógica para autenticar a través de la llave de acceso o contraseña guardada y permitir la entrada del usuario:
  • Comprueba el bloque else con un comentario para llamar a createPasskey() y reemplaza con el siguiente código:

SignInFragment.kt

//TODO : Call getSavedCredentials() method to signin using passkey/password

val data = getSavedCredentials()
  • Dentro del método getSavedCredentials(), debes crear un GetPublicKeyCredentialOption() con los parámetros necesarios para obtener las credenciales de tu proveedor de credenciales.

SigninFragment.kt

//TODO create a GetPublicKeyCredentialOption() with necessary registration json from server

val getPublicKeyCredentialOption =
   GetPublicKeyCredentialOption(fetchAuthJsonFromServer(), null)

Este fetchAuthJsonFromServer() es un método que lee la respuesta json de autenticación de los activos y devuelve el json de autenticación para recuperar todas las llaves asociadas con esta cuenta de usuario.

2º parámetro: clientDataHash, un hash que se utiliza para verificar la identidad de la parte de confianza, establecido solo si configuró GetCredentialRequest.origin. Para la app de ejemplo, esto es nulo.

El 3° parámetro es true si prefieres que la operación devuelva inmediatamente cuando no hay credenciales disponibles en lugar de volver a descubrir credenciales remotas, y false (de forma predeterminada), en caso contrario.

  • Busca el método fetchAuthJsonFromServer() y reemplaza TODO por el siguiente código para devolver json y quitar la cadena vacía de la sentencia return:

SignInFragment.kt

//TODO fetch authentication mock json

return requireContext().readFromAsset("AuthFromServer")

Nota: El servidor de este codelab está diseñado para devolver un JSON lo más similar posible al diccionarioPublicKeyCredentialRequestOptions que se pasa a la llamada getCredential() de la API. El siguiente fragmento de código incluye algunas opciones de ejemplo que deberías recibir:

{
  "challenge": String,
  "rpId": String,
  "userVerification": "",
  "timeout": 1800000
}

La siguiente tabla no es exhaustiva, pero contiene los parámetros importantes del diccionario PublicKeyCredentialRequestOptions:

Parámetros

Descripciones

challenge

Un desafío generado por el servidor en un objeto ArrayBuffer. Esta acción es necesaria para evitar ataques de reproducción. Nunca aceptes el mismo desafío en una respuesta dos veces. Considéralo un token de CSRF.

rpId

Un ID de RP es un dominio. Un sitio web puede especificar su dominio o un sufijo que se pueda registrar. Este valor debe coincidir con el parámetro rp.id que se usó cuando se creó la llave de acceso.

  • A continuación, debes crear un objeto PasswordOption() para recuperar todas las contraseñas guardadas en tu proveedor de contraseñas a través de la API de Credential Manager para esta cuenta de usuario. Dentro del método getSavedCredentials(), busca TODO y reemplázalo por lo siguiente:

SigninFragment.kt

//TODO create a PasswordOption to retrieve all the associated user's password

val getPasswordOption = GetPasswordOption()

Obtén credenciales

  • A continuación, debes llamar a la solicitud getCredential() con todas las opciones anteriores para recuperar las credenciales asociadas:

SignInFragment.kt

//TODO call getCredential() with required credential options

val result = try {
   credentialManager.getCredential(
       requireActivity(),
       GetCredentialRequest(
           listOf(
               getPublicKeyCredentialOption,
               getPasswordOption
           )  
     )
   )
} catch (e: Exception) {
   configureViews(View.INVISIBLE, true)
   Log.e("Auth", "getCredential failed with exception: " + e.message.toString())
   activity?.showErrorAlert(
       "An error occurred while authenticating through saved credentials. Check logs for additional details"
   )
   return null
}

if (result.credential is PublicKeyCredential) {
   val cred = result.credential as PublicKeyCredential
   DataProvider.setSignedInThroughPasskeys(true)
   return "Passkey: ${cred.authenticationResponseJson}"
}
if (result.credential is PasswordCredential) {
   val cred = result.credential as PasswordCredential
   DataProvider.setSignedInThroughPasskeys(false)
   return "Got Password - User:${cred.id} Password: ${cred.password}"
}
if (result.credential is CustomCredential) {
   //If you are also using any external sign-in libraries, parse them here with the utility functions provided.
}

  • Pasas la información requerida a getCredential(). Esto toma la lista de opciones de credenciales y un contexto de actividad para renderizar las opciones en la hoja inferior en ese contexto.
  • Una vez que la solicitud se haya realizado correctamente, aparecerá en pantalla una hoja inferior con todas las credenciales creadas para la cuenta asociada.
  • Ahora los usuarios pueden verificar su identidad a través datos biométricos o bloqueo de pantalla, etc. para autenticar la credencial elegida.
  • Estableces el parámetro SignedInThroughPasskeys como true, indicando que estás iniciando sesión a través de una llave de acceso. De lo contrario, es false.
  • Controlas la visibilidad de las vistas renderizadas y las excepciones si la solicitud falla o no tiene éxito por algún motivo. Aquí se registran los mensajes de error y se muestran en la app en un cuadro de diálogo de error. Puedes comprobar los registros de error completos a través de Android Studio o el comando de depuración de adb.
  • Ahora, por último, debes completar el proceso de registro enviando la credencial de clave pública al servidor y permitiendo que el usuario ingrese. La app recibe un objeto credential que contiene una clave pública que puedes enviar al servidor para autenticarse a través de la llave de acceso.

Aquí, usamos un servidor simulado, por lo que solo devolvemos true indicando que el servidor validó la clave pública.

Dentro del método signInWithSavedCredentials(), busca el comentario correspondiente y reemplázalo por el siguiente código:

SignInFragment.kt

//TODO : complete the authentication process after validating the public key credential to your server and let the user in.

data?.let {
   sendSignInResponseToServer()
   listener.showHome()
}
  • sendSigninResponseToServer() devuelve true indicando que el servidor (simulado) validó la clave pública para su uso futuro.
  • Una vez que el usuario ingresa a la cuenta, lo rediriges a la pantalla principal.

En el siguiente fragmento de código, se incluye un objeto PublicKeyCredential de ejemplo:

{
  "id": String
  "rawId": String
  "type": "public-key",
  "response": {
    "clientDataJSON": String
    "authenticatorData": String
    "signature": String
    "userHandle": String
  }
}

La siguiente tabla no es exhaustiva, pero contiene los parámetros importantes del objeto PublicKeyCredential:

Parámetros

Descripciones

id

El ID codificado en Base64URL de la credencial de llave de acceso autenticada.

rawId

Una versión del objeto ArrayBuffer del ID de credencial.

response.clientDataJSON

Un objeto ArrayBuffer de datos de cliente. Este campo contiene información, como el desafío y el origen, que el servidor de RP debe verificar.

response.authenticatorData

Un objeto ArrayBuffer de datos de autenticador. Este campo contiene información como el ID del RP.

response.signature

Un objeto ArrayBuffer de la firma. Este valor es el núcleo de la credencial y se debe verificar en el servidor.

response.userHandle

Un objeto ArrayBuffer que contiene el ID del usuario establecido en el momento de la creación. Este valor se puede usar en lugar del ID de credencial si el servidor necesita elegir los valores de ID que usa o si el backend desea evitar la creación de un índice en los IDs de credencial.

Ejecuta la aplicación, ve a Acceder -> Acceder con llaves de acceso/contraseña guardada e intenta ingresar con las credenciales guardadas.

Haz la prueba

Implementaste la creación de llaves de acceso, el almacenamiento de contraseñas en Credential Manager y la autenticación a través de llaves de acceso o contraseñas guardadas utilizando la API de Credential Manager en tu app para Android.

6. ¡Felicitaciones!

¡Terminaste este codelab! Si deseas consultar la resolución final, puedes hacerlo en https://github.com/android/identity-samples/tree/main/CredentialManager.

Si tienes alguna pregunta, hazla en StackOverflow con la etiqueta passkey.

Más información