Schnelle Anmeldung mit dem Modus „Sofortige UI“

Wenn nicht authentifizierte Nutzer auf einen Link „Anmelden“ klicken, den Bezahlvorgang starten, einen Artikel zu den Favoriten hinzufügen oder eine Paywall für einen Artikel erreichen, werden sie auf eine spezielle Anmeldeseite weitergeleitet. Das führt zu einem Kontextverlust und erhöht die Abbruchrate. Der Immediate UI-Modus (uiMode: 'immediate' in navigator.credentials.get()) ermöglicht eine kontextbezogene schnelle Anmeldung, indem sofort eine einheitliche Kontoauswahl im Browser angezeigt wird, wenn auf dem Gerät lokale Passkeys (publicKey) oder gespeicherte Passwörter (password: true) vorhanden sind.

Im Gegensatz zu standardmäßigen modalen WebAuthn-Aufforderungen, bei denen immer die Browser-UI angezeigt wird und die auf geräteübergreifende QR-Codes oder externe Sicherheitsschlüssel zurückgreifen, schlägt der Immediate UI-Modus schnell mit einem NotAllowedError fehl, wenn auf dem Gerät keine geeigneten Anmeldedaten lokal verfügbar sind. So kann Ihre Anwendung wiederkehrende Nutzer mit nur einem Tippen authentifizieren und Nutzer ohne lokale Anmeldedaten nahtlos zur Standardanmeldeseite, zum Bezahlen als Gast oder zum Inline-Anmeldeformular weiterleiten.

Lösen Sie den Modus „Sofortige UI“ in einem vom Nutzer initiierten Event-Handler aus (erfordert eine vorübergehende Nutzeraktivierung), wenn die Nutzerabsicht klar ist, unmittelbar bevor der Nutzer die aktuelle Seite verlässt. Wenn die sofortige Authentifizierung erfolgreich ist, führen Sie die Aktion des Nutzers sofort aus (entweder durch Aktualisieren der aktuellen Seite oder durch direktes Weiterleiten zum authentifizierten Ziel). Wenn der Vorgang mit NotAllowedError fehlschlägt, zeige einen Anmelde- oder Registrierungsablauf mit einem Pfad an, über den der Nutzer sein Vorhaben abschließen kann.

Anwendungsfälle:

  • Abfangen von Klicks auf den Anmeldelink oder die Anmeldeschaltfläche im Header: Sie können einen Klick auf einen globalen Anmeldelink (event.preventDefault()) abfangen, um wiederkehrende Nutzer sofort zu authentifizieren, oder die Navigation zur dedizierten Anmeldeseite auf NotAllowedError fortsetzen.
  • Dynamische Anmeldung vor dem Bezahlen: Authentifizieren Sie wiederkehrende Käufer mit nur einem Tippen, wenn sie auf „Zur Kasse“ klicken, damit sie direkt zum Versand- und Zahlungsschritt gelangen, oder leiten Sie sie im Fallback-Modus zur Standardanmeldung oder zum Bildschirm „Als Gast bezahlen“ weiter.
  • Inline-Anmeldung für Mikroaktionen (Favorit, Like, Lesezeichen, Speichern): Authentifizieren Sie sich direkt, wenn ein nicht angemeldeter Nutzer auf eine Schaltfläche für eine Mikroaktion klickt, und schließen Sie die Aktion sofort ab (z. B. durch Umschalten von aria-pressed="true"). Alternativ können Sie ein Fallback-Anmeldemodal oder eine Anmeldeseite mit einem Rückgabe-URL-Parameter öffnen.
  • Weiche Paywalls in Artikeln: Leser können sich direkt authentifizieren, wenn sie auf einem Paywall-Banner auf „Mit deinem Konto weiterlesen“ klicken. Alternativ können sie ein Inline-Anmelde- oder Registrierungsformular im Fallback-Modus maximieren.

Zu vermeidende Anti-Patterns:

  • Verwenden Sie den Modus „Immediate UI“ NICHT auf einer dedizierten Anmeldeseite: Verknüpfen Sie uiMode: 'immediate' niemals mit der primären Schaltfläche „Anmelden“ oder „Mit einem Passkey anmelden“ auf der endgültigen Anmeldeseite. Nutzer gelangen auf die spezielle Anmeldeseite, wenn sie keine lokalen Anmeldedaten auf dem aktuellen Gerät haben oder wenn sie einen geräteübergreifenden Passkey (Scannen eines QR-Codes mit einem Smartphone) oder einen externen Hardware-Sicherheitsschlüssel verwenden möchten. Da uiMode: 'immediate' die geräteübergreifenden QR-Code- und Sicherheitsschlüsseloptionen unterdrückt und sofort NotAllowedError auslöst, wenn keine lokalen Anmeldedaten vorhanden sind, führt die Verwendung auf einer dedizierten Anmeldeseite dazu, dass der Nutzer in einer Sackgasse landet, in der das Klicken auf die Schaltfläche nichts bewirkt. Verwenden Sie auf einer dedizierten Anmeldeseite die bedingte Benutzeroberfläche (mediation: 'conditional' mit autocomplete="username webauthn") für Formulareingaben zusammen mit einer standardmäßigen modalen navigator.credentials.get({ publicKey })-Schaltfläche (siehe Passkey-Authentifizierung).
  • Verwenden Sie den Modus „Immediate UI“ NICHT für die erneute Authentifizierung mit erhöhten Rechten: Verwenden Sie uiMode: 'immediate' niemals, um einen bereits angemeldeten Nutzer vor sensiblen Vorgängen (z. B. Ändern eines Passworts, Aktualisieren einer Versandadresse, Bearbeiten gespeicherter Zahlungsmethoden oder Hinzufügen eines neuen Passkeys) noch einmal zu authentifizieren. Bei der erneuten Authentifizierung muss die Aufforderung über eine nicht leere allowCredentials-Liste auf die Anmeldedaten des angemeldeten Nutzers beschränkt werden. Der Modus „Immediate UI“ lehnt dies mit NotAllowedError ab. Wenn allowCredentials weggelassen wird, kann der Nutzer versehentlich ein anderes Konto als das aktuell angemeldete auswählen. Informationen zur Step-up-Reauthentifizierung finden Sie unter Passkey-Reauthentifizierung.

