Connexions rapides avec le mode d'interface utilisateur immédiat

Rediriger un utilisateur non authentifié vers une page de connexion dédiée lorsqu'il clique sur un lien "Se connecter", commence à payer, enregistre un article dans ses favoris ou atteint un paywall d'article entraîne une perte de contexte et augmente le taux d'abandon. Le mode UI immédiat (uiMode: 'immediate' dans navigator.credentials.get()) permet des connexions rapides contextuelles en présentant immédiatement un sélecteur de compte de navigateur unifié lorsque des clés d'accès locales (publicKey) ou des mots de passe enregistrés (password: true) existent sur l'appareil.

Contrairement aux invites WebAuthn modales standards, qui affichent toujours l'UI du navigateur et se rabattent sur des QR codes multi-appareils ou des clés de sécurité externes, le mode UI immédiate échoue rapidement avec une erreur NotAllowedError si aucun identifiant éligible n'est disponible localement sur l'appareil. Votre application peut ainsi authentifier les utilisateurs connus d'un simple geste, tout en redirigeant de manière fluide les utilisateurs sans identifiants locaux vers votre page de connexion standard, votre page de paiement sans connexion ou votre formulaire de connexion intégré.

Déclenchez le mode UI immédiate dans un gestionnaire d'événements initié par l'utilisateur (nécessitant une activation temporaire de l'utilisateur) lorsque l'intention de l'utilisateur est claire, juste avant qu'il ne quitte la page actuelle. Si l'authentification immédiate réussit, répondez immédiatement à l'intention de l'utilisateur (en mettant à jour la page actuelle ou en accédant directement à sa destination authentifiée). Si elle échoue avec NotAllowedError, affichez un flux de connexion ou d'inscription avec un chemin d'accès pour répondre à l'intention de l'utilisateur une fois l'opération terminée.

Cas d'utilisation possibles :

  • Interception du lien ou du bouton de connexion dans l'en-tête : interceptez un clic sur un lien "Se connecter" global (event.preventDefault()) pour authentifier immédiatement les utilisateurs connus ou continuez à accéder à la page de connexion dédiée sur NotAllowedError.
  • Connexion dynamique avant le paiement : authentifiez les clients connus d'un simple geste lorsqu'ils cliquent sur "Passer au paiement" pour qu'ils accèdent directement à l'étape de livraison et de paiement, ou passez à l'écran de connexion standard ou de paiement sans connexion en cas d'échec.
  • Connexion intégrée pour les micro-actions (Favori, J'aime, Ajouter aux favoris, Enregistrer) : authentifiez l'utilisateur sur place lorsqu'un utilisateur déconnecté clique sur un bouton de micro-action et effectuez immédiatement l'action (par exemple, en activant aria-pressed="true") ou ouvrez une fenêtre modale de connexion de remplacement ou une page de connexion avec un paramètre d'URL de retour.
  • Paywalls souples dans les articles : authentifiez les lecteurs directement lorsqu'ils cliquent sur "Continuer la lecture avec votre compte" sur une bannière de paywall, ou développez un formulaire de connexion ou d'inscription intégré en cas d'échec.

Anti-modèles à éviter :

  • N'utilisez PAS le mode UI immédiat sur une page de connexion dédiée : n'associez jamais uiMode: 'immediate' au bouton principal "Se connecter" ou "Se connecter avec une clé d'accès" sur votre page de connexion de destination finale. Les utilisateurs accèdent à la page de connexion dédiée spécifiquement lorsqu'ils n'ont pas d'identifiants locaux sur l'appareil actuel, ou lorsqu'ils ont l'intention d'utiliser une clé d'accès multi-appareil (en scannant un QR code avec un téléphone) ou une clé de sécurité matérielle externe. Étant donné que uiMode: 'immediate' supprime les options de QR code et de clé de sécurité multi-appareils, et génère immédiatement NotAllowedError lorsqu'il n'existe aucun identifiant local, l'utiliser sur une page de connexion dédiée piège l'utilisateur dans une impasse où cliquer sur le bouton ne produit aucun effet. Sur une page de connexion dédiée, utilisez l'UI conditionnelle (mediation: 'conditional' avec autocomplete="username webauthn") sur les entrées de formulaire associées à un bouton modal navigator.credentials.get({ publicKey }) standard (voir Authentification par clé d'accès).
  • N'utilisez PAS le mode UI immédiat pour la réauthentification renforcée : n'utilisez jamais uiMode: 'immediate' pour réauthentifier un utilisateur déjà connecté avant des opérations sensibles (comme la modification d'un mot de passe, la mise à jour d'une adresse de livraison, la modification des modes de paiement enregistrés ou l'ajout d'une nouvelle clé d'accès). La réauthentification doit limiter l'invite aux identifiants de l'utilisateur connecté via une liste allowCredentials non vide, que le mode UI immédiat rejette avec NotAllowedError. L'omission de allowCredentials permettrait à l'utilisateur de sélectionner par inadvertance un compte différent de celui actuellement connecté. Pour la réauthentification renforcée, suivez les instructions de la section Réauthentification par clé d'accès.

Contraintes de confidentialité et de sécurité

Les navigateurs appliquent des règles de confidentialité et de sécurité strictes lorsque uiMode: 'immediate' est demandé :

  • L'activation temporaire de l'utilisateur est requise : vous DEVEZ appeler navigator.credentials.get({ uiMode: 'immediate' }) dans un gestionnaire d'événements initié par l'utilisateur (comme un écouteur click sur un élément <button> ou <a>) pour empêcher l'analyse silencieuse des identifiants. Ne l'appelez jamais automatiquement au chargement de page (DOMContentLoaded). L'appel de l'API ne consomme pas l'activation temporaire.
  • Ne transmettez pas de liste allowCredentials non vide : ne renseignez PAS publicKey.allowCredentials lorsque uiMode: 'immediate' est spécifié. Les navigateurs rejettent immédiatement toute requête en mode UI immédiat avec une liste allowCredentials non vide en générant une erreur NotAllowedError. Ainsi, les tiers de confiance ne peuvent pas rechercher des ID d'identifiants spécifiques pour déduire des visites antérieures ou suivre les utilisateurs d'une session à l'autre.
  • Ne transmettez pas de AbortSignal (signal) : ne définissez PAS la propriété signal sur navigator.credentials.get() lorsque uiMode: 'immediate' est spécifié, car les navigateurs n'autorisent pas la fermeture programmatique de la boîte de dialogue de connexion immédiate. Si votre page exécute également une requête d'UI conditionnelle (mediation: 'conditional') en arrière-plan, appelez abort() sur cette UI conditionnelle AbortController avant d'appeler le mode UI immédiat, et omettez signal de la requête immédiate elle-même.
  • Sessions de navigation privée : les requêtes effectuées dans des fenêtres de navigation privée sont toujours rejetées avec NotAllowedError afin que les sessions privées ne puissent pas être distinguées des sessions sans identifiants locaux.

Configuration côté serveur

Point de terminaison de génération d'options

Exposez un point de terminaison qui génère des options de requête WebAuthn détectables pour les visiteurs non authentifiés :

  1. Générer un challenge lié à la session : créez un tampon de challenge cryptographiquement aléatoire, stockez-le dans la session du serveur et encodez-le en Base64URL.
  2. Conserver la préférence de vérification de l'utilisateur : définissez userVerification: 'preferred' et stockez expectedUserVerification: 'preferred' dans la session.
  3. Omettre allowCredentials : n'incluez pas de liste allowCredentials non vide dans les options du mode UI immédiat, car les navigateurs rejettent les listes d'autorisation non vides avec NotAllowedError.
// Server-side options generation example (/api/login/options)
router.post('/api/login/options', (req, res) => {
  const options = {
    challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
    rpId: 'example.com',
    userVerification: 'preferred',
  };

  req.session.challenge = options.challenge;
  req.session.expectedUserVerification = 'preferred';
  return res.json(options);
});

Points de terminaison de validation à double identifiant

Étant donné que navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' }) peut être résolu avec une clé d'accès (PublicKeyCredential) ou un mot de passe enregistré (PasswordCredential), fournissez des gestionnaires de validation pour les deux types d'identifiants :

  1. Validation de l'assertion WebAuthn (/api/login/verify) : validez l'assertion par rapport au test de session et à la clé publique stockée. Si l'ID d'identifiant est introuvable dans la base de données, renvoyez l'état HTTP 404 afin que le client puisse en informer le gestionnaire de mots de passe via PublicKeyCredential.signalUnknownCredential().
  2. Validation du mot de passe (/api/login/password) : validez le username (cred.id) et le password (cred.password) par rapport à la base de données utilisateur et établissez une session authentifiée.

Implémentation côté client

Demande d'identifiants unifiée

  1. Demander à la fois des clés d'accès et des mots de passe : décodez les options du serveur à l'aide de PublicKeyCredential.parseRequestOptionsFromJSON() et appelez navigator.credentials.get() dans un gestionnaire de clics initié par l'utilisateur avec password: true, publicKey et uiMode: 'immediate' (sans transmettre signal).
  2. Validation de la succursale par cred.type :
    • Si cred.type === 'public-key', sérialisez l'assertion avec cred.toJSON() et envoyez-la à votre point de terminaison de validation WebAuthn à l'aide d'une requête POST. Lorsque le serveur répond avec le code HTTP 404, appelez PublicKeyCredential.signalUnknownCredential({ rpId: publicKey.rpId, credentialId: encoded.id }).
    • Si la valeur est cred.type === 'password', envoyez une requête POST { username: cred.id, password: cred.password } à votre point de terminaison de validation du mot de passe.
  3. Gérer NotAllowedError lorsqu'il n'existe pas d'identifiants locaux : interceptez NotAllowedError (généré immédiatement lorsqu'il n'existe pas de clés d'accès locales ni de mots de passe enregistrés sur l'appareil, ou lorsque l'utilisateur ferme le sélecteur) et passez à votre parcours de connexion standard (par exemple, en ouvrant une boîte de dialogue de connexion intégrée ou en accédant à /signin avec une URL de retour).
let isAuthenticating = false;

async function authenticateWithImmediateUi() {
  const optionsResponse = await fetch('/api/login/options', { method: 'POST' });
  if (!optionsResponse.ok) {
    throw new Error('Failed to fetch authentication options');
  }
  const optionsJSON = await optionsResponse.json();
  const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJSON);

  // MANDATORY: Request both password and publicKey with uiMode: 'immediate' and NO signal parameter
  const cred = await navigator.credentials.get({
    password: true,
    publicKey,
    uiMode: 'immediate',
  });

  if (!cred) {
    throw new DOMException('No credential returned', 'NotAllowedError');
  }

  if (cred.type === 'public-key') {
    const encoded = cred.toJSON();
    const verifyResponse = await fetch('/api/login/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(encoded),
    });

    if (verifyResponse.status === 404) {
      await PublicKeyCredential.signalUnknownCredential({
        rpId: publicKey.rpId || window.location.hostname,
        credentialId: encoded.id, // Base64URL-encoded credential ID string
      });
      throw new Error('Passkey credential is no longer recognized by the server');
    }

    if (!verifyResponse.ok) {
      throw new Error('Passkey verification failed');
    }
    return await verifyResponse.json();
  }

  if (cred.type === 'password') {
    const verifyResponse = await fetch('/api/login/password', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        username: cred.id,
        password: cred.password,
      }),
    });

    if (!verifyResponse.ok) {
      throw new Error('Password verification failed');
    }
    return await verifyResponse.json();
  }

  throw new Error(`Unsupported credential type: ${cred.type}`);
}

