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é.
Modèles et antimodèles d'expérience utilisateur recommandés
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 surNotAllowedError. - 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é queuiMode: 'immediate'supprime les options de QR code et de clé de sécurité multi-appareils, et génère immédiatementNotAllowedErrorlorsqu'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'avecautocomplete="username webauthn") sur les entrées de formulaire associées à un bouton modalnavigator.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 listeallowCredentialsnon vide, que le mode UI immédiat rejette avecNotAllowedError. L'omission deallowCredentialspermettrait à 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 écouteurclicksur 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
allowCredentialsnon vide : ne renseignez PASpublicKey.allowCredentialslorsqueuiMode: 'immediate'est spécifié. Les navigateurs rejettent immédiatement toute requête en mode UI immédiat avec une listeallowCredentialsnon vide en générant une erreurNotAllowedError. 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ésignalsurnavigator.credentials.get()lorsqueuiMode: '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, appelezabort()sur cette UI conditionnelleAbortControlleravant d'appeler le mode UI immédiat, et omettezsignalde 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
NotAllowedErrorafin 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 :
- 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.
- Conserver la préférence de vérification de l'utilisateur : définissez
userVerification: 'preferred'et stockezexpectedUserVerification: 'preferred'dans la session. - Omettre
allowCredentials: n'incluez pas de listeallowCredentialsnon vide dans les options du mode UI immédiat, car les navigateurs rejettent les listes d'autorisation non vides avecNotAllowedError.
// 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 :
- 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 HTTP404afin que le client puisse en informer le gestionnaire de mots de passe viaPublicKeyCredential.signalUnknownCredential(). - Validation du mot de passe (
/api/login/password) : validez leusername(cred.id) et lepassword(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
- 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 appeleznavigator.credentials.get()dans un gestionnaire de clics initié par l'utilisateur avecpassword: true,publicKeyetuiMode: 'immediate'(sans transmettresignal). - Validation de la succursale par
cred.type:- Si
cred.type === 'public-key', sérialisez l'assertion aveccred.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 HTTP404, appelezPublicKeyCredential.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.
- Si
- Gérer
NotAllowedErrorlorsqu'il n'existe pas d'identifiants locaux : interceptezNotAllowedError(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 à/signinavec 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écutioncapabilities.immediateGetviaPublicKeyCredential.getClientCapabilities(). OBLIGATOIRE : Vérifiez toujourscapabilities.immediateGetavant d'appelernavigator.credentials.get({ uiMode: 'immediate' }). Les navigateurs qui ne reconnaissent pasuiMode: '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. Lorsquecapabilities.immediateGetest faux ou quenavigator.credentials.get()rejette avecNotAllowedError, 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
PasswordCredentialignorent l'optionpassword: trueou 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.