Datenschutz- und Sicherheitseinschränkungen

Browser erzwingen strenge Datenschutz- und Sicherheitsregeln, wenn uiMode: 'immediate' angefordert wird:

  • Vorübergehende Nutzeraktivierung erforderlich: Sie MÜSSEN navigator.credentials.get({ uiMode: 'immediate' }) innerhalb eines vom Nutzer initiierten Event-Handlers aufrufen (z. B. eines click-Listeners für ein <button>- oder <a>-Element), um eine unbemerkte Abfrage von Anmeldedaten zu verhindern. Rufen Sie sie niemals automatisch beim Seitenaufbau auf (DOMContentLoaded). Durch den Aufruf der API wird die vorübergehende Aktivierung nicht verbraucht.
  • Keine nicht leere allowCredentials-Liste übergeben: Füllen Sie publicKey.allowCredentials NICHT aus, wenn uiMode: 'immediate' angegeben ist. Browser lehnen jede Anfrage im Immediate UI-Modus mit einer nicht leeren allowCredentials-Liste sofort ab, indem sie NotAllowedError auslösen. So können vertrauende Seiten nicht nach bestimmten Anmeldedaten-IDs suchen, um frühere Besuche abzuleiten oder Nutzer über Sitzungen hinweg zu verfolgen.
  • Keine AbortSignal (signal) übergeben: Legen Sie die Property signal für navigator.credentials.get() NICHT fest, wenn uiMode: 'immediate' angegeben ist, da Browser das programmatische Schließen des Dialogfelds für die sofortige Anmeldung nicht zulassen. Wenn auf Ihrer Seite auch eine Anfrage für eine bedingte Benutzeroberfläche (mediation: 'conditional') im Hintergrund ausgeführt wird, rufen Sie abort() für diese bedingte Benutzeroberfläche (AbortController) auf, bevor Sie den Modus „Sofortige Benutzeroberfläche“ aufrufen. Lassen Sie signal in der Anfrage für die sofortige Benutzeroberfläche weg.
  • Inkognito- und private Browsersitzungen: Anfragen, die in Inkognito- oder privaten Browserfenstern gestellt werden, werden immer mit NotAllowedError abgelehnt, sodass private Sitzungen nicht von Sitzungen ohne lokale Anmeldedaten unterschieden werden können.

Serverseitige Einrichtung

Endpunkt für die Generierung von Optionen

Stellen Sie einen Endpunkt bereit, der auffindbare WebAuthn-Anfrageoptionen für nicht authentifizierte Besucher generiert:

  1. Sitzungsgebundene Challenge generieren: Erstellen Sie einen kryptografisch zufälligen Challenge-Zwischenspeicher, speichern Sie ihn in der Serversitzung und codieren Sie ihn als Base64URL.
  2. Einstellung für die Nutzerbestätigung beibehalten: Legen Sie userVerification: 'preferred' fest und speichern Sie expectedUserVerification: 'preferred' in der Sitzung.
  3. allowCredentials weglassen: Fügen Sie den Optionen für den Modus „Sofortige UI“ keine nicht leere allowCredentials-Liste hinzu, da Browser nicht leere Zulassungslisten mit NotAllowedError ablehnen.
// 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);
});

Endpunkte für die Bestätigung mit zwei Anmeldedaten

Da navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' }) entweder mit einem Passkey (PublicKeyCredential) oder einem gespeicherten Passwort (PasswordCredential) aufgelöst werden kann, müssen Sie Bestätigungshandler für beide Arten von Anmeldedaten bereitstellen:

  1. WebAuthn-Assertion-Überprüfung (/api/login/verify): Die Assertion wird mit der Sitzungs-Challenge und dem gespeicherten öffentlichen Schlüssel abgeglichen. Wenn die Anmeldedaten-ID nicht in der Datenbank gefunden wird, wird der HTTP-Status 404 zurückgegeben, damit der Client den Passwortmanager über PublicKeyCredential.signalUnknownCredential() benachrichtigen kann.
  2. Passwortbestätigung (/api/login/password): username (cred.id) und password (cred.password) werden mit der Nutzerdatenbank abgeglichen und eine authentifizierte Sitzung wird eingerichtet.