// Example: Intercepting an inline micro-action (Favorite button) or Header Sign-in click
const favoriteButton = document.getElementById('favorite-btn');

favoriteButton.addEventListener('click', async (event) => {
  event.preventDefault();
  if (isAuthenticating) return;

  // If already signed in, toggle the micro-action state immediately
  if (document.body.dataset.signedIn === 'true') {
    const nextPressed = favoriteButton.getAttribute('aria-pressed') !== 'true';
    favoriteButton.setAttribute('aria-pressed', String(nextPressed));
    return;
  }

  isAuthenticating = true;
  try {
    const user = await authenticateWithImmediateUi();
    // Complete the user's intended action immediately
    document.body.dataset.signedIn = 'true';
    favoriteButton.setAttribute('aria-pressed', 'true');
    updateHeaderUserState(user);
  } catch (err) {
    if (err.name === 'NotAllowedError') {
      // No local credentials available or user dismissed dialog:
      // transition smoothly to fallback sign-in UI or redirect with return URL
      showFallbackSignInUi({ returnAction: 'favorite' });
      return;
    }
    console.error('Authentication error:', err);
  } finally {
    isAuthenticating = false;
  }
});

Stratégies de remplacement

Création de remplacement immédiate en mode UI

  • Expérience d'amélioration progressive : traitez uiMode: 'immediate' comme une amélioration progressive. Ne déduisez pas la compatibilité à partir du nom ou de la version d'un navigateur. Le seul signal de compatibilité fiable est la vérification de l'environnement d'exécution capabilities.immediateGet via PublicKeyCredential.getClientCapabilities(). OBLIGATOIRE : Vérifiez toujours capabilities.immediateGet avant d'appeler navigator.credentials.get({ uiMode: 'immediate' }). Les navigateurs qui ne reconnaissent pas uiMode: 'immediate' ignorent la clé de dictionnaire inconnue et ouvrent une boîte de dialogue modale bloquante pour la clé d'accès et le QR code. Lorsque capabilities.immediateGet est faux ou que navigator.credentials.get() rejette avec NotAllowedError, revenez à l'expérience de connexion standard de votre application (par exemple, en ouvrant un modal de connexion intégré ou en accédant à la page de connexion dédiée avec un paramètre d'URL de retour), afin que les utilisateurs de navigateurs non compatibles voient votre flux de connexion existant inchangé :
