Présentation
Voici une présentation générale des principales étapes de l'enregistrement d'une clé d'accès :

- Définissez les options de création d'une clé d'accès. Envoyez-les au client pour pouvoir les transmettre à votre appel de création de clé d'accès : l'appel d'API WebAuthn
navigator.credentials.createsur le Web etcredentialManager.createCredentialsur Android. Une fois que l'utilisateur a confirmé la création de la clé d'accès, l'appel de création de la clé d'accès est résolu et renvoie un identifiantPublicKeyCredential. - Validez 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 FIDO côté serveur. Elle propose généralement une fonction utilitaire qui peut créer ces options pour vous. 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 la partie de confiance et une configuration pour les propriétés des identifiants 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 chargée de créer l'objet PublicKeyCredentialCreationOptions.
Certains champs de PublicKeyCredentialCreationOptions peuvent être des constantes. D'autres doivent être définis de manière dynamique sur le serveur :
rpId: pour renseigner l'ID de la partie de confiance 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.nameetuser.displayName: pour renseigner ces champs, utilisez les informations de session de l'utilisateur connecté (ou les informations du nouveau compte utilisateur, si l'utilisateur crée une clé d'accès lors de l'inscription).user.nameest généralement une adresse e-mail et est unique pour la partie de confiance.user.displayNameest un nom convivial. Notez que toutes les plates-formes n'utilisent pasdisplayName.user.id: chaîne aléatoire unique générée lors de la création du compte. Il doit être permanent, contrairement à un nom d'utilisateur qui peut être modifiable. L'ID utilisateur identifie un compte, mais 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, mais si nécessaire, créez-en un spécifiquement pour les clés d'accès afin qu'il ne contienne aucune information permettant d'identifier personnellement l'utilisateur.excludeCredentials: liste des ID d'identifiants existants pour éviter de dupliquer une clé d'accès du fournisseur de clés d'accès. Pour renseigner ce champ, recherchez dans votre base de données les identifiants existants pour cet utilisateur. Pour en savoir plus, consultez Empêcher la création d'une clé d'accès si elle existe déjà.challenge: Pour l'enregistrement des identifiants, la question n'est pas pertinente, sauf si vous utilisez l'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 l'attestation, le challenge reste un champ obligatoire. Pour savoir comment créer un test sécurisé pour l'authentification, consultez Authentification par clé d'accès côté serveur.
Encodage et décodage
PublicKeyCredentialCreationOptions envoyé par le serveur. Les champs binaires tels que challenge et user.id sont encodés côté serveur en base64URL afin que les options puissent être transmises au format JSON via HTTPS.L'objet WebAuthn PublicKeyCredentialCreationOptions standard inclut des champs de type ArrayBuffer, qui ne peuvent pas être directement sérialisés avec JSON.stringify(). Pour envoyer ces options via HTTPS, les champs binaires doivent être encodés en tant que chaînes base64URL :
- Sur le serveur, votre bibliothèque côté serveur FIDO (comme SimpleWebAuthn) produit automatiquement un objet d'options sérialisable au format JSON (par exemple,
PublicKeyCredentialCreationOptionsJSON) avec des champs encodés en Base64URL. - Sur le client, les navigateurs récents sont compatibles avec
PublicKeyCredential.parseCreationOptionsFromJSON()pour convertir les options JSON directement enPublicKeyCredentialCreationOptions. Lorsque vous utilisez des bibliothèques côté client telles que@simplewebauthn/browser, des fonctions commestartRegistration({ optionsJSON: options })gèrent automatiquement cette conversion.
Exemple de code : créer des options de création d'identifiants
Dans nos exemples, nous utilisons la bibliothèque SimpleWebAuthn. Ici, nous confions la création des options d'identifiants de clé publique à sa fonction generateRegistrationOptions.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
} from '@simplewebauthn/server';
import { isoBase64URL, isoUint8Array } 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: cred.id,
type: 'public-key',
transports: cred.transports,
});
}
}
// Generate registration options for WebAuthn create
const options = await generateRegistrationOptions({
rpName: process.env.RP_NAME,
rpID: process.env.HOSTNAME,
userID: isoUint8Array.fromUTF8String(user.id),
userName: user.username,
userDisplayName: user.displayName || '',
attestationType: 'none',
excludeCredentials,
authenticatorSelection: {
authenticatorAttachment: 'platform',
residentKey: 'required',
userVerification: 'preferred',
},
});
// 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
navigator.credentials.create renvoie un objet PublicKeyCredential.Lorsque navigator.credentials.create est résolu 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.
Confiez cette étape de validation à votre bibliothèque côté serveur FIDO. Il propose généralement une fonction utilitaire à cet effet. SimpleWebAuthn propose par exemple verifyRegistrationResponse. Pour en savoir plus sur ce qui se passe en arrière-plan, consultez l'annexe : validation de la réponse d'enregistrement.
Une fois la validation réussie, 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 à ces identifiants.
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 il 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 que vous pouvez utiliser pour stocker les informations d'identification :

