Snel inloggen met de directe UI-modus

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.

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 op NotAllowedError.
  • 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. Omdat uiMode: 'immediate' opties voor QR-codes en beveiligingssleutels voor meerdere apparaten onderdrukt en onmiddellijk NotAllowedError genereert 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' met autocomplete="username webauthn") voor formulierinvoer in combinatie met een standaard modale navigator.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-lege allowCredentials-lijst, die de modus Onmiddellijke UI weigert met NotAllowedError. Als je allowCredentials weglaat, 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 een click-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: Vul publicKey.allowCredentials NIET in als uiMode: 'immediate' is opgegeven. Browsers weigeren onmiddellijk elk verzoek voor de onmiddellijke UI-modus met een niet-lege allowCredentials-lijst door NotAllowedError te 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 property signal NIET in op navigator.credentials.get() als uiMode: '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 je abort() aan voor die voorwaardelijke UI AbortController voordat je de modus Onmiddellijke UI aanroept en laat je signal weg 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:

  1. Genereer een sessiegebonden challenge: Maak een cryptografisch willekeurige challengebuffer, sla deze op in de serversessie en codeer deze als Base64URL.
  2. Voorkeur voor gebruikersverificatie behouden: Stel userVerification: 'preferred' in en sla expectedUserVerification: 'preferred' op in de sessie.
  3. Weglaten allowCredentials: Voeg geen niet-lege allowCredentials-lijst toe aan de opties voor de modus Onmiddellijke UI, omdat browsers niet-lege toelatingslijsten met NotAllowedError weigeren.
// 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:

  1. 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-status 404, zodat de client de wachtwoordmanager op de hoogte kan stellen via PublicKeyCredential.signalUnknownCredential().
  2. Wachtwoordverificatie (/api/login/password): Verifieer de username (cred.id) en password (cred.password) aan de hand van de gebruikersdatabase en stel een geverifieerde sessie in.

Implementatie aan de clientzijde

Verzoek voor uniforme inloggegevens

  1. Zowel toegangssleutels als wachtwoorden aanvragen: Decodeer de serveropties met PublicKeyCredential.parseRequestOptionsFromJSON() en roep navigator.credentials.get() aan in een door de gebruiker gestarte klikhandler met password: true, publicKey en uiMode: 'immediate' (zonder signal door te geven).
  2. Filiaalverificatie door cred.type:
    • Als cred.type === 'public-key', serialiseer je de bewering met cred.toJSON() en POST je deze naar je WebAuthn-verificatie-eindpunt. Als de server reageert met HTTP 404, roep je PublicKeyCredential.signalUnknownCredential({ rpId: publicKey.rpId, credentialId: encoded.id }) aan.
    • Als cred.type === 'password', POST { username: cred.id, password: cred.password } naar je eindpunt voor wachtwoordverificatie.
  3. Verwerk NotAllowedError als er geen lokale inloggegevens bestaan: Vang NotAllowedError op (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 /signin gaan 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 de capabilities.immediateGet-runtimecheck via PublicKeyCredential.getClientCapabilities(). VERPLICHT: Controleer altijd capabilities.immediateGet voordat je navigator.credentials.get({ uiMode: 'immediate' }) aanroept. Browsers die uiMode: 'immediate' niet herkennen, negeren de onbekende woordenboeksleutel en openen een blokkerend modaal dialoogvenster voor toegangssleutels en QR-codes. Als capabilities.immediateGet onwaar is of als navigator.credentials.get() wordt geweigerd met NotAllowedError, 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 PasswordCredential niet ondersteunen, negeren de optie password: true of vallen via de functiecheck capabilities.immediateGet terug 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.