Votre première API Android FIDO2

1. Introduction

Qu'est-ce que l'API FIDO2 ?

L'API FIDO2 permet aux applications Android de créer et d'utiliser des identifiants basés sur des clés publiques fiables et certifiées afin d'authentifier les utilisateurs. L'API fournit une implémentation du client WebAuthn, qui prend en charge l'utilisation d'authentificateurs itinérants BLE, NFC et USB (clés de sécurité), ainsi qu'un authentificateur de plate-forme, qui permet à l'utilisateur de s'authentifier à l'aide de son empreinte digitale ou du verrouillage de son écran.

Objectifs de l'atelier...

Dans cet atelier de programmation, vous allez créer une application Android avec une fonctionnalité de réauthentification simple à l'aide d'un lecteur d'empreinte digitale. "Réauthentification" se produit lorsqu'un utilisateur se connecte à une application, puis s'authentifie à nouveau lorsqu'il revient à votre application ou lorsqu'il tente d'accéder à une section importante de votre application. Ce second cas est également appelé "authentification étape par étape".

Ce que vous allez apprendre...

Vous apprendrez à appeler l'API Android FIDO2 et découvrirez les options à votre disposition pour répondre à différents besoins. Vous découvrirez également les bonnes pratiques spécifiques à la réauthentification.

Ce dont vous avez besoin...

  • Appareil Android équipé d'un lecteur d'empreinte digitale (même sans lecteur d'empreinte digitale, le verrouillage de l'écran peut fournir une fonctionnalité de validation utilisateur équivalente)
  • Android OS 7.0 ou versions ultérieures avec les dernières mises à jour. Veillez à enregistrer une empreinte digitale ou un verrouillage de l'écran.

2. Configuration

Cloner le dépôt

Consultez le dépôt GitHub.

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

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

Que allons-nous implémenter ?

  • Autoriser les utilisateurs à enregistrer un "authentificateur de plate-forme de validation des utilisateurs" (le téléphone Android équipé d'un lecteur d'empreinte digitale servira comme tel).
  • Autorisez les utilisateurs à s'authentifier de nouveau à l'aide de leur empreinte digitale.

Pour afficher un aperçu de ce que vous allez compiler, cliquez ici.

Démarrer le projet d'atelier de programmation

Une fois terminée, l'application envoie des requêtes à un serveur à l'adresse https://webauthn-codelab.glitch.me. Vous pouvez essayer la version Web de la même application sur cette page.

c2234c42ba8a6ef1.png

Vous allez travailler sur votre propre version de l'application.

  1. Accédez à la page de modification du site Web à l'adresse https://glitch.com/edit/#!/webauthn-codelab.
  2. Recherchez "Remixer pour modifier". dans le coin supérieur droit. En appuyant sur le bouton, vous pouvez "dupliquer" le code et continuez avec votre propre version avec une nouvelle URL de projet. 9ef108869885e4ce.png
  3. Copiez le nom du projet en haut à gauche (vous pouvez le modifier si vous le souhaitez). c91d0d59c61021a4.png
  4. Collez-le dans la section HOSTNAME du fichier .env dans le glitch. 889b55b1cf74b894.png

3. Associer votre application et un site Web à Digital Asset Links

Pour utiliser l'API FIDO2 sur une application Android, associez-la à un site Web et partagez les identifiants entre les deux. Pour ce faire, utilisez Digital Asset Links. Vous pouvez déclarer des associations en hébergeant un fichier JSON Digital Asset Links sur votre site Web et en ajoutant un lien vers ce fichier dans le fichier manifeste de votre application.

Héberger .well-known/assetlinks.json sur votre domaine

Vous pouvez définir une association entre votre application et le site Web en créant un fichier JSON et en le plaçant à .well-known/assetlinks.json. Heureusement, nous avons un code de serveur qui affiche automatiquement le fichier assetlinks.json, simplement en ajoutant les paramètres d'environnement suivants au fichier .env dans un glitch:

  • ANDROID_PACKAGENAME: nom du package de votre application (com.example.android.fido2)
  • ANDROID_SHA256HASH: hachage SHA256 de votre certificat de signature

