Als je een niet-geverifieerde gebruiker naar een speciale inlogpagina stuurt wanneer die op een link Inloggen klikt, begint met afrekenen, een item opslaat in favorieten of een betaalmuur voor een artikel bereikt, gaat de context verloren en haken gebruikers vaker af. De modus Onmiddellijke UI (uiMode: 'immediate' in navigator.credentials.get()) maakt contextueel snel inloggen mogelijk door onmiddellijk een uniforme browseraccountkiezer te tonen als er lokale toegangssleutels (publicKey) of opgeslagen wachtwoorden (password: true) op het apparaat staan.
In tegenstelling tot standaard modale WebAuthn-prompts, die altijd de browser-UI tonen en terugvallen op QR-codes voor meerdere apparaten of externe beveiligingssleutels, mislukt de modus Onmiddellijke UI snel met een NotAllowedError als er geen in aanmerking komende inloggegevens lokaal beschikbaar zijn op het apparaat. Hiermee kan je app terugkerende gebruikers ter plekke verifiëren met één tik, terwijl gebruikers zonder lokale inloggegevens naadloos worden overgezet naar je standaard inlogpagina, betalen zonder account of inline inlogformulier.
Aanbevolen UX-patronen en anti-patronen
Activeer de modus Onmiddellijke UI in een door de gebruiker gestarte gebeurtenishandler (waarvoor tijdelijke gebruikersactivering is vereist) op momenten van duidelijke gebruikersintentie vlak voordat de gebruiker de huidige pagina verlaat. Als de onmiddellijke verificatie lukt, voer je de intentie van de gebruiker onmiddellijk uit (door de huidige pagina ter plekke te updaten of rechtstreeks naar de geverifieerde bestemming te gaan). Als dit mislukt met NotAllowedError, toon je een inlog- of registratieproces met een pad om de intentie van de gebruiker af te ronden.
Mogelijke toepassingen:
- Onderschepping van de inloglink of -knop in de kop: Onderschep een klik op een algemene inloglink (
event.preventDefault()) om terugkerende gebruikers onmiddellijk te verifiëren of ga verder naar de speciale inlogpagina opNotAllowedError. - Dynamisch inloggen vóór betalen: Verifieer terugkerende kopers met één tik als ze op Doorgaan naar betalen klikken, zodat ze rechtstreeks op de verzend- en betaalstap terechtkomen, of ga over naar het standaard inlogscherm of het scherm voor betalen zonder account als terugvaloptie.
- Inline inloggen voor micro-acties (Favoriet, Like, Bookmark, Opslaan): Verifieer ter plekke wanneer een uitgelogde gebruiker op een knop voor een micro-actie klikt en voer de actie onmiddellijk uit (bijvoorbeeld
aria-pressed="true"aan- of uitzetten) of open een reserve inlogvenster of inlogpagina met een URL-parameter voor de retour-URL. - Zachte betaalmuren in artikelen: Verifieer lezers ter plekke als ze op 'Verder lezen met je account' klikken op een betaalmuurbanner of vouw een inline inlog- of registratieformulier uit als terugvaloptie.
Anti-patronen die je moet vermijden:
- Gebruik de modus Onmiddellijke UI NIET op een speciale inlogpagina: Koppel
uiMode: 'immediate'nooit aan de primaire knop Inloggen of Inloggen met een toegangssleutel op de inlogpagina van je uiteindelijke bestemming. Gebruikers komen op de speciale inlogpagina als ze geen lokale inloggegevens hebben op het huidige apparaat of als ze een toegangssleutel voor meerdere apparaten willen gebruiken (een QR-code scannen met een telefoon) of een externe fysieke beveiligingssleutel. OmdatuiMode: 'immediate'opties voor QR-codes en beveiligingssleutels voor meerdere apparaten onderdrukt en onmiddellijkNotAllowedErrorgenereert als er geen lokale inloggegevens bestaan, komt de gebruiker op een speciale inlogpagina vast te zitten als deze op de knop klikt. Gebruik op een speciale inlogpagina de voorwaardelijke UI (mediation: 'conditional'metautocomplete="username webauthn") voor formulierinvoer in combinatie met een standaard modalenavigator.credentials.get({ publicKey })-knop (zie Verificatie met toegangssleutel). - Gebruik de modus Onmiddellijke UI NIET voor hernieuwde verificatie voor een hoger beveiligingsniveau: Gebruik
uiMode: 'immediate'nooit om een al ingelogde gebruiker opnieuw te laten verifiëren vóór gevoelige bewerkingen (zoals een wachtwoord wijzigen, een verzendadres updaten, opgeslagen betaalmethoden bewerken of een nieuwe toegangssleutel toevoegen). Opnieuw verifiëren moet de prompt beperken tot de inloggegevens van de ingelogde gebruiker via een niet-legeallowCredentials-lijst, die de modus Onmiddellijke UI weigert metNotAllowedError. Als jeallowCredentialsweglaat, kan de gebruiker onbedoeld een ander account selecteren dan het account waarmee die op dat moment is ingelogd. Volg Opnieuw verifiëren met toegangssleutel voor herverificatie op een hoger niveau.
Privacy- en beveiligingsbeperkingen
Browsers dwingen strenge privacy- en beveiligingsregels af als uiMode: 'immediate' wordt aangevraagd:
- Tijdelijke gebruikersactivering is vereist: Je MOET
navigator.credentials.get({ uiMode: 'immediate' })aanroepen in een door de gebruiker gestarte gebeurtenishandler (zoals eenclick-listener op een<button>- of<a>-element) om te voorkomen dat inloggegevens op de achtergrond worden gecontroleerd. Roep de API nooit automatisch aan als de pagina wordt geladen (DOMContentLoaded). Als je de API aanroept, wordt de tijdelijke activering niet verbruikt. - Geef geen niet-lege
allowCredentials-lijst door: VulpublicKey.allowCredentialsNIET in alsuiMode: 'immediate'is opgegeven. Browsers weigeren onmiddellijk elk verzoek voor de onmiddellijke UI-modus met een niet-legeallowCredentials-lijst doorNotAllowedErrorte genereren. Vertrouwende partijen kunnen dus niet zoeken naar specifieke inloggegevens-ID's om eerdere bezoeken af te leiden of gebruikers tussen sessies te volgen. - Geef geen
AbortSignal(signal) door: Stel de propertysignalNIET in opnavigator.credentials.get()alsuiMode: 'immediate'is opgegeven, omdat browsers het programmatisch sluiten van het dialoogvenster voor onmiddellijk inloggen niet toestaan. Als je pagina ook een voorwaardelijk UI-verzoek (mediation: 'conditional') op de achtergrond uitvoert, roep jeabort()aan voor die voorwaardelijke UIAbortControllervoordat je de modus Onmiddellijke UI aanroept en laat jesignalweg uit het onmiddellijke verzoek zelf. - Incognito- en privésessies: Verzoeken die worden gedaan in incognito- of privévensters, worden altijd geweigerd met
NotAllowedError, zodat privésessies niet te onderscheiden zijn van sessies zonder lokale inloggegevens.
Instellen aan de serverzijde
Eindpunt voor optiegeneratie
Maak een eindpunt beschikbaar dat vindbare WebAuthn-verzoekopties genereert voor niet-geverifieerde bezoekers:
- Genereer een sessiegebonden challenge: Maak een cryptografisch willekeurige challengebuffer, sla deze op in de serversessie en codeer deze als Base64URL.
- Voorkeur voor gebruikersverificatie behouden: Stel
userVerification: 'preferred'in en slaexpectedUserVerification: 'preferred'op in de sessie. - Weglaten
allowCredentials: Voeg geen niet-legeallowCredentials-lijst toe aan de opties voor de modus Onmiddellijke UI, omdat browsers niet-lege toelatingslijsten metNotAllowedErrorweigeren.
// 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);
});
Eindpunten voor verificatie met 2 sets inloggegevens
Omdat navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' }) kan worden opgelost met een toegangssleutel (PublicKeyCredential) of een opgeslagen wachtwoord (PasswordCredential), moet je verificatiehandlers voor beide typen inloggegevens opgeven:
- WebAuthn-bevestigingsverificatie (
/api/login/verify): Verifieer de bevestiging aan de hand van de sessie-challenge en de opgeslagen openbare sleutel. Als de inloggegevens-ID niet in de database wordt gevonden, retourneer je de HTTP-status404, zodat de client de wachtwoordmanager op de hoogte kan stellen viaPublicKeyCredential.signalUnknownCredential(). - Wachtwoordverificatie (
/api/login/password): Verifieer deusername(cred.id) enpassword(cred.password) aan de hand van de gebruikersdatabase en stel een geverifieerde sessie in.
Implementatie aan de clientzijde
Verzoek voor uniforme inloggegevens
- Zowel toegangssleutels als wachtwoorden aanvragen: Decodeer de serveropties met
PublicKeyCredential.parseRequestOptionsFromJSON()en roepnavigator.credentials.get()aan in een door de gebruiker gestarte klikhandler metpassword: true,publicKeyenuiMode: 'immediate'(zondersignaldoor te geven). - Filiaalverificatie door
cred.type:- Als
cred.type === 'public-key', serialiseer je de bewering metcred.toJSON()en POST je deze naar je WebAuthn-verificatie-eindpunt. Als de server reageert met HTTP404, roep jePublicKeyCredential.signalUnknownCredential({ rpId: publicKey.rpId, credentialId: encoded.id })aan. - Als
cred.type === 'password', POST{ username: cred.id, password: cred.password }naar je eindpunt voor wachtwoordverificatie.
- Als
- Verwerk
NotAllowedErrorals er geen lokale inloggegevens bestaan: VangNotAllowedErrorop (wordt onmiddellijk gegenereerd als er geen lokale toegangssleutels of opgeslagen wachtwoorden op het apparaat bestaan of als de gebruiker de kiezer sluit) en ga over naar je standaard inlogproces (zoals een inline inlogdialoogvenster openen of naar/signingaan met een retour-URL).
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;
}
});
Reservestrategieën
Onmiddellijke terugval naar UI-modus
- Progressive enhancement: Behandel
uiMode: 'immediate'als een progressive enhancement. Leid ondersteuning niet af uit de naam of versie van een browser. Het enige betrouwbare ondersteuningssignaal is decapabilities.immediateGet-runtimecheck viaPublicKeyCredential.getClientCapabilities(). VERPLICHT: Controleer altijdcapabilities.immediateGetvoordat jenavigator.credentials.get({ uiMode: 'immediate' })aanroept. Browsers dieuiMode: 'immediate'niet herkennen, negeren de onbekende woordenboeksleutel en openen een blokkerend modaal dialoogvenster voor toegangssleutels en QR-codes. Alscapabilities.immediateGetonwaar is of alsnavigator.credentials.get()wordt geweigerd metNotAllowedError, val je terug op de standaard inlogfunctionaliteit van je app (zoals een inline inlogvenster openen of naar de speciale inlogpagina gaan met een retour-URL-parameter), zodat gebruikers in niet-ondersteunde browsers je bestaande inlogproces ongewijzigd zien:
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' })
}
Terugvaloptie voor detectie van clientmogelijkheden
Installeer 'webauthn-polyfills' altijd en importeer deze in je clientmodule, zodat PublicKeyCredential.getClientCapabilities() beschikbaar is wanneer PublicKeyCredential wordt ondersteund:
import 'webauthn-polyfills';
Terugvaloptie voor wachtwoordgegevens
Als je password: true samen met publicKey doorgeeft in navigator.credentials.get(), kunnen ondersteunde browsers opgeslagen wachtwoorden uit de wachtwoordmanager van de browser opnemen in de uniforme directe accountkiezer.
- Fallback-functionaliteit: Browsers die
PasswordCredentialniet ondersteunen, negeren de optiepassword: trueof vallen via de functiecheckcapabilities.immediateGetterug op het standaard inlogformulier, waarbij<input autocomplete="username webauthn">en<input type="password" autocomplete="current-password">native automatisch wachtwoorden invullen.
Makkelijke JSON-serialisatiefallback
Installeer 'webauthn-polyfills' altijd en importeer het in de clientbundel, zodat PublicKeyCredential.parseRequestOptionsFromJSON() en PublicKeyCredential.prototype.toJSON() consistent werken in browsers die PublicKeyCredential ondersteunen:
import 'webauthn-polyfills';
Terugvaloptie voor Signal API-synchronisatie
De WebAuthn Signal API (webauthn-signals) is een progressive enhancement die verouderde toegangssleutels uit de wachtwoordmanager van de gebruiker verwijdert als de server reageert met HTTP 404.
- Reservefunctionaliteit: Gesprekken worden geblokkeerd door
if (PublicKeyCredential.signalUnknownCredential). Als de functie niet wordt ondersteund, sla je de signaaloproep over en toon je een duidelijke melding die de gebruiker aanraadt om in te loggen met een alternatieve methode.