Tu primera API de FIDO2 para Android

1. Introducción

¿Qué es la API de FIDO2?

La API de FIDO2 permite que las aplicaciones para Android creen y usen credenciales públicas certificadas y sólidas basadas en claves con el fin de autenticar usuarios. La API proporciona una implementación de cliente de WebAuthn, que admite el uso de autenticadores BLE, NFC y roaming de USB (llaves de seguridad), así como un autenticador de plataforma, que permite al usuario autenticarse con su huella dactilar o bloqueo de pantalla.

Qué compilarás...

En este codelab, compilarás una app para Android con una funcionalidad de reautenticación simple que usa un sensor de huellas dactilares. "Reautenticación" es cuando un usuario accede a una app y, luego, vuelve a autenticarse cuando vuelve a la app o cuando intenta acceder a una sección importante de ella. Este último caso también se conoce como "autenticación incremental".

Qué aprenderás

Aprenderás a llamar a la API de FIDO2 de Android y a conocer las opciones que puedes proporcionar para atender diversas ocasiones. También aprenderás prácticas recomendadas específicas para volver a autenticarla.

Requisitos

  • Dispositivo Android con sensor de huellas dactilares (incluso sin sensor de huellas dactilares, el bloqueo de pantalla puede proporcionar una funcionalidad equivalente de verificación del usuario)
  • Android OS 7.0 o superior con las últimas actualizaciones. Asegúrate de registrar una huella dactilar (o un bloqueo de pantalla).

2. Cómo prepararte

Clona el repositorio

Consulta el repositorio de GitHub.

https://github.com/android/codelab-fido2

$ git clone https://github.com/android/codelab-fido2.git

¿Qué implementaremos?

  • Permitir que los usuarios registren un “autenticador de plataforma de verificación de usuarios” (el teléfono Android con sensor de huellas dactilares actuará como uno solo).
  • Permitir que los usuarios se vuelvan a autenticar en la app con su huella digital

Aquí puede obtener una vista previa de lo que compilará.

Inicia tu proyecto de codelab

La app completa envía solicitudes a un servidor en https://webauthn-codelab.glitch.me. Allí puedes probar la versión web de la misma app.

c2234c42ba8a6ef1.png

Trabajarás en tu propia versión de la app.

  1. Ve a la página de edición del sitio web: https://glitch.com/edit/#!/webauthn-codelab.
  2. Busca la opción “Remix para editar”. botón en la esquina superior derecha. Cuando presionas el botón, puedes “bifurcar” el código y continuar con tu propia versión junto con la URL de un proyecto nuevo. 9ef108869885e4ce.png
  3. Copia el nombre del proyecto en la parte superior izquierda (puedes modificarlo como desees). c91d0d59c61021a4.png
  4. Pégala en la sección HOSTNAME del archivo .env en la falla. 889b55b1cf74b894.png

3. Asocia tu app y un sitio web con los Vínculos de recursos digitales

Para usar la API de FIDO2 en una app para Android, asóciala con un sitio web y comparte credenciales entre ellos. Para ello, usa los Vínculos de recursos digitales. Para declarar asociaciones, aloja un archivo JSON de Vínculos de recursos digitales en tu sitio web y agrega un vínculo al archivo de Vínculos de recursos digitales en el manifiesto de tu app.

Aloja .well-known/assetlinks.json en tu dominio

Para definir una asociación entre tu app y el sitio web, crea un archivo JSON y colócalo en .well-known/assetlinks.json. Por suerte, tenemos un código de servidor que muestra automáticamente el archivo assetlinks.json, solo agregando los siguientes parámetros de entorno al archivo .env en la falla:

  • ANDROID_PACKAGENAME: Es el nombre del paquete de tu app (com.example.android.fido2).
  • ANDROID_SHA256HASH: Hash SHA256 de tu certificado de firma

Para obtener el hash SHA256 de tu certificado de firma de desarrollador, usa el siguiente comando. La contraseña predeterminada del almacén de claves de depuración es "android".

$ keytool -exportcert -list -v -alias androiddebugkey -keystore ~/.android/debug.keystore

Cuando accedas a https://<your-project-name>.glitch.me/.well-known/assetlinks.json , deberías ver una cadena JSON como la siguiente:

[{
  "relation": ["delegate_permission/common.handle_all_urls", "delegate_permission/common.get_login_creds"],
  "target": {
    "namespace": "web",
    "site": "https://<your-project-name>.glitch.me"
  }
}, {
  "relation": ["delegate_permission/common.handle_all_urls", "delegate_permission/common.get_login_creds"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.android.fido2",
    "sha256_cert_fingerprints": ["DE:AD:BE:EF:..."]
  }
}]

Abre el proyecto en Android Studio

Haz clic en "Open an existing Android Studio project". en la pantalla de bienvenida de Android Studio.