Pour obtenir le hachage SHA256 de votre certificat de signature du développeur, utilisez la commande ci-dessous. Le mot de passe par défaut du keystore de débogage est "android".

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

En accédant à https://<your-project-name>.glitch.me/.well-known/assetlinks.json , vous devriez obtenir une chaîne JSON semblable à celle-ci:

[{
  "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:..."]
  }
}]

Ouvrir le projet dans Android Studio

Cliquez sur "Ouvrir un projet Android Studio existant". sur l'écran d'accueil d'Android Studio.

Choisissez la version "Android" dans le dépôt "dépôt".

1062875cf11ffb95.png

Associer l'application à votre remix

Ouvrir le fichier gradle.properties. En bas du fichier, remplacez l'URL de l'hôte par le remix Glitch que vous venez de créer.

// ...

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

À ce stade, votre configuration Digital Asset Links devrait être prête.

4. Découvrez le fonctionnement de l'application

Commençons par vérifier comment l'application fonctionne maintenant. Veillez à sélectionner "app-start" dans la liste déroulante de la configuration d'exécution. Cliquez sur "Exécuter". (le triangle vert à côté de la liste déroulante) pour lancer l'application sur votre appareil Android connecté.

29351fb97062b43c.png

Lorsque vous lancez l'application, un écran s'affiche pour vous permettre de saisir votre nom d'utilisateur. Il s'agit de la radio UsernameFragment. À des fins de démonstration, l'application et le serveur acceptent n'importe quel nom d'utilisateur. Il vous suffit de taper quelque chose et d'appuyer sur « Suivant ».

bd9007614a9a3644.png

L'écran suivant est AuthFragment. C'est ici que l'utilisateur peut se connecter avec un mot de passe. Plus tard, nous ajouterons ici une fonctionnalité de connexion avec FIDO2. Encore une fois, pour les besoins de la démonstration, l'application et le serveur acceptent n'importe quel mot de passe. Il vous suffit de saisir votre texte et d'appuyer sur "Connexion".

d9caba817a0a99bd.png

Voici le dernier écran de cette appli, HomeFragment. Pour l'instant, vous ne voyez ici qu'une liste d'identifiants vide. Appuyer sur "Réauthentification" vous ramène à AuthFragment. Appuyer sur "Se déconnecter" vous ramène à UsernameFragment. Le bouton d'action flottant avec "+" ne fait rien pour l'instant, mais lance l'enregistrement d'un

un nouvel identifiant une fois que vous avez implémenté le flux d’enregistrement FIDO2.

1cfcc6c884020e37.png

Avant de commencer à coder, voici une technique utile. Dans Android Studio, appuyez sur TODO. en bas. La liste de toutes les tâches à effectuer de cet atelier de programmation s'affiche. Nous allons commencer par le premier TODO dans la section suivante.

e5a811bbc7cd7b30.png

5. Enregistrer un identifiant à l'aide d'une empreinte digitale

Pour activer l'authentification à l'aide d'une empreinte digitale, vous devez d'abord enregistrer un identifiant généré par un authentificateur de plate-forme de validation de l'utilisateur (un authentificateur intégré à l'appareil qui valide l'utilisateur à l'aide de données biométriques, comme un lecteur d'empreinte digitale, par exemple).

37ce78fdf2759832.png

Comme nous l'avons vu dans la section précédente, le bouton d'action flottant ne sert plus à rien. Voyons comment enregistrer un nouvel identifiant.

Appelez l'API du serveur: /auth/registerRequest

Ouvrez AuthRepository.kt et recherchez TODO(1).