- Tableau "Utilisateurs" :
user_id: ID utilisateur principal. Identifiant aléatoire, unique et permanent de l'utilisateur. Utilisez-le comme clé primaire pour votre table Users.username: nom d'utilisateur défini par l'utilisateur, potentiellement modifiable.passkey_user_id: ID utilisateur spécifique à la clé d'accès, sans informations permettant d'identifier personnellement l'utilisateur, représenté paruser.iddans vos options d'enregistrement. Lorsque l'utilisateur tente de s'authentifier ultérieurement, l'authentificateur rend cepasskey_user_iddisponible dans sa réponse d'authentification dansuserHandle. Nous vous recommandons de ne pas définirpasskey_user_idcomme clé primaire. Les clés primaires ont tendance à devenir des informations permettant d'identifier personnellement l'utilisateur de facto dans les systèmes, car elles sont largement utilisées.
- Tableau Identifiants de clé publique :
id: ID des identifiants. Utilisez-le comme clé primaire pour votre table Identifiants de clé publique.public_key: clé publique de l'identifiant.passkey_user_id: utilisez cette 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 le fournisseur de clés d'accès. Le stockage de l'état de sauvegarde est utile si vous envisagez de supprimer les mots de passe à l'avenir pour les utilisateurs qui possèdent des clés d'accèsbacked_up. Vous pouvez vérifier si la clé d'accès est sauvegardée en examinant l'indicateur BE dansauthenticatorDataou en utilisant une fonctionnalité de bibliothèque côté serveur FIDO, généralement disponible pour vous permettre d'accéder facilement à ces informations. Le stockage de l'éligibilité à la sauvegarde peut être utile pour répondre aux éventuelles demandes des utilisateurs.name: (facultatif) nom à afficher pour l'identifiant, afin de permettre aux utilisateurs de donner des noms personnalisés aux identifiants.transports: tableau de 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ùallowCredentialsn'est pas vide.
Il peut être utile de stocker d'autres informations pour améliorer l'expérience utilisateur, comme le fournisseur de la clé d'accès, l'heure de création des identifiants et l'heure de la dernière utilisation. Pour en savoir plus, consultez Conception de l'interface utilisateur des clés d'accès.
Exemple de code : stocker l'identifiant
Dans nos exemples, nous utilisons la bibliothèque SimpleWebAuthn.
Ici, nous confions la validation de la réponse d'enregistrement à sa 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 {
aaguid,
credential,
credentialBackedUp
} = registrationInfo;
// Name the credential based on AAGUID
const name =
aaguid === undefined ||
aaguid === '00000000-0000-0000-0000-000000000000' ?
req.useragent?.platform : aaguids[aaguid]?.name;
const base64PublicKey = isoBase64URL.fromBuffer(credential.publicKey);
// Existing, signed-in user
const { user } = res.locals;
// Save the credential
await Credentials.update({
id: credential.id,
passkey_user_id: user.passkey_user_id,
publicKey: base64PublicKey,
name,
aaguid,
transports: credential.transports || response.response.transports,
backed_up: credentialBackedUp,
registered_at: new Date().getTime()
});
// 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.clientDataJSONest une version JSON des données client, qui, sur le Web, sont les données telles qu'elles sont vues par le navigateur. Il contient l'origine de la RP, la question d'authentification etandroidPackageNamesi le client est une application Android. En tant que RP, la lecture declientDataJSONvous donne accès aux informations que le navigateur a vues au moment de la requêtecreate.response.attestationObjectcontient deux informations :attestationStatement, qui n'est pas pertinent, sauf si vous utilisez l'attestation.authenticatorDatacorrespond aux données telles qu'elles sont vues par le fournisseur de clés d'accès. En tant que tiers de confiance, la lecture deauthenticatorDatavous donne accès aux données vues par le fournisseur de clé d'accès et renvoyées au moment de la requêtecreate.
authenticatorData contient des informations essentielles sur l'identifiant de clé publique associé à la clé d'accès nouvellement créée :
- La clé publique elle-même et un ID d'identification unique.
- ID de tiers assujetti à des restrictions associé aux identifiants.
- Indicateurs décrivant l'état de l'utilisateur lors de la création de la clé d'accès : si un utilisateur était réellement présent et s'il a été validé (voir Présentation détaillée de userVerification).
- L'AAGUID est un identifiant du fournisseur de clé d'accès, comme le Gestionnaire de mots de passe de Google. En fonction de l'AAGUID, vous pouvez identifier le fournisseur de la clé d'accès et afficher son nom sur une page de gestion des clés d'accès. (voir Déterminer le fournisseur de clés d'accès avec l'AAGUID)
Même si authenticatorData est imbriqué dans attestationObject, les informations qu'il contient sont nécessaires pour l'implémentation de votre clé d'accès, que vous utilisiez ou non l'attestation. authenticatorData est encodé et contient des champs encodés au 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, envisagez d'utiliser getAuthenticatorData() côté client pour vous épargner du travail d'analyse et de décodage côté serveur.
Annexe : Validation de la réponse d'enregistrement
En arrière-plan, la vérification de la réponse d'enregistrement consiste à effectuer les vérifications suivantes :
- Assurez-vous que l'ID de la partie de confiance correspond à votre site.
- Assurez-vous que l'origine de la requête est une origine attendue pour votre site (URL du site principal, application Android).
- Si vous exigez la vérification de l'utilisateur, assurez-vous que l'option de vérification de l'utilisateur
authenticatorData.uvest définie surtrue. - L'indicateur de présence de l'utilisateur
authenticatorData.upest généralement défini surtrue, mais si l'identifiant est créé de manière conditionnelle, il est défini surfalse. - Vérifiez que le client a pu fournir le test que vous lui avez donné. Si vous n'utilisez pas l'attestation, cette vérification n'est pas importante. Toutefois, 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'ID d'identifiant n'est pas encore enregistré pour un utilisateur.
- Vérifiez que l'algorithme utilisé par le fournisseur de clé d'accès pour créer l'identifiant est un algorithme que vous avez listé (dans chaque champ
algdepublicKeyCredentialCreationOptions.pubKeyCredParams, qui est généralement défini dans votre bibliothèque côté serveur et n'est pas visible par vous). Cela garantit que les utilisateurs ne peuvent s'enregistrer qu'avec les algorithmes que vous avez choisi d'autoriser.
Pour en savoir plus, consultez le code source de SimpleWebAuthn pour verifyRegistrationResponse ou parcourez la liste complète des vérifications dans la spécification.