Découvrez comment simplifier les parcours d'authentification à l'aide de l'API Credential Manager dans votre application Android

1. Avant de commencer

Les solutions d'authentification traditionnelles présentent plusieurs défauts en matière de sécurité et de facilité d'utilisation.

L'utilisation des mots de passe est très répandue, mais…

  • Les utilisateurs ont tendance à oublier leurs mots de passe.
  • Les utilisateurs doivent apprendre à créer des mots de passe sécurisés.
  • Les utilisateurs sont vulnérables à l'hameçonnage par des pirates informatiques, qui peuvent facilement découvrir, collecter et utiliser les mots de passe.

Android s'est attelé à la création d'une API Credential Manager afin de simplifier la procédure de connexion et de réduire les risques de sécurité en prenant en charge les clé d'accès, qui représentent la prochaine génération de méthodes standard dans l'industrie pour l'authentification sans mot de passe.

Credential Manager allie cette compatibilité avec les clés d'accès aux méthodes d'authentification traditionnelles, telles que les mots de passe, Se connecter avec Google, etc.

Les utilisateurs pourront créer des clés d'accès et les stocker dans le Gestionnaire de mots de passe de Google, qui les synchronisera sur tous les appareils Android connectés au même compte. Vous devez créer une clé d'accès, l'associer à un compte utilisateur et stocker sa clé publique sur un serveur afin qu'un utilisateur puisse s'en servir pour se connecter.

Dans cet atelier de programmation, vous allez découvrir comment vous connecter à l'aide avec l'API Credential Manager avec une clé d'accès ou un mot de passe que vous utiliserez à des fins d'authentification à l'avenir. Nous examinerons deux flux :

  • Inscription à l'aide de clés d'accès ou d'un mot de passe.
  • Connexion à l'aide de clés d'accès ou d'un mot de passe enregistré.

Conditions préalables

  • Vous disposez de connaissances de base concernant l'exécution d'applications dans Android Studio.
  • Vous disposez de connaissances de base concernant le flux d'authentification dans les applications Android.
  • Vous disposez de connaissances de base concernant les clés d'accès.

Points abordés

  • Créer une clé d'accès.
  • Enregistrer un mot de passe dans le Gestionnaire de mots de passe.
  • Authentifier les utilisateurs avec une clé d'accès ou un mot de passe enregistré.

Ce dont vous avez besoin

