Authentification par clé d'accès

Ce guide explique comment implémenter l'authentification des utilisateurs connus à l'aide d'identifiants détectables, à la fois via des déclencheurs de boutons explicites et des suggestions de saisie automatique du navigateur (UI conditionnelle).

Côté serveur

Génération d'options

Créez un point de terminaison qui génère des paramètres de requête WebAuthn à l'aide d'une bibliothèque validée conformément aux normes.

  1. Utiliser l'ID RP prédéfini : utilisez l'ID RP approprié prédéfini comme chaîne constante.
  2. Générer un test : générez un tampon aléatoire à entropie élevée et sécurisé de manière cryptographique, stockez-le de manière sécurisée dans la session de l'utilisateur et encodez-le au format Base64URL.
  3. Mappage des identifiants détectables : spécifiez un tableau vide [] pour allowCredentials. Cela demande des identifiants détectables, ce qui signifie que l'utilisateur n'a pas besoin de saisir son nom d'utilisateur en premier. Le fournisseur de clés d'accès présentera les comptes disponibles.
  4. Niveau de validation de l'utilisateur : définissez userVerification: "preferred" (ou "required" si les règles de conformité de l'entreprise l'exigent explicitement).
    • Le niveau de contrainte userVerification demandé DOIT être conservé dans l'enregistrement de session du serveur au niveau du point de terminaison des options, plutôt que d'être renvoyé par le client via des chaînes de requête. Cela permet au point de terminaison de validation d'appliquer des contraintes de correspondance strictes de manière sécurisée, sans risque de manipulation du client.
// Options generation example (discoverable flow)
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
  rpId: "example.com",
  allowCredentials: [], // Request discoverable passkeys
  userVerification: "preferred",
};

// Persist expected UV level to user session
req.session.expectedUserVerification = "preferred";

Point de terminaison de validation

Vérifiez de manière sécurisée l'assertion renvoyée par le client pour authentifier l'utilisateur :

  1. Valider le test de session : appliquez une correspondance stricte entre la réponse du client et le test attendu stocké dans la session.
  2. Appliquer les préférences de vérification de l'utilisateur :
    • Autorisez les authentificateurs sans vérification de l'utilisateur (par exemple, les verrouillages d'écran de l'authentificateur désactivés) si la session expectedUserVerification a demandé "preferred", en transmettant requireUserVerification: false à votre bibliothèque de validation côté serveur. Si vous y êtes invité "required", appliquez strictement la saisie des données biométriques/du code.
  3. Erreur du serveur 404 propre : si l'ID d'identifiant renvoyé par le client est introuvable dans la base de données, renvoyez une erreur HTTP 404 explicite afin que le client puisse déclencher l'API Signal.

Logique côté client

Annotation de formulaire HTML

Annotez vos entrées de nom d'utilisateur et de mot de passe pour exploiter l'UI conditionnelle de manière native. Les jetons de saisie semi-automatique combinent les paramètres de la spécification WebAuthn, et la mise au point automatique déclenche immédiatement le pop-up de saisie automatique du navigateur lorsque l'entrée est sélectionnée.

<!-- Autocomplete tokens must contain webauthn space-separated -->
<form id="signin-form">
  <input
    type="text"
    name="username"
    autocomplete="username webauthn"
    autofocus
    data-testid="username-field"
  />
  <input type="password" name="password" autocomplete="current-password" />
  <button type="submit">Sign in</button>
</form>

Flux de boutons explicite

Déclenchez l'authentification par clé d'accès lorsqu'un utilisateur clique sur un bouton "Se connecter avec une clé d'accès". Annulez tous les appels de saisie automatique de formulaire (Conditional Get) en cours avant d'appeler l'invite de clé d'accès.

Flux de médiation conditionnelle (saisie automatique de formulaire)