Ici, registerRequest est la méthode appelée lorsque l'utilisateur appuie sur le bouton d'action flottant. Nous aimerions que cette méthode appelle l'API du serveur /auth/registerRequest. L'API renvoie un ApiResult avec tous les PublicKeyCredentialCreationOptions dont le client a besoin pour générer de nouveaux identifiants.

Nous pouvons ensuite appeler getRegisterPendingIntent avec les options. Cette API FIDO2 renvoie un PendingIntent Android pour ouvrir une boîte de dialogue d'empreinte digitale et générer un nouvel identifiant. Nous pouvons renvoyer ce PendingIntent à l'appelant.

La méthode se présente alors comme suit :

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
}

Ouvrir la boîte de dialogue d'enregistrement de l'empreinte

Ouvrez HomeFragment.kt et recherchez TODO(2).

C'est là que l'UI récupère l'intent à partir de AuthRepository. Ici, nous allons utiliser le membre createCredentialIntentLauncher pour lancer le PendingIntent obtenu à l'étape précédente. Une boîte de dialogue s'ouvre pour la génération d'identifiants.

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

Recevoir ActivityResult avec le nouvel identifiant

Ouvrez HomeFragment.kt et recherchez TODO(3).

Cette méthode handleCreateCredentialResult est appelée après la fermeture de la boîte de dialogue de l'empreinte. Si un identifiant a bien été généré, le membre data d'ActivityResult contient les informations d'identification.

Tout d'abord, nous devons extraire un PublicKeyCredential du data. L'intent de données comporte un champ supplémentaire de tableau d'octets avec la clé Fido.FIDO2_KEY_CREDENTIAL_EXTRA. Dans PublicKeyCredential, vous pouvez utiliser une méthode statique appelée deserializeFromBytes pour transformer le tableau d'octets en un objet PublicKeyCredential.

Vérifiez ensuite si le membre response de cet objet d'identification est un AuthenticationErrorResponse. Si c'est le cas, cela signifie qu'une erreur s'est produite lors de la génération des identifiants. sinon nous pouvons envoyer les identifiants à notre backend.

Une fois terminée, la méthode se présente comme suit:

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)
      }
    }
  }
}

Appelez l'API du serveur: /auth/registerResponse

Ouvrez AuthRepository.kt et recherchez TODO(4).

Cette méthode registerResponse est appelée une fois que l'UI a généré un nouvel identifiant et que nous voulons le renvoyer au serveur.

L'objet PublicKeyCredential contient des informations sur les nouveaux identifiants générés. Nous voulons maintenant mémoriser l'ID de notre clé locale afin de la distinguer des autres clés enregistrées sur le serveur. Dans l'objet PublicKeyCredential, prenez sa propriété rawId et placez-la dans une variable de chaîne locale à l'aide de toBase64.

Nous sommes maintenant prêts à envoyer les informations au serveur. Utilisez api.registerResponse pour appeler l'API du serveur et renvoyer la réponse. La valeur renvoyée contient la liste de tous les identifiants enregistrés sur le serveur, y compris le nouveau.

Enfin, nous pouvons enregistrer les résultats dans DataStore. La liste des identifiants doit être enregistrée avec la clé CREDENTIALS en tant que StringSet. Vous pouvez utiliser toStringSet pour convertir la liste d'identifiants en StringSet.

De plus, nous enregistrons l'identifiant avec la clé 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)
  }
}

Exécutez l'application. Vous pourrez cliquer sur le bouton d'action flottant et enregistrer un nouvel identifiant.

7d64d9289c5a3cbc.png

6. Authentifier l'utilisateur avec une empreinte

Un identifiant est désormais enregistré sur l'application et sur le serveur. Nous pouvons maintenant l'utiliser pour permettre à l'utilisateur de se connecter. Nous ajoutons une fonctionnalité de connexion par empreinte digitale à AuthFragment. Lorsqu'un utilisateur y a accès, une boîte de dialogue d'empreinte digitale s'affiche. Une fois l'authentification effectuée, l'utilisateur est redirigé vers HomeFragment.

