Ce guide explique comment permettre aux utilisateurs d'enregistrer une clé d'accès pour leur compte, offrant ainsi une alternative de connexion sans mot de passe hautement sécurisée et résistante au hameçonnage.
Exigences concernant la base de données
Pour prendre en charge l'enregistrement des clés d'accès, votre table d'identifiants de base de données doit stocker les champs suivants :
export interface StoredPasskeyCredential {
id: string; // Base64URL-encoded credential ID (unique lookup key)
passkeyUserId: string; // Associated application user ID
credentialPublicKey: string; // Base64URL-encoded public key used to verify assertion signatures
credentialType: "public-key";
credentialDeviceType: "singleDevice" | "multiDevice"; // Helps distinguish device-bound vs cloud-synced passkeys
credentialBackedUp: boolean; // Boolean backup state reported by the authenticator
aaguid: string; // Authenticator Attestation GUID
providerIcon?: string; // Provider icon derived from the AAGUID registry (dark or light theme URLs)
name: string; // Provider name derived from AAGUID registry
transports: string[]; // Array of transport names (e.g. 'internal', 'hybrid') necessary for exclusion options
lastUsedAt?: number; // Optional epoch timestamp of last sign-in
registeredAt: number; // Registration epoch timestamp
counter: number; // Authenticator sign-in signature counter used to prevent replay attacks
}
Côté serveur
Génération d'options
Créez un point de terminaison qui génère des paramètres de création WebAuthn. Utilisez une bibliothèque approuvée conformément aux normes de la catégorie au lieu de créer votre propre cryptographie.
- Utiliser l'ID RP prédéfini : utilisez l'ID RP approprié prédéfini comme chaîne constante.
- Créer un challenge sécurisé : générez un tampon aléatoire à haute entropie et sécurisé par chiffrement sur le serveur, stockez-le de manière sécurisée dans la session de l'utilisateur et encodez-le en Base64URL pour l'envoi des options.
- Évitez les clés d'accès en double : mappez les ID d'identifiants préenregistrés existants de l'utilisateur au tableau d'options
excludeCredentials. Cela empêche l'authentificateur d'enregistrer des identifiants en double sur le même compte de fournisseur de clé d'accès. - Appliquer les identifiants détectables : définissez
requireResidentKey: trueetresidentKey: "required"dans les optionsauthenticatorSelectionpour demander un identifiant détectable, ce qui est nécessaire pour les connexions détectables. - Configurer la validation de l'utilisateur : spécifiez
userVerification: "preferred"ouuserVerification: "required". De nombreux cas d'utilisation de conformité (par exemple, finance, santé) nécessitent que'required'applique la validation des utilisateurs lors de la création. - Déterminer le champ d'application du rattachement :
- Flux de promotion : lorsque vous proposez la création d'une clé d'accès juste après une connexion par mot de passe standard ou une promotion après l'inscription, définissez
authenticatorAttachment: "platform"pour appliquer l'authentificateur de plate-forme et ignorer les invites de clé de sécurité externe. - Flux de gestion : lorsque l'appel est effectué depuis un panneau de paramètres ou de sécurité dédié où les clés de sécurité externes sont acceptées en plus de l'authentificateur de la plate-forme, omettez complètement la propriété
authenticatorAttachment. - Conseil : Acceptez un flag de requête
promotion: booleanpour gérer de manière conditionnelle les deux flux avec un seul point de terminaison.
- Flux de promotion : lorsque vous proposez la création d'une clé d'accès juste après une connexion par mot de passe standard ou une promotion après l'inscription, définissez
// Options generation example
const options = {
challenge: serverGeneratedBase64UrlChallenge, // Cryptographically random challenge
rp: { id: "example.com", name: "Secure Application" },
user: {
id: userBase64UrlId, // Unique base64url string identifying the account
name: "user@example.com",
displayName: "Jane Doe",
},
pubKeyCredParams: [
{
type: "public-key",
alg: -7,
},
{
type: "public-key",
alg: -257,
},
],
excludeCredentials: userExistingCredentials.map((cred) => ({
type: "public-key",
id: cred.id,
transports: cred.transports,
})),
authenticatorSelection: {
residentKey: "required",
requireResidentKey: true,
userVerification: "preferred",
...(isPromotionFlow && { authenticatorAttachment: "platform" }),
},
};
Validation
- Validation du challenge : validez de manière sécurisée le challenge par rapport au challenge lié à la session attendu.
- Vérifier la présence de l'utilisateur :
- Assurez-vous que l'indicateur de présence de l'utilisateur (UP) renvoyé dans les données d'authentificateur analysées est défini sur
truepour confirmer la présence physique de l'utilisateur au moment de la création.
- Assurez-vous que l'indicateur de présence de l'utilisateur (UP) renvoyé dans les données d'authentificateur analysées est défini sur
- Assouplissement de la validation pour "preferred" :
- Lorsque les options de création spécifient
userVerification: "preferred", l'appel de validation côté serveur DOIT être configuré avecrequireUserVerification: false. Sinon, les authentificateurs qui s'enregistrent sans validation de l'utilisateur (par exemple, lorsque le verrouillage de l'écran est désactivé) déclencheront des échecs de validation du serveur erronés.
- Lorsque les options de création spécifient
Logique côté client
- Limiter l'accès à l'UI au chargement de la page :
- Lors du chargement de la page, appelez
PublicKeyCredential.getClientCapabilities()et désactivez le bouton "Créer une clé d'accès" siconditionalGetoupasskeyPlatformAuthenticatorn'est pas disponible.
- Lors du chargement de la page, appelez
- Appeler la création et sérialiser : décodez les options du serveur avec
PublicKeyCredential.parseCreationOptionsFromJSON()et transmettez la configuration obtenue ànavigator.credentials.create().- Appelez
credential.toJSON()pour encoderAuthenticatorAttestationResponsedans un objet JSON sérialisable valide avant de récupérer le point de terminaison de validation.
- Appelez
- Gérer les exceptions WebAuthn :
InvalidStateError: une clé d'accès correspondante existe déjà (correspondance parexcludeCredentials).NotAllowedError: l'utilisateur a annulé la boîte de dialogue de la clé d'accès d'authentification ou le délai d'expiration a été dépassé.AbortError: l'opération a été annulée.SecurityError: erreurs d'origine sécurisée (HTTPS) ou de non-concordance de l'ID de la partie de confiance (problèmes de configuration).
- Séparation des blocs try/catch pour l'API Signal :
- Encapsulez l'appel de validation du serveur
fetch()dans un bloc try/catch. AppelezsignalUnknownCredential()lorsque la récupération de la validation du serveur échoue (tout étatresponse.ok === falseou exception réseau).
- Encapsulez l'appel de validation du serveur
// optionsFetch and registerVerifyFetch are app-defined HTTP methods
import { optionsFetch, registerVerifyFetch } from "./api.js";
async function registerPasskey(isPromotion = false) {
// Verify passkey capability and conditional UI are available
const capabilities = await PublicKeyCredential.getClientCapabilities();
if (
!capabilities.passkeyPlatformAuthenticator ||
!capabilities.conditionalGet
) {
// Hide "Create passkey" buttons and fall back to password flows instead
showStandardPasswordFallbackUI();
return;
}
const creationOptionsJSON = await optionsFetch({ promotion: isPromotion });
const publicKey =
PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);
let credential;
try {
// passkey prompt execution
credential = await navigator.credentials.create({ publicKey });
} catch (err) {
if (err.name === "InvalidStateError") {
console.log("A passkey already exists for this account.");
alert("A passkey already exists for this account.");
} else if (err.name === "SecurityError") {
console.error("Configuration RP ID or Secure Context error.");
alert("Configuration RP ID or Secure Context error.");
} else if (err.name === "NotAllowedError") {
console.log("User cancelled the passkey dialog.");
} else if (err.name === "AbortError") {
console.log("The creation operation has been aborted.");
}
return; // Safe API exit, do not signal unknown for standard WebAuthn cancels
}
// Server Verification phase (Segregated Try/Catch)
let encodedResponse = credential.toJSON();
try {
const response = await registerVerifyFetch(encodedResponse);
if (!response.ok) {
// Server verification failed to verify/authenticate the credential (orphaned)
await PublicKeyCredential.signalUnknownCredential({
rpId, // RP ID must match the one defined on the server
credentialId: encodedResponse.id, // Base64URL-encoded credential ID
});
}
} catch (serverErr) {
console.error("Server verification network failure:", serverErr);
await publickeycredential.signalunknowncredential({
rpId, // RP ID must match the one defined on the server
credentialid: encodedresponse.id, // base64url-encoded credential id
});
}
}
Stratégies de remplacement
Solution de secours pour la synchronisation de l'API Signal
L'API WebAuthn Signal (webauthn-signals) est une optimisation progressive utilisée pour synchroniser les gestionnaires de mots de passe avec l'état des identifiants du serveur.
- Expérience de remplacement : si
PublicKeyCredential.signalUnknownCredentialn'est pas compatible avec le navigateur, l'appel DOIT être contourné de manière sécurisée via un contrôle de détection des fonctionnalités (if (PublicKeyCredential.signalUnknownCredential)), et la validation côté serveur consigne simplement l'échec sans déclencher de mises à jour du gestionnaire.
Easy JSON Serialization Fallback
Installez toujours "webauthn-polyfills" et importez-le dans le contexte.
Tant que PublicKeyCredential est compatible, PublicKeyCredential.parseCreationOptionsFromJSON et PublicKeyCredential.prototype.toJSON le sont également.
import 'webauthn-polyfills';