Elige la página "Android" carpeta dentro del repositorio.

1062875cf11ffb95.png

Cómo asociar la app con tu remix

Abre el archivo gradle.properties. En la parte inferior del archivo, cambia la URL del host por el remix de Glitch que acabas de crear.

// ...

# The URL of the server
host=https://<your-project-name>.glitch.me

En este punto, tu configuración de Vínculos de recursos digitales ya debería estar establecida.

4. Mira cómo funciona la app ahora

Primero, veamos cómo funciona la app. Asegúrate de seleccionar “app-start” en el cuadro combinado de la configuración de ejecución. Haz clic en "Ejecutar". (el triangular verde junto al cuadro combinado) para iniciar la app en tu dispositivo Android conectado.

29351fb97062b43c.png

Cuando inicies la app, verás la pantalla en la que debes escribir tu nombre de usuario. Es UsernameFragment. A los fines de la demostración, la app y el servidor aceptan cualquier nombre de usuario. Escribe algo y presiona "Siguiente".

bd9007614a9a3644.png

La siguiente pantalla que verás es AuthFragment. Aquí es donde el usuario puede acceder con una contraseña. Más adelante, agregaremos aquí una función para acceder con FIDO2. Nuevamente, a los efectos de esta demostración, la app y el servidor aceptan cualquier contraseña. Solo tienes que escribir algo y presionar "Acceder".

d9caba817a0a99bd.png

Esta es la última pantalla de la app, HomeFragment. Por ahora, solo puedes ver una lista vacía de credenciales aquí. Presionar "Volver a autenticar" te lleva de regreso a AuthFragment. Presionar "Salir" te lleva de regreso a UsernameFragment. El botón de acción flotante con "+" ahora no hace nada, pero iniciará el registro de un

nueva credencial una vez que hayas implementado el flujo de registro FIDO2.

1cfcc6c884020e37.png

Antes de comenzar a programar, aquí hay una técnica útil. En Android Studio, presiona "TODO". abajo. Se mostrará una lista de todas las tareas pendientes de este codelab. Comenzaremos con el primer TODO en la siguiente sección.

e5a811bbc7cd7b30.png

5. Registra una credencial con una huella digital

Para habilitar la autenticación con una huella digital, primero deberás registrar una credencial generada por un usuario que verifica un autenticador de plataforma, un autenticador integrado en el dispositivo que verifica al usuario usando datos biométricos, como un sensor de huellas dactilares.

37ce78fdf2759832.png

Como vimos en la sección anterior, el botón de acción flotante no hace nada ahora. Veamos cómo podemos registrar una credencial nueva.

Llama a la API del servidor: /auth/registerRequest

Abre AuthRepository.kt y busca TODO(1).

Aquí, registerRequest es el método al que se llama cuando se presiona el BAF. Queremos que este método llame a la API del servidor /auth/registerRequest. La API muestra un ApiResult con todos los PublicKeyCredentialCreationOptions que el cliente necesita para generar una credencial nueva.

Luego, podemos llamar a getRegisterPendingIntent con las opciones. Esta API de FIDO2 muestra un PendingIntent de Android para abrir un diálogo de huella digital y generar una credencial nueva. Podemos mostrarle ese PendingIntent al emisor.

El método se verá de la siguiente manera.

suspend fun registerRequest(): PendingIntent? {
  fido2ApiClient?.let { client ->
    try {
      val sessionId = dataStore.read(SESSION_ID)!!
      when (val apiResult = api.registerRequest(sessionId)) {
        ApiResult.SignedOutFromServer -> forceSignOut()
        is ApiResult.Success -> {
          if (apiResult.sessionId != null) {
            dataStore.edit { prefs ->
              prefs[SESSION_ID] = apiResult.sessionId
            }
          }
          val task = client.getRegisterPendingIntent(apiResult.data)
          return task.await()
        }
      }
    } catch (e: Exception) {
      Log.e(TAG, "Cannot call registerRequest", e)
    }
  }
  return null
}

Abrir diálogo de huella digital para el registro

Abre HomeFragment.kt y busca TODO(2).

Aquí es donde la IU recupera el intent de nuestro AuthRepository. Aquí, usaremos el miembro createCredentialIntentLauncher para iniciar el PendingIntent que obtuvimos como resultado del paso anterior. Se abrirá un diálogo para la generación de credenciales.

binding.add.setOnClickListener {
  lifecycleScope.launch {
    val intent = viewModel.registerRequest()
    if (intent != null) {
      createCredentialIntentLauncher.launch(
        IntentSenderRequest.Builder(intent).build()
      )
    }
  }
}

Cómo recibir ActivityResult con la nueva credencial

Abre HomeFragment.kt y busca TODO(3).