L'une des combinaisons "appareil-logiciel" suivantes :

  • Un appareil exécutant Android 9 ou version ultérieure (pour les clés d'accès), et Android 4.4 ou version ultérieure (pour l'authentification par mot de passe par le biais de l'API Credential Manager).
  • De préférence, un appareil doté d'un capteur biométrique.
  • Veillez à enregistrer un identifiant biométrique (ou un écran de verrouillage).
  • La version 1.8.10 du plug-in Kotlin.

2. Configuration

  1. Clonez ce dépôt sur votre ordinateur portable à partir de la branche credman_codelab : https://github.com/android/identity-samples/tree/credman_codelab.
  2. Accédez au module CredentialManager et ouvrez le projet dans Android Studio.

Examiner l'état initial de l'application

Pour observer le fonctionnement à l'état initial de l'application, procédez comme suit :

  1. Lancez l'application.
  2. Vous verrez un écran principal avec des boutons pour s'inscrire et se connecter.
  3. Cliquez sur le bouton d'inscription pour vous inscrire avec une clé d'accès ou un mot de passe.
  4. Vous pouvez cliquer sur le bouton de connexion pour vous identifier à l'aide de la clé d'accès ou du mot de passe enregistré.

8c0019ff9011950a.jpeg

Pour en savoir plus sur les clés d'accès et leur fonctionnement, consultez cette ressource. .

3. Ajouter la possibilité de s'inscrire avec une clé d'accès

Lorsqu'ils s'inscrivent avec un nouveau compte sur une application Android qui utilise l'API Credential Manager, les utilisateurs peuvent créer une clé d'accès associée. Cette clé d'accès sera stockée de façon sécurisée par le fournisseur d'identifiants choisi et servira lors des prochaines connexions sans que l'utilisateur n'ait à saisir son mot de passe.

Vous allez maintenant créer une clé d'accès et enregistrer des identifiants utilisateur à l'aide du capteur biométrique ou de l'écran de verrouillage.

S'inscrire avec une clé d'accès

Dans Credential Manager > app > main > java > SignUpFragment.kt, vous trouverez un champ de texte "username" (nom d'utilisateur) et un bouton pour s'inscrire avec une clé d'accès.

dcc5c529b310f2fb.jpeg

Transmettre le test d'authentification et les autres réponses JSON à l'appel createPasskey()

Pour qu'il soit possible de créer une clé d'accès, vous devez demander au serveur d'obtenir les informations requises, qui doivent être transmises à l'API Credential Manager lors de l'appel createCredential().

Par chance, vous disposez déjà dans vos ressources (RegFromServer.txt) d'une réponse fictive qui retourne les paramètres voulus pour cet atelier de programmation.

  • Dans votre application, accédez à SignUpFragment.kt et localisez la méthode signUpWithPasskeys, dans laquelle sera insérée la logique pour créer une clé d'accès et autoriser l'utilisateur à se connecter. Vous trouverez la méthode dans la même classe.
  • Examinez le bloc Else avec un commentaire invitant à appeler createPasskey(), que vous remplacerez par le code suivant :

SignUpFragment.kt

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

val data = createPasskey()

Cette méthode sera appelée lorsqu'un nom d'utilisateur valide est saisi à l'écran.

  • Dans la méthode createPasskey(), vous devez créer une requête CreatePublicKeyCredentialRequest() avec les paramètres requis retournés.

SignUpFragment.kt

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

val request = CreatePublicKeyCredentialRequest(fetchRegistrationJsonFromServer())

fetchRegistrationJsonFromServer() est une méthode qui lit la réponse JSON d'inscription à partir des ressources et retourne le fichier JSON d'inscription à transmettre lors de la création de la clé d'accès.

  • Localisez la méthode fetchRegistrationJsonFromServer() et remplacez le TODO par le code suivant afin de retourner le fichier JSON et de supprimer l'instruction return de chaîne vide :

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())
  • À cette étape, vous lisez le fichier JSON d'inscription à partir des ressources.
  • Ce fichier JSON comporte 4 champs à remplacer.
  • Le contenu du champ UserId doit être unique, afin qu'un utilisateur puisse créer plusieurs clés d'accès, si nécessaire. Vous pouvez remplacer <userId> par l'identifiant utilisateur généré.
  • Le contenu du champ <challenge> doit également être unique, afin que vous puissiez générer un test d'authentification aléatoire unique. La méthode est déjà dans votre code.

L'extrait de code suivant inclut des exemples des options que vous recevez du serveur :

{
  "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"
  }
}

Le tableau suivant n'est pas exhaustif, mais contient les principaux paramètres du dictionnaire PublicKeyCredentialCreationOptions :

Paramètres

Descriptions

challenge

Chaîne aléatoire générée par le serveur avec un degré d'entropie suffisant pour être impossible à deviner. Longueur minimale recommandée : 16 octets. Ce paramètre est obligatoire mais n'est pas utilisé lors de l'enregistrement, sauf si vous effectuez une attestation.

user.id

Identifiant unique d'un utilisateur. Cette valeur ne doit contenir aucune information permettant d'identifier personnellement l'utilisateur, comme une adresse e-mail ou un nom d'utilisateur. Nous recommandons de générer une valeur aléatoire de 16 octets pour chaque compte.

user.name