Appelez l'API du serveur: /auth/signinRequest

Ouvrez AuthRepository.kt et recherchez TODO(5).

Cette méthode signinRequest est appelée lorsque AuthFragment est ouvert. Ici, nous voulons demander le serveur et voir si nous pouvons laisser l'utilisateur se connecter avec FIDO2.

Nous devons d'abord récupérer PublicKeyCredentialRequestOptions sur le serveur. Utilisez api.signInRequest pour appeler l'API du serveur. Le ApiResult renvoyé contient PublicKeyCredentialRequestOptions.

Avec PublicKeyCredentialRequestOptions, nous pouvons utiliser l'API FIDO2 getSignIntent pour créer un PendingIntent afin d'ouvrir la boîte de dialogue de l'empreinte.

Enfin, nous pouvons renvoyer le PendingIntent à l'UI.

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
}

Ouvrir la boîte de dialogue d'empreinte digitale pour l'assertion

Ouvrez AuthFragment.kt et recherchez TODO(6).

C'est à peu près la même chose que pour l'enregistrement. Nous pouvons lancer la boîte de dialogue d'empreinte digitale avec le membre signIntentLauncher.

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

Gérer l'ActivityResult

Ouvrez AuthFragment.kt et recherchez TODO(7).

Là encore, c'est la même chose que pour l'enregistrement. Nous pouvons extraire PublicKeyCredential, rechercher une erreur et la transmettre à 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)
      }
    }
  }
}

Appelez l'API du serveur: /auth/signinResponse

Ouvrez AuthRepository.kt et recherchez TODO(8).

L'objet PublicKeyCredential comporte un ID d'identification sous la forme keyHandle. Comme pour le flux d'inscription, enregistrons-la dans une variable de chaîne locale afin de pouvoir la stocker ultérieurement.

Nous sommes maintenant prêts à appeler l'API du serveur avec api.signinResponse. La valeur renvoyée contient une liste d'identifiants.

La connexion est maintenant établie. Nous devons stocker tous les résultats dans notre DataStore. La liste des identifiants doit être stockée sous forme de StringSet avec la clé CREDENTIALS. L'ID local que nous avons enregistré ci-dessus doit être stocké sous forme de chaîne avec la clé LOCAL_CREDENTIAL_ID.

Enfin, nous devons mettre à jour l'état de connexion afin que l'UI puisse rediriger l'utilisateur vers HomeFragment. Pour ce faire, émettez un objet SignInState.SignedIn au SharedFlow nommé signInStateMutable. Nous voulons également appeler refreshCredentials pour récupérer les identifiants de l'utilisateur afin qu'ils soient listés dans l'interface utilisateur.

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)
  }
}

Exécutez l'application et cliquez sur "Reauth" (Réauthentifier). pour ouvrir AuthFragment. Vous devriez maintenant voir une boîte de dialogue vous invitant à vous connecter avec votre empreinte digitale.

45f81419f84952c8.png

Félicitations ! Vous savez maintenant utiliser l'API FIDO2 sur Android pour l'enregistrement et la connexion.

7. Félicitations !

Vous avez terminé l'atelier de programmation Votre première API Android FIDO2.

Connaissances acquises

  • Enregistrer un identifiant à l'aide d'un authentificateur de plate-forme de validation des utilisateurs
  • Authentifier un utilisateur à l'aide d'un authentificateur enregistré
  • Options disponibles pour enregistrer un nouvel authentificateur.
  • Bonnes pratiques concernant l'expérience utilisateur pour la réauthentification à l'aide d'un capteur biométrique.

Étape suivante

  • Découvrez comment créer une expérience similaire sur un site Web.

Pour l'apprendre, suivez le premier atelier de programmation WebAuthn.

Ressources

Un grand merci à Yuriy Ackermann de FIDO Alliance pour votre aide.