Se llama a este método handleCreateCredentialResult después de que se cierra el diálogo de huellas dactilares. Si se generó correctamente una credencial, el miembro data de ActivityResult contendrá la información de la credencial.

Primero, debes extraer una PublicKeyCredential de data. El intent de datos tiene un campo adicional de array de bytes con la clave Fido.FIDO2_KEY_CREDENTIAL_EXTRA. Puedes usar un método estático en PublicKeyCredential llamado deserializeFromBytes para convertir el array de bytes en un objeto PublicKeyCredential.

Luego, verifica si el miembro response de este objeto de credencial es AuthenticationErrorResponse. Si es así, se produjo un error cuando se generaba la credencial. de lo contrario, podemos enviar la credencial a nuestro backend.

El método finalizado se verá de la siguiente manera:

private fun handleCreateCredentialResult(activityResult: ActivityResult) {
  val bytes = activityResult.data?.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)
  when {
    activityResult.resultCode != Activity.RESULT_OK ->
      Toast.makeText(requireContext(), R.string.cancelled, Toast.LENGTH_LONG).show()
    bytes == null ->
      Toast.makeText(requireContext(), R.string.credential_error, Toast.LENGTH_LONG)
        .show()
    else -> {
      val credential = PublicKeyCredential.deserializeFromBytes(bytes)
      val response = credential.response
      if (response is AuthenticatorErrorResponse) {
        Toast.makeText(requireContext(), response.errorMessage, Toast.LENGTH_LONG)
          .show()
      } else {
        viewModel.registerResponse(credential)
      }
    }
  }
}

Llama a la API del servidor: /auth/registerResponse

Abre AuthRepository.kt y busca TODO(4).

Se llama a este método registerResponse después de que la IU generó correctamente una credencial nueva, y queremos devolverla al servidor.

El objeto PublicKeyCredential tiene información sobre la credencial generada recientemente. Ahora queremos recordar el ID de nuestra clave local para poder distinguirla de otras claves registradas en el servidor. En el objeto PublicKeyCredential, toma su propiedad rawId y colócala en una variable de cadena local con toBase64.

Ya estamos listos para enviar la información al servidor. Usa api.registerResponse para llamar a la API del servidor y enviar la respuesta. El valor que se muestra contiene una lista de todas las credenciales registradas en el servidor, incluida la nueva.

Por último, podemos guardar los resultados en nuestra DataStore. La lista de credenciales debe guardarse con la clave CREDENTIALS como StringSet. Puedes usar toStringSet para convertir la lista de credenciales en una StringSet.

Además, guardamos el ID de la credencial con la clave LOCAL_CREDENTIAL_ID.

suspend fun registerResponse(credential: PublicKeyCredential) {
  try {
    val sessionId = dataStore.read(SESSION_ID)!!
    val credentialId = credential.rawId.toBase64()
    when (val result = api.registerResponse(sessionId, credential)) {
      ApiResult.SignedOutFromServer -> forceSignOut()
      is ApiResult.Success -> {
        dataStore.edit { prefs ->
          result.sessionId?.let { prefs[SESSION_ID] = it }
          prefs[CREDENTIALS] = result.data.toStringSet()
          prefs[LOCAL_CREDENTIAL_ID] = credentialId
        }
      }
    }
  } catch (e: ApiException) {
    Log.e(TAG, "Cannot call registerResponse", e)
  }
}

Ejecuta la app. Podrás hacer clic en el BAF y registrar una credencial nueva.

7d64d9289c5a3cbc.png

6. Autentica al usuario con una huella digital

Ahora tenemos una credencial registrada en la app y el servidor. Ahora podemos usarlo para permitir que el usuario acceda. Agregamos la función de acceso con huella dactilar a AuthFragment. Cuando un usuario aterriza en él, se muestra un diálogo de huella dactilar. Cuando la autenticación se realiza de forma correcta, se redirecciona al usuario a HomeFragment.

Llama a la API del servidor: /auth/signinRequest

Abre AuthRepository.kt y busca TODO(5).

Se llama a este método signinRequest cuando se abre AuthFragment. Aquí, queremos solicitar el servidor y ver si podemos permitir que el usuario acceda con FIDO2.

Primero, debemos recuperar PublicKeyCredentialRequestOptions del servidor. Usa api.signInRequest para llamar a la API del servidor. El ApiResult que se muestra contiene PublicKeyCredentialRequestOptions.

Con PublicKeyCredentialRequestOptions, podemos usar la API de FIDO2 getSignIntent para crear un PendingIntent para abrir el diálogo de huellas digitales.

Por último, podemos mostrar el PendingIntent a la IU.