Ce champ doit contenir un identifiant unique pour le compte, reconnaissable par l'utilisateur, comme son adresse e-mail ou son nom d'utilisateur. Cet identifiant apparaîtra dans le sélecteur de comptes. (S'il s'agit d'un nom d'utilisateur, spécifiez la même valeur que pour l'authentification par mot de passe.)

user.displayName

Ce champ facultatif permet de spécifier un nom plus convivial pour le compte. Il s'agit d'un nom de compte adapté à un utilisateur humain, dont la seule vocation est d'être affiché.

rp.id

L'entité Partie de confiance correspond aux informations de votre application et a besoin des éléments suivants :

  • Un nom (obligatoire) : le nom de votre application
  • Un identifiant (facultatif) : correspond au domaine ou au sous-domaine. Si rien n'est spécifié, votre domaine actuel sera utilisé.
  • Une icône (facultatif).

pubKeyCredParams

Les paramètres d'identifiants de clé publique constituent la liste des algorithmes et des types de clés autorisés. Cette liste doit contenir au moins un élément.

excludeCredentials

Un utilisateur qui essaie d'enregistrer un appareil pourrait en avoir enregistré d'autres. Le cas échéant, pour limiter la création de multiples identifiants pour un même compte et sur un même authentificateur, vous pouvez ignorer ces appareils. Si le membre transports est fourni, il doit contenir le résultat de l'appel de getTransports() lors de l'enregistrement de chaque identifiant.

authenticatorSelection.authenticatorAttachment

Indique si l'appareil doit ou non être rattaché à la plate-forme, ou si aucune exigence n'est exprimée en ce sens. Définissez le paramètre sur "platform". Cela indique que nous vous utiliser un authentificateur intégré à l'appareil servant de plate-forme, et qu'il ne sera pas demandé à l'utilisateur d'en insérer un (par exemple, une clé de sécurité USB).

residentKey

Indique une valeur "required" (exigé) pour créer une clé d'accès.

Créer un identifiant

  1. Après avoir créé une CreatePublicKeyCredentialRequest(), vous devez appeler createCredential() avec votre requête.

SignUpFragment.kt

//TODO call createCredential() with createPublicKeyCredentialRequest

try {
   response = credentialManager.createCredential(
       requireActivity(),
       request
   ) as CreatePublicKeyCredentialResponse
} catch (e: CreateCredentialException) {
   configureProgress(View.INVISIBLE)
   handlePasskeyFailure(e)
}
  • Transmettez les informations requises à createCredential().
  • Lorsque de la requête est traitée, vous devriez voir apparaître une bottom sheet vous invitant à créer la clé d'accès.
  • L'utilisateur peut alors confirmer son identité grâce au capteur biométrique ou à l'écran de verrouillage, etc.
  • Vous gérez la visibilité des vues et traitez les exceptions si la requête échoue pour une quelconque raison. Dans le cas présent, les messages d'erreur sont consignés et affichés dans l'application, dans une boîte de dialogue d'erreur. Vous pouvez consulter les journaux d'erreur complets dans Android Studio ou avec la commande de débogage adb.

93022cb87c00f1fc.png

  1. Pour finir, vous devez terminer le processus d'inscription en transmettant l'identifiant de clé publique au serveur et en autorisant l'utilisateur à se connecter. L'application reçoit un objet d'identification qui contient une clé publique, que vous pouvez envoyer au serveur pour enregistrer la clé d'accès.

Dans le cas présent, nous utilisons un serveur fictif. Nous renvoyons simplement "true" pour indiquer que le serveur a enregistré la clé publique pour les opérations d'authentification et de validation ultérieures.

Localisez le commentaire pertinent dans la méthode signUpWithPasskeys(), à remplacer par le code suivant :

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 renvoie "true", indiquant que le serveur (fictif) a enregistré la clé publique pour les opérations futures.
  • Vous avez défini l'indicateur SignedInThroughPasskeys sur "true" pour indiquer une connexion à l'aide d'une clé d'accès.
  • Une fois la connexion établie, vous redirigez votre utilisateur vers l'écran d'accueil.

L'extrait de code suivant contient un exemple de réponse attendue :

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

Le tableau suivant n'est pas exhaustif, mais contient les principaux paramètres dans PublicKeyCredential :

Paramètres

Descriptions

id

ID de la clé d'accès créée, encodé en Base64URL. Cet identifiant aide le navigateur à déterminer si une clé d'accès correspondante est enregistrée sur l'appareil au moment de l'authentification. Cette valeur doit être stockée dans la base de données sur le backend.

rawId

Version de l'objet ArrayBuffer de l'ID.

response.clientDataJSON

Données client encodées sous la forme d'un objet ArrayBuffer.

response.attestationObject

Objet d'attestation encodé au format ArrayBuffer. Il contient des informations importantes telles qu'un ID de RP, des indicateurs et une clé publique.

Exécutez l'application. Vous devriez pouvoir cliquer sur le bouton d'inscription avec une clé d'accès, puis créer une clé d'accès.

4. Enregistrer le mot de passe dans Credential Provider

Dans cette application, l'écran d'inscription devrait déjà afficher les options d'inscription avec nom d'utilisateur et mot de passe, qui ont été implémentées à des fins de démonstration.

Pour enregistrer le mot de passe utilisateur dans le fournisseur d'identifiants de l'utilisateur, vous devez implémenter une CreatePasswordRequest pour indiquer à createCredential() d'enregistrer le mot de passe.

  • Localisez la méthode signUpWithPassword() et remplacez le TODO pour appeler createPassword :

SignUpFragment.kt

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

createPassword()
  • Dans la méthode createPassword(), vous devez créer une requête de mot de passe comme ici. Remplacez le TODO par le code suivant :

SignUpFragment.kt

//TODO : CreatePasswordRequest with entered username and password

val request = CreatePasswordRequest(
   binding.username.text.toString(),
   binding.password.text.toString()
)
  • Toujours dans la méthode createPassword(), vous devez créer l'identifiant avec la requête de création de mot de passe, puis enregistrer cet identifiant (le mot de passe utilisateur) dans le fournisseur de mots de passe de l'utilisateur. Remplacez le TODO par le code suivant :

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)
}
  • Vous avez enregistré le mot de passe dans le fournisseur de mot de passe de l'utilisateur afin de permettre l'authentification par mot de passe d'un simple geste.

