Enregistrement d'une clé d'accès

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.

  1. Utiliser l'ID RP prédéfini : utilisez l'ID RP approprié prédéfini comme chaîne constante.
  2. 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.
  3. É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.
  4. Appliquer les identifiants détectables : définissez requireResidentKey: true et residentKey: "required" dans les options authenticatorSelection pour demander un identifiant détectable, ce qui est nécessaire pour les connexions détectables.
  5. Configurer la validation de l'utilisateur : spécifiez userVerification: "preferred" ou userVerification: "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.
  6. 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: boolean pour gérer de manière conditionnelle les deux flux avec un seul point de terminaison.
// 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

  1. Validation du challenge : validez de manière sécurisée le challenge par rapport au challenge lié à la session attendu.
  2. 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 true pour confirmer la présence physique de l'utilisateur au moment de la création.
  3. 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é avec requireUserVerification: 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.

Logique côté client

  1. 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" si conditionalGet ou passkeyPlatformAuthenticator n'est pas disponible.
  2. 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 encoder AuthenticatorAttestationResponse dans un objet JSON sérialisable valide avant de récupérer le point de terminaison de validation.
  3. Gérer les exceptions WebAuthn :
    • InvalidStateError : une clé d'accès correspondante existe déjà (correspondance par excludeCredentials).
    • 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).
  4. Séparation des blocs try/catch pour l'API Signal :
    • Encapsulez l'appel de validation du serveur fetch() dans un bloc try/catch. Appelez signalUnknownCredential() lorsque la récupération de la validation du serveur échoue (tout état response.ok === false ou exception réseau).
// 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.signalUnknownCredential n'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';