suspend fun signinRequest(): PendingIntent? {
  fido2ApiClient?.let { client ->
    val sessionId = dataStore.read(SESSION_ID)!!
    val credentialId = dataStore.read(LOCAL_CREDENTIAL_ID)
    if (credentialId != null) {
      when (val apiResult = api.signinRequest(sessionId, credentialId)) {
        ApiResult.SignedOutFromServer -> forceSignOut()
        is ApiResult.Success -> {
          val task = client.getSignPendingIntent(apiResult.data)
          return task.await()
        }
      }
    }
  }
  return null
}

Abrir diálogo de huella digital para la aserción

Abre AuthFragment.kt y encuentra TODO(6).

Esto es más o menos lo mismo que hicimos con el registro. Podemos iniciar el diálogo de huellas dactilares con el miembro signIntentLauncher.

viewLifecycleOwner.lifecycleScope.launchWhenStarted {
  launch {
    viewModel.signinRequests.collect { intent ->
      signIntentLauncher.launch(
        IntentSenderRequest.Builder(intent).build()
      )
    }
  }
  launch {
    ...
  }
}

Cómo controlar ActivityResult

Abre AuthFragment.kt y busca TODO(7).

Esto es lo mismo que hicimos con el registro. Podemos extraer el PublicKeyCredential, verificar si hay un error y pasarlo al ViewModel.

private fun handleSignResult(activityResult: ActivityResult) {
  val bytes = activityResult.data?.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)
  when {
    activityResult.resultCode != Activity.RESULT_OK ->
      Toast.makeText(requireContext(), R.string.cancelled, Toast.LENGTH_SHORT).show()
    bytes == null ->
      Toast.makeText(requireContext(), R.string.auth_error, Toast.LENGTH_SHORT)
        .show()
    else -> {
      val credential = PublicKeyCredential.deserializeFromBytes(bytes)
      val response = credential.response
      if (response is AuthenticatorErrorResponse) {
        Toast.makeText(requireContext(), response.errorMessage, Toast.LENGTH_SHORT)
          .show()
      } else {
        viewModel.signinResponse(credential)
      }
    }
  }
}

Llama a la API del servidor: /auth/signinResponse

Abre AuthRepository.kt y encuentra TODO(8).

El objeto PublicKeyCredential tiene un ID de credencial como keyHandle. Como lo hicimos en el flujo de registro, guardemos esto en una variable de cadena local para poder almacenarlo más tarde.

Ahora estamos listos para llamar a la API del servidor con api.signinResponse. El valor que se muestra contiene una lista de credenciales.

En este punto, el acceso se realizó correctamente. Debemos almacenar todos los resultados en nuestra DataStore. La lista de credenciales debe almacenarse como StringSet con la clave CREDENTIALS. El ID de la credencial local que guardamos antes se debe almacenar como una cadena con la clave LOCAL_CREDENTIAL_ID.

Por último, debemos actualizar el estado de acceso para que la IU pueda redireccionar al usuario a HomeFragment. Para ello, se puede emitir un objeto SignInState.SignedIn al SharedFlow llamado signInStateMutable. También queremos llamar a refreshCredentials para recuperar las credenciales del usuario para que se incluyan en la IU.

suspend fun signinResponse(credential: PublicKeyCredential) {
  try {
    val username = dataStore.read(USERNAME)!!
    val sessionId = dataStore.read(SESSION_ID)!!
    val credentialId = credential.rawId.toBase64()
    when (val result = api.signinResponse(sessionId, credential)) {
      ApiResult.SignedOutFromServer -> forceSignOut()
      is ApiResult.Success -> {
        dataStore.edit { prefs ->
          result.sessionId?.let { prefs[SESSION_ID] = it }
          prefs[CREDENTIALS] = result.data.toStringSet()
          prefs[LOCAL_CREDENTIAL_ID] = credentialId
        }
        signInStateMutable.emit(SignInState.SignedIn(username))
        refreshCredentials()
      }
    }
  } catch (e: ApiException) {
    Log.e(TAG, "Cannot call registerResponse", e)
  }
}

Ejecuta la app y haz clic en “Reauth”. para abrir AuthFragment. Ahora deberías ver un diálogo de huella dactilar que te pide que accedas con esa huella.

45f81419f84952c8.png

¡Felicitaciones! Aprendiste a usar la API de FIDO2 en Android para el registro y el acceso.

7. ¡Felicitaciones!

Completaste correctamente el codelab Tu primera API de FIDO2 de Android.

Qué aprendiste

  • Cómo registrar una credencial con un autenticador de plataforma que verifica un usuario
  • Cómo autenticar un usuario con un autenticador registrado
  • Opciones disponibles para registrar un autenticador nuevo.
  • Prácticas recomendadas de UX para la reautenticación con un sensor biométrico.

Próximo paso

  • Obtenga información sobre cómo crear una experiencia similar en un sitio web.

Para aprenderlo, prueba el codelab Tu primer WebAuthn.

Recursos

Queremos dar un agradecimiento especial a Yuriy Ackermann de FIDO Alliance por su ayuda.