5. Ajouter la possibilité de s'authentifier avec une clé d'accès ou un mot de passe

Vous voilà prêt à utiliser ces options pour l'authentification sécurisée dans votre application.

629001f4a778d4fb.png

Récupérer le test d'authentification et d'autres options à transmettre à l'appel getPasskey()

Avant de demander à l'utilisateur de s'authentifier, vous devez demander au serveur les paramètres à transmettre dans le fichier JSON WebAuthn, dont un test d'authentification.

Vous disposez déjà dans vos ressources (AuthFromServer.txt) d'une réponse fictive qui retourne les paramètres voulus pour cet atelier de programmation.

  • Dans votre application, accédez à SignInFragment.kt, localisez la méthode signInWithSavedCredentials dans laquelle sera insérée la logique pour l'authentification par clé d'accès ou mot de passe enregistré et pour autoriser l'utilisateur à se connecter :
  • Examinez le bloc Else avec un commentaire invitant à appeler createPasskey(), que vous remplacerez par le code suivant :

SignInFragment.kt

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

val data = getSavedCredentials()
  • Dans la méthode getSavedCredentials(), vous devez créer une GetPublicKeyCredentialOption() avec les paramètres requis pour obtenir les identifiants auprès de votre fournisseur.

SigninFragment.kt

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

val getPublicKeyCredentialOption =
   GetPublicKeyCredentialOption(fetchAuthJsonFromServer(), null)

fetchAuthJsonFromServer() est une méthode qui lit la réponse JSON d'authentification à partir des ressources et retourne le fichier JSON d'authentification pour récupérer toutes les clés d'accès associées à ce compte utilisateur.

Le deuxième paramètre, clientDataHash, est un hachage utilisé pour confirmer l'identité de la Partie de confiance. À spécifier uniquement si vous avez défini GetCredentialRequest.origin. Dans l'application exemple, sa valeur est nulle.

Le troisième paramètre est défini sur "true" si vous préférez un retour d'opération immédiat lorsqu'aucun identifiant n'est disponible, plutôt que de passer à la découverte d'identifiants distants. Défini sur "false" (par défaut) dans le cas contraire.

  • Localisez la méthode fetchAuthJsonFromServer() et remplacez le TODO par le code suivant afin de retourner le fichier JSON et de supprimer l'instruction return de chaîne vide :

SignInFragment.kt

//TODO fetch authentication mock json

return requireContext().readFromAsset("AuthFromServer")

Remarque : Le serveur de cet atelier de programmation est conçu pour retourner un fichier JSON aussi semblable que possible au dictionnaire PublicKeyCredentialRequestOptions transmis à l'API lors de l'appel getCredential(). L'extrait de code suivant inclut quelques exemples d'options attendues :

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

Le tableau suivant n'est pas exhaustif, mais contient les principaux paramètres du dictionnaire PublicKeyCredentialRequestOptions :

Paramètres

Descriptions

challenge

Question d'authentification générée par le serveur dans un objet ArrayBuffer. Ce paramètre est nécessaire pour éviter les attaques par rejeu. N'acceptez jamais deux fois la même question d'authentification dans une réponse. Considérez-la comme un jeton CSRF.

rpId

