Présentation
Voici une présentation générale des principales étapes à suivre pour enregistrer une clé d'accès:
- Définissez des options pour créer une clé d'accès. Envoyez-les au client afin de pouvoir les transmettre à votre appel de création de clé d'accès: l'API WebAuthn appelle
navigator.credentials.create
sur le Web etcredentialManager.createCredential
sur Android. Une fois que l'utilisateur a confirmé la création de la clé d'accès, l'appel de création de clé d'accès est résolu et renvoie un identifiantPublicKeyCredential
. - Vérifiez l'identifiant et stockez-le sur le serveur.
Les sections suivantes décrivent les spécificités de chaque étape.
Créer des options de création d'identifiants
La première étape à effectuer sur le serveur consiste à créer un objet PublicKeyCredentialCreationOptions
.
Pour ce faire, utilisez votre bibliothèque côté serveur FIDO. Elle offre généralement une fonction utilitaire qui crée ces options à votre place. SimpleWebAuthn propose, par exemple, generateRegistrationOptions
.
PublicKeyCredentialCreationOptions
doit inclure tout ce qui est nécessaire à la création de la clé d'accès: des informations sur l'utilisateur, sur le RP et une configuration des propriétés de l'identifiant que vous créez. Une fois que vous avez défini tous ces éléments, transmettez-les si nécessaire à la fonction de votre bibliothèque côté serveur FIDO qui est chargée de créer l'objet PublicKeyCredentialCreationOptions
.
Certains champs de PublicKeyCredentialCreationOptions
peuvent être des constantes. D'autres doivent être définies dynamiquement sur le serveur:
rpId
: pour renseigner l'ID de RP sur le serveur, utilisez des fonctions ou des variables côté serveur qui vous fournissent le nom d'hôte de votre application Web, par exempleexample.com
.user.name
etuser.displayName
:pour renseigner ces champs, utilisez les informations de session de l'utilisateur connecté (ou les informations du nouveau compte, s'il crée une clé d'accès lors de l'inscription).user.name
est généralement une adresse e-mail et est propre au tiers assujetti à des restrictions.user.displayName
est un nom convivial. Notez que toutes les plates-formes n'utilisent pasdisplayName
.user.id
: chaîne aléatoire et unique générée lors de la création du compte. Il doit être permanent, contrairement à un nom d'utilisateur qui peut être modifié. L'ID utilisateur permet d'identifier un compte, mais il ne doit pas contenir d'informations permettant d'identifier personnellement l'utilisateur. Vous disposez probablement déjà d'un ID utilisateur dans votre système. Toutefois, si nécessaire, créez-en un spécialement pour les clés d'accès afin qu'elles ne contiennent aucune information permettant d'identifier personnellement l'utilisateur.excludeCredentials
: liste des ID des identifiants existants pour empêcher la duplication d'une clé d'accès provenant du fournisseur. Pour renseigner ce champ, recherchez les identifiants existants de cet utilisateur dans votre base de données. Pour en savoir plus, consultez Empêcher la création d'une clé d'accès s'il en existe déjà une.challenge
: pour l'enregistrement d'identifiants, la question d'authentification n'est pas pertinente, sauf si vous utilisez une attestation, une technique plus avancée pour valider l'identité d'un fournisseur de clés d'accès et les données qu'il émet. Toutefois, même si vous n'utilisez pas d'attestation, le champ d'authentification reste obligatoire. Dans ce cas, pour plus de simplicité, vous pouvez définir ce défi sur un seul0
. Pour savoir comment créer une question d'authentification sécurisée, consultez Authentification par clé d'accès côté serveur.
Encodage et décodage
![PublicKeyCredentialCreationOptions envoyés par le serveur](https://developers.google.cn/identity/passkeys/developer-guides/server-registration/images/server-publicKeyCredentialCreationOptions.png?authuser=5&hl=fr)
PublicKeyCredentialCreationOptions
envoyé par le serveur. challenge
, user.id
et excludeCredentials.credentials
doivent être encodés côté serveur dans base64URL
pour que PublicKeyCredentialCreationOptions
puisse être diffusé via HTTPS.Les PublicKeyCredentialCreationOptions
incluent des champs qui sont des ArrayBuffer
. Ils ne sont donc pas compatibles avec JSON.stringify()
. Cela signifie qu'actuellement, pour envoyer PublicKeyCredentialCreationOptions
via HTTPS, certains champs doivent être encodés manuellement sur le serveur à l'aide de base64URL
, puis décodés sur le client.
- Sur le serveur, l'encodage et le décodage sont généralement gérés par votre bibliothèque FIDO côté serveur.
- Sur le client, l'encodage et le décodage doivent être effectués manuellement pour le moment. Cela sera simplifié à l'avenir: une méthode de conversion des options JSON au format
PublicKeyCredentialCreationOptions
sera disponible. Vérifiez l'état de l'implémentation dans Chrome.
Exemple de code: créer des options de création d'identifiants
Nous utilisons la bibliothèque SimpleWebAuthn dans nos exemples. Ici, nous transférons la création des options d'identifiants de clé publique à sa fonction generateRegistrationOptions
.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
const { user } = res.locals;
// Ensure you nest verification function calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// `excludeCredentials` prevents users from re-registering existing
// credentials for a given passkey provider
const excludeCredentials = [];
const credentials = Credentials.findByUserId(user.id);
if (credentials.length > 0) {
for (const cred of credentials) {
excludeCredentials.push({
id: isoBase64URL.toBuffer(cred.id),
type: 'public-key',
transports: cred.transports,
});
}
}
// Generate registration options for WebAuthn create
const options = generateRegistrationOptions({
rpName: process.env.RP_NAME,
rpID: process.env.HOSTNAME,
userID: user.id,
userName: user.username,
userDisplayName: user.displayName || '',
attestationType: 'none',
excludeCredentials,
authenticatorSelection: {
authenticatorAttachment: 'platform',
requireResidentKey: true
},
});
// Keep the challenge in the session
req.session.challenge = options.challenge;
return res.json(options);
} catch (e) {
console.error(e);
return res.status(400).send({ error: e.message });
}
});
Stocker la clé publique
![PublicKeyCredentialCreationOptions envoyés par le serveur](https://developers.google.cn/identity/passkeys/developer-guides/server-registration/images/server-publicKeyCredentialCreationOptions.png?authuser=5&hl=fr)
navigator.credentials.create
renvoie un objet PublicKeyCredential
.Lorsque navigator.credentials.create
est résolu correctement sur le client, cela signifie qu'une clé d'accès a bien été créée. Un objet PublicKeyCredential
est renvoyé.
L'objet PublicKeyCredential
contient un objet AuthenticatorAttestationResponse
, qui représente la réponse du fournisseur de clés d'accès à l'instruction du client de créer une clé d'accès. Il contient des informations sur les nouveaux identifiants dont vous avez besoin en tant que RP pour authentifier l'utilisateur ultérieurement. Pour en savoir plus sur AuthenticatorAttestationResponse
, consultez l'annexe: AuthenticatorAttestationResponse
.
Envoyez l'objet PublicKeyCredential
au serveur. Une fois que vous l'avez reçu, validez-le.
Transférez cette étape de validation à votre bibliothèque côté serveur FIDO. Il offre généralement une fonction utilitaire à cet effet. SimpleWebAuthn propose, par exemple, verifyRegistrationResponse
. Découvrez ce qui se passe en arrière-plan dans l'annexe: vérification de la réponse d'inscription.
Une fois la validation effectuée, stockez les informations d'identification dans votre base de données afin que l'utilisateur puisse s'authentifier ultérieurement avec la clé d'accès associée à cet identifiant.
Utilisez une table dédiée pour les identifiants de clé publique associés aux clés d'accès. Un utilisateur ne peut avoir qu'un seul mot de passe, mais peut avoir plusieurs clés d'accès (par exemple, une clé d'accès synchronisée via le trousseau iCloud d'Apple et une autre via le Gestionnaire de mots de passe de Google).
Voici un exemple de schéma permettant de stocker des informations d'identification:
- Table Users :
user_id
: ID de l'utilisateur principal. ID aléatoire, unique et permanent pour l'utilisateur. Utilisez-la comme clé primaire pour votre table Utilisateurs.username
: nom d'utilisateur défini par l'utilisateur, potentiellement modifiable.passkey_user_id
: ID utilisateur sans informations personnelles spécifique à la clé d'accès, représenté paruser.id
dans vos options d'inscription. Lorsque l'utilisateur tente ultérieurement de s'authentifier, l'authentificateur rend cepasskey_user_id
disponible dans sa réponse d'authentification dansuserHandle
. Nous vous recommandons de ne pas définirpasskey_user_id
comme clé primaire. Les clés primaires ont tendance à devenir de facto des informations permettant d’identifier personnellement l’utilisateur dans les systèmes, car elles sont largement utilisées.
- Table Public key credentials (Identifiants de clé publique) :
id
: identifiant. Utilisez-la comme clé primaire pour votre table Identifiants de clé publique.public_key
: clé publique de l'identifiant.passkey_user_id
: utilisez cette clé comme clé étrangère pour établir un lien avec la table Users.backed_up
: une clé d'accès est sauvegardée si elle est synchronisée par son fournisseur. Le stockage de l'état de sauvegarde est utile si vous souhaitez envisager de supprimer les mots de passe à l'avenir pour les utilisateurs qui détiennent des clés d'accèsbacked_up
. Pour vérifier si la clé d'accès est sauvegardée, examinez les indicateurs dansauthenticatorData
ou utilisez une bibliothèque côté serveur FIDO généralement disponible pour faciliter l'accès à ces informations. Stocker l'éligibilité à la sauvegarde peut être utile pour répondre aux demandes des utilisateurs potentiels.name
: (facultatif) nom à afficher pour les identifiants afin de permettre aux utilisateurs d'attribuer des noms personnalisés aux identifiants.transports
: tableau des transports. Le stockage des transports est utile pour l'expérience utilisateur d'authentification. Lorsque des transports sont disponibles, le navigateur peut se comporter en conséquence et afficher une UI correspondant au transport utilisé par le fournisseur de clés d'accès pour communiquer avec les clients, en particulier pour les cas d'utilisation de réauthentification oùallowCredentials
n'est pas vide.
D'autres informations peuvent être utiles à stocker à des fins d'expérience utilisateur, telles que le fournisseur de clés d'accès, l'heure de création des identifiants et la date de leur dernière utilisation. Pour en savoir plus, consultez Conception de l'interface utilisateur pour les clés d'accès.
Exemple de code: stocker l'identifiant
Nous utilisons la bibliothèque SimpleWebAuthn dans nos exemples.
Ici, nous transférons la validation de la réponse d'inscription à la fonction verifyRegistrationResponse
.
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
const expectedChallenge = req.session.challenge;
const expectedOrigin = getOrigin(req.get('User-Agent'));
const expectedRPID = process.env.HOSTNAME;
const response = req.body;
// This sample code is for registering a passkey for an existing,
// signed-in user
// Ensure you nest verification function calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// Verify the credential
const { verified, registrationInfo } = await verifyRegistrationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
requireUserVerification: false,
});
if (!verified) {
throw new Error('Verification failed.');
}
const { credentialPublicKey, credentialID } = registrationInfo;
// Existing, signed-in user
const { user } = res.locals;
// Save the credential
await Credentials.update({
id: base64CredentialID,
publicKey: base64PublicKey,
// Optional: set the platform as a default name for the credential
// (example: "Pixel 7")
name: req.useragent.platform,
transports: response.response.transports,
passkey_user_id: user.passkey_user_id,
backed_up: registrationInfo.credentialBackedUp
});
// Kill the challenge for this session
delete req.session.challenge;
return res.json(user);
} catch (e) {
delete req.session.challenge;
console.error(e);
return res.status(400).send({ error: e.message });
}
});
Annexe: AuthenticatorAttestationResponse
AuthenticatorAttestationResponse
contient deux objets importants:
response.clientDataJSON
est une version JSON des données client. Sur le Web, il s'agit des données affichées par le navigateur. Il contient l'origine de la RP, le défi etandroidPackageName
si le client est une application Android. En tant que RP, lireclientDataJSON
vous donne accès aux informations que le navigateur a vues au moment de la requêtecreate
.response.attestationObject
contient deux informations :attestationStatement
, ce qui n'est pas pertinent, sauf si vous utilisez une attestation.authenticatorData
correspond aux données affichées par le fournisseur de clés d'accès. En tant que RP, lireauthenticatorData
vous donne accès aux données vues par le fournisseur de clés d'accès et renvoyées au moment de la requêtecreate
.
authenticatorData
contient des informations essentielles sur les identifiants de clé publique associés à la clé d'accès nouvellement créée:
- L'identifiant de clé publique proprement dit et un identifiant unique associé à celle-ci.
- ID de tiers assujetti à des restrictions associé au justificatif.
- Indicateurs décrivant l'état de l'utilisateur au moment de la création de la clé d'accès, indiquant si un utilisateur était effectivement présent et si celui-ci a bien été validé (voir
userVerification
). - AAGUID, qui identifie le fournisseur de clé d'accès. L'affichage du fournisseur de clés d'accès peut être utile à vos utilisateurs, en particulier s'ils ont enregistré une clé d'accès pour votre service auprès de plusieurs fournisseurs.
Même si authenticatorData
est imbriqué dans attestationObject
, les informations qu'il contient sont nécessaires à l'implémentation de votre clé d'accès, que vous utilisiez ou non une attestation. authenticatorData
est encodé et contient des champs encodés dans un format binaire. Votre bibliothèque côté serveur gère généralement l'analyse et le décodage. Si vous n'utilisez pas de bibliothèque côté serveur, pensez à exploiter getAuthenticatorData()
côté client pour vous éviter d'avoir à analyser et à décoder des tâches côté serveur.
Annexe: Vérification de la réponse d'inscription
En arrière-plan, la validation de la réponse d'enregistrement comprend les vérifications suivantes:
- Vérifiez que l'ID du tiers assujetti à des restrictions correspond à votre site.
- Assurez-vous que l'origine de la requête est une origine attendue de votre site (URL du site principal, application Android).
- Si vous exigez la validation de l'utilisateur, assurez-vous que l'indicateur de validation
authenticatorData.uv
est défini surtrue
. Vérifiez que l'indicateur de présence de l'utilisateurauthenticatorData.up
est défini surtrue
, car la présence de l'utilisateur est toujours obligatoire pour les clés d'accès. - Vérifiez que le client a pu proposer le défi que vous lui avez proposé. Si vous n'utilisez pas d'attestation, cette vérification n'est pas importante. Cependant, l'implémentation de cette vérification est une bonne pratique: elle garantit que votre code est prêt si vous décidez d'utiliser l'attestation à l'avenir.
- Assurez-vous que l'identifiant n'est encore enregistré pour aucun utilisateur.
- Vérifiez que l'algorithme utilisé par le fournisseur de clés d'accès pour créer l'identifiant est bien un algorithme que vous avez indiqué (dans chaque champ
alg
depublicKeyCredentialCreationOptions.pubKeyCredParams
, qui est généralement défini dans votre bibliothèque côté serveur et que vous ne pouvez pas le voir). Ainsi, les utilisateurs ne pourront s'inscrire qu'avec les algorithmes que vous avez choisi d'autoriser.
Pour en savoir plus, consultez le code source de SimpleWebAuthn pour verifyRegistrationResponse
ou consultez la liste complète des vérifications dans la spécification.