Activez les suggestions de saisie automatique de formulaire au chargement de la page pour proposer l'authentification par clé d'accès de manière native lorsque les utilisateurs sélectionnent les champs de connexion :

  1. Détection des fonctionnalités : appelez PublicKeyCredential.getClientCapabilities() au chargement de page et ignorez la connexion avec une clé d'accès si conditionalGet n'est pas disponible.
  2. Options de décodage : décodez l'objet JSON d'identifiant récupéré avec PublicKeyCredential.parseRequestOptionsFromJSON().
  3. Appeler une requête GET conditionnelle : appelez navigator.credentials.get() avec mediation: "conditional" et transmettez un signal AbortController. Cela enregistre la saisie automatique de manière silencieuse sans afficher de boîte de dialogue de clé d'accès.
  4. Ségrégation des exceptions Try/Catch : encapsulez l'appel navigator.credentials.get dans un bloc try/catch :
    • NotAllowedError : l'utilisateur a annulé l'invite de connexion par clé d'accès ou le délai d'expiration a été dépassé.
    • AbortError : la requête d'authentification a été annulée de manière programmatique.
  5. API Call Signal : encapsulez l'appel de vérification du serveur fetch() dans un bloc try/catch :
    • Affichez un message d'erreur pour que l'utilisateur comprenne ce qui s'est passé.
    • N'appelez signalUnknownCredential() QUE lorsque le serveur répond explicitement avec l'état HTTP 404 (Identifiant introuvable) et que l'utilisateur n'est pas authentifié.
    • Le paramètre credentialId transmis à signalUnknownCredential() DOIT être strictement la chaîne d'ID d'identifiant encodée en Base64URL (par exemple, encoded.id), et NON l'objet ArrayBuffer brut credential.rawId.
  6. Encoder la réponse : encodez l'identifiant AuthenticatorAssertionResponse avec .toJSON() avant de l'envoyer au serveur pour validation.
// optionsFetch and loginVerifyFetch are app-defined HTTP methods
import { optionsFetch, loginVerifyFetch } from "./api.js";

let autofillAbortController = new AbortController();

async function initializeConditionalAutofill() {
  // Feature detect Conditional Get autofill support
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (capabilities.conditionalGet === true) {
    const loginOptionsJSON = await optionsFetch();
    const publicKey =
      PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

    try {
      // Initiate Conditional UI form autofill suggestions
      const credential = await navigator.credentials.get({
        publicKey,
        signal: autofillAbortController.signal,
        mediation: "conditional",
      });

      // Segregated verification fetch
      const encoded = credential.toJSON();
      const response = await loginVerifyFetch(encoded);
      if (!response.ok && response.status === 404) {
        // Note: this code path runs pre-authentication, satisfying the unauth precondition
        if (PublicKeyCredential.signalUnknownCredential) {
          await PublicKeyCredential.signalUnknownCredential({
            rpId, // RP ID must match the one defined on the server
            credentialId: encoded.id,
          });
        }
      }
    } catch (err) {
      // Silently swallow expected client WebAuthn exceptions
      if (["NotAllowedError", "AbortError"].includes(err.name)) {
        return;
      }
      console.error("Unexpected conditional get error:", err);
    }
  }
}

async function triggerButtonAuthentication() {
  // Abort any pending Conditional Get call to prevent passkey prompt collisions
  autofillAbortController.abort();
  autofillAbortController = new AbortController(); // Reset controller for next triggers

  const loginOptionsJSON = await optionsFetch();
  const publicKey =
    PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

  let credential;
  try {
    // Passkey explicit prompt trigger
    credential = await navigator.credentials.get({
      publicKey,
      signal: autofillAbortController.signal,
    });
  } catch (err) {
    if (err.name === "NotAllowedError") {
      console.log("User cancelled passkey login.");
    } else if (err.name === "AbortError") {
      console.log("The authentication operation was aborted.");
    }
    // Re-arm Conditional autofill Suggestions after cancelled explicit button prompts
    initializeConditionalAutofill();
    return; // Safe exit
  }

  // Segregated verification try/catch (HTTP 404 trigger)
  const encoded = credential.toJSON();
  try {
    const response = await loginVerifyFetch(encoded);
    if (!response.ok && response.status === 404) {
      // Note: this code path runs pre-authentication, satisfying the unauth precondition
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encoded.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Verification request error:", serverErr);
  }
}

// Trigger Conditional Get on load
window.addEventListener("DOMContentLoaded", initializeConditionalAutofill);

Stratégies de remplacement

Solution de secours pour la détection des fonctionnalités de clé d'accès

Installez toujours "webauthn-polyfills" et importez-le dans le contexte. Tant que PublicKeyCredential est compatible, PublicKeyCredential.getClientCapabilities l'est également.

import 'webauthn-polyfills';

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 : accès limité via if (PublicKeyCredential.signalUnknownCredential). Si elle n'est pas compatible, la synchronisation de la validation en arrière-plan est ignorée sans générer d'exceptions de navigateur.

Easy JSON Serialization Fallback

Installez toujours "webauthn-polyfills" et importez-le dans le contexte. Tant que PublicKeyCredential est compatible, PublicKeyCredential.parseRequestOptionsFromJSON et PublicKeyCredential.prototype.toJSON le sont également.

import 'webauthn-polyfills';