Un ID de RP est un domaine. Un site Web peut spécifier son propre domaine ou un suffixe enregistrable. Cette valeur doit correspondre au paramètre rp.id utilisé lors de la création de la clé d'accès.

  • Ensuite, vous devez créer un objet PasswordOption() pour récupérer tous les mots de passe enregistrés dans le fournisseur par le biais de l'API Credential Manager pour ce compte utilisateur. Dans la méthode getSavedCredentials(), localisez et remplacez le TODO par ce qui suit :

SigninFragment.kt

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

val getPasswordOption = GetPasswordOption()

Obtenir des identifiants

  • À présent, vous devez appeler la requête getCredential() avec toutes les options ci-dessus, afin de récupérer les identifiants associés :

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

  • Vous transmettez les informations requises à getCredential(). Cette opération récupère la liste des options d'identifiants ainsi qu'un contexte d'activité pour restituer dans la bottom sheet les options correspondant à ce contexte.
  • Lorsque de la requête est traitée, vous devriez voir apparaître une bottom sheet répertoriant tous les identifiants créés pour le compte associé.
  • L'utilisateur peut alors confirmer son identité grâce au capteur biométrique ou à l'écran de verrouillage, etc., afin d'authentifier l'identifiant sélectionné.
  • Vous avez défini l'indicateur SignedInThroughPasskeys sur "true" pour indiquer une connexion à l'aide d'une clé d'accès. Dans le cas contraire, la valeur est "false".
  • Vous gérez la visibilité des vues et traitez les exceptions si la requête échoue pour une quelconque raison. Dans le cas présent, les messages d'erreur sont consignés et affichés dans l'application, dans une boîte de dialogue d'erreur. Vous pouvez consulter les journaux d'erreur complets dans Android Studio ou avec la commande de débogage adb.
  • Pour finir, vous devez terminer le processus d'inscription en transmettant l'identifiant de clé publique au serveur et en autorisant l'utilisateur à se connecter. L'application reçoit un objet d'identification qui contient une clé publique, que vous pouvez envoyer au serveur pour permettre l'authentification avec la clé d'accès.

Dans le cas présent, nous utilisons un serveur fictif. Nous renvoyons simplement "true" pour indiquer que le serveur a validé la clé publique.

Localisez le commentaire pertinent dans la méthode signInWithSavedCredentials(), à remplacer par le code suivant :

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() renvoie "true", indiquant que le serveur (fictif) a validé la clé publique pour les opérations futures.
  • Une fois la connexion établie, vous redirigez votre utilisateur vers l'écran d'accueil.

L'extrait de code suivant inclut un exemple d'objet PublicKeyCredential :

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

Le tableau suivant n'est pas exhaustif, mais contient les principaux paramètres de l'objet PublicKeyCredential :

Paramètres

Descriptions

id

ID de la clé d'accès authentifiée, encodé en Base64URL.

rawId

Version de l'objet ArrayBuffer de l'ID.

response.clientDataJSON

Objet ArrayBuffer des données client. Ce champ contient des informations telles que la question d'authentification et l'origine que le serveur de la RP doit vérifier.

response.authenticatorData

Objet ArrayBuffer des données de l'authentificateur. Ce champ contient des informations telles que l'ID de RP.

response.signature

Objet ArrayBuffer de la signature. Cette valeur est l'élément central de l'identifiant et doit être validée sur le serveur.

response.userHandle

Objet ArrayBuffer contenant l'ID utilisateur défini au moment de la création. Cette valeur peut être utilisée à la place de l'identifiant si le serveur a besoin de choisir les valeurs d'identification qu'il utilise ou si le backend souhaite éviter la création d'un index sur les identifiants.

Exécutez l'application et accédez à l'écran de connexion, puis sélectionnez l'option de connexion à l'aide d'une clé d'accès ou d'un mot de passe enregistré afin de vérifier que vous parvenez à vous connecter avec les identifiants enregistrés.

Essayer

Vous avez implémenté la création de clés d'accès et l'enregistrement de mots de passe avec Credential Manager, ainsi que l'authentification à l'aide de clés d'accès ou de mots de passe enregistrés avec l'API Credential Manager dans votre application Android.

6. Félicitations !

Vous avez terminé cet atelier de programmation. Vous pouvez examiner la version finale à l'adresse https://github.com/android/identity-samples/tree/main/CredentialManager

Si vous avez des questions, posez-les sur StackOverflow avec un tag passkey.

En savoir plus