Clientseitige Implementierung

Anfrage für einheitliche Anmeldedaten

  1. Sowohl Passkeys als auch Passwörter anfordern: Decodieren Sie die Serveroptionen mit PublicKeyCredential.parseRequestOptionsFromJSON() und rufen Sie navigator.credentials.get() in einem vom Nutzer initiierten Klick-Handler mit password: true, publicKey und uiMode: 'immediate' auf (ohne signal zu übergeben).
  2. Bestätigung der Filiale durch cred.type:
    • Wenn cred.type === 'public-key', serialisieren Sie die Assertion mit cred.toJSON() und senden Sie sie per POST an Ihren WebAuthn-Bestätigungsendpunkt. Wenn der Server mit HTTP 404 antwortet, rufen Sie PublicKeyCredential.signalUnknownCredential({ rpId: publicKey.rpId, credentialId: encoded.id }) auf.
    • Wenn cred.type === 'password', POST { username: cred.id, password: cred.password } an den Endpunkt für die Passwortbestätigung.
  3. NotAllowedError verarbeiten, wenn keine lokalen Anmeldedaten vorhanden sind: Fangen Sie NotAllowedError ab (wird sofort ausgelöst, wenn keine lokalen Passkeys oder gespeicherten Passwörter auf dem Gerät vorhanden sind oder wenn der Nutzer die Auswahl schließt) und wechseln Sie zu Ihrem Standard-Anmeldeablauf (z. B. Öffnen eines Inline-Anmeldedialogs oder Weiterleitung zu /signin mit einer Rückgabe-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;
  }
});

Fallback-Strategien

Sofortiger Fallback für den UI-Modus

  • Progressive Verbesserung: Behandle uiMode: 'immediate' als progressive Verbesserung. Schließe nicht aus dem Namen oder der Version eines Browsers auf die Unterstützung. Das einzige zuverlässige Signal für die Unterstützung ist die Laufzeitprüfung von capabilities.immediateGet über PublicKeyCredential.getClientCapabilities(). OBLIGATORISCH: Prüfen Sie immer capabilities.immediateGet, bevor Sie navigator.credentials.get({ uiMode: 'immediate' }) aufrufen. Browser, die uiMode: 'immediate' nicht erkennen, ignorieren den unbekannten Wörterbuchschlüssel und öffnen ein blockierendes modales Dialogfeld für Passkeys und QR-Codes. Wenn capabilities.immediateGet falsch ist oder navigator.credentials.get() mit NotAllowedError abgelehnt wird, greifen Sie auf die Standardanmeldung Ihrer Anwendung zurück (z. B. durch Öffnen eines Inline-Anmeldemodals oder durch Aufrufen der dedizierten Anmeldeseite mit einem Rückgabe-URL-Parameter), damit Nutzer in nicht unterstützten Browsern Ihren bestehenden Anmeldeablauf unverändert sehen:
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' })
}

Fallback für die Erkennung von Clientfunktionen

Installieren Sie immer 'webauthn-polyfills' und importieren Sie es in Ihr Clientmodul, damit PublicKeyCredential.getClientCapabilities() verfügbar ist, wenn PublicKeyCredential unterstützt wird:

import 'webauthn-polyfills';

Fallback für Passwort-Anmeldedaten

Wenn Sie password: true zusammen mit publicKey in navigator.credentials.get() übergeben, können unterstützte Browser gespeicherte Passwörter aus dem Passwortmanager des Browsers in die einheitliche Kontoauswahl aufnehmen.

  • Fallback-Verfahren: Browser, die PasswordCredential nicht unterstützen, ignorieren die Option password: true oder greifen über die Funktionsprüfung capabilities.immediateGet auf das Standard-Anmeldeformular zurück, bei dem <input autocomplete="username webauthn"> und <input type="password" autocomplete="current-password"> das native automatische Ausfüllen von Passwörtern ermöglichen.

Einfacher Fallback für die JSON-Serialisierung

Installieren Sie immer 'webauthn-polyfills' und importieren Sie es in das Client-Bundle, damit PublicKeyCredential.parseRequestOptionsFromJSON() und PublicKeyCredential.prototype.toJSON() in allen Browsern, die PublicKeyCredential unterstützen, einheitlich funktionieren:

import 'webauthn-polyfills';

Fallback für die Signal API-Synchronisierung

Die WebAuthn Signal API (webauthn-signals) ist eine progressive Verbesserung, mit der veraltete Passkeys aus dem Passwortmanager des Nutzers entfernt werden, wenn der Server mit HTTP 404 antwortet.

  • Fallback-Erfahrung: Aufrufe hinter if (PublicKeyCredential.signalUnknownCredential) blockieren. Wenn die Funktion nicht unterstützt wird, überspringen Sie den Signalaufruf und zeigen Sie eine klare Meldung an, in der der Nutzer aufgefordert wird, sich mit einer alternativen Methode anzumelden.