async function isImmediateUiSupported() {
  if (!window.PublicKeyCredential || !PublicKeyCredential.getClientCapabilities) {
    return false;
  }
  try {
    const capabilities = await PublicKeyCredential.getClientCapabilities();
    return Boolean(capabilities?.immediateGet);
  } catch {
    return false;
  }
}

async function authenticateWithImmediateUi() {
  // MANDATORY: Check immediateGet support so unsupported browsers do not open a modal QR dialog
  const supported = await isImmediateUiSupported();
  if (!supported) {
    // Throw NotAllowedError so the caller's single catch path treats "unsupported" like "no local credentials"
    throw new DOMException('Immediate UI mode is not supported', 'NotAllowedError');
  }
  // Proceed with fetching options and calling navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' })
}

Solution de remplacement pour la détection des fonctionnalités client

Installez toujours 'webauthn-polyfills' et importez-le dans votre module client afin que PublicKeyCredential.getClientCapabilities() soit disponible chaque fois que PublicKeyCredential est compatible :

import 'webauthn-polyfills';

Utiliser les identifiants de mot de passe en cas d'échec

Transmettre password: true avec publicKey dans navigator.credentials.get() permet aux navigateurs compatibles d'inclure les mots de passe enregistrés dans le gestionnaire de mots de passe du navigateur dans le sélecteur de compte immédiat unifié.

  • Expérience de remplacement : les navigateurs qui ne sont pas compatibles avec PasswordCredential ignorent l'option password: true ou reviennent au formulaire de connexion standard via la vérification de la fonctionnalité capabilities.immediateGet, où <input autocomplete="username webauthn"> et <input type="password" autocomplete="current-password"> fournissent la saisie automatique native des mots de passe.

Solution de secours simple pour la sérialisation JSON

Installez toujours 'webauthn-polyfills' et importez-le dans le bundle client afin que PublicKeyCredential.parseRequestOptionsFromJSON() et PublicKeyCredential.prototype.toJSON() fonctionnent de manière cohérente sur les navigateurs compatibles avec PublicKeyCredential :

import 'webauthn-polyfills';

Solution de secours pour la synchronisation de l'API Signal

L'API WebAuthn Signal (webauthn-signals) est une amélioration progressive qui supprime les clés d'accès obsolètes du gestionnaire de mots de passe de l'utilisateur lorsque le serveur répond avec le code HTTP 404.

  • Expérience de remplacement : les appels sont bloqués derrière if (PublicKeyCredential.signalUnknownCredential). Si elle n'est pas prise en charge, ignorez l'appel de signal de manière appropriée et affichez un message clair invitant l'utilisateur à se connecter avec une autre méthode.