Verificatie met toegangssleutel

In deze handleiding wordt uitgelegd hoe je verificatie van terugkerende gebruikers implementeert met vindbare inloggegevens, zowel via expliciete knoptriggers als naadloze suggesties voor automatisch invullen in de browser (voorwaardelijke UI).

Aan de serverzijde

Opties genereren

Maak een eindpunt dat WebAuthn-verzoekparameters genereert met een gecontroleerde bibliotheek volgens de normen.

  1. De vooraf gedefinieerde RP-ID gebruiken: Gebruik de vooraf gedefinieerde juiste RP-ID als een constante tekenreeks.
  2. Challenge genereren: Genereer een cryptografisch beveiligde willekeurige buffer met hoge entropie, sla deze beveiligd op in de sessie van de gebruiker en codeer deze als Base64URL.
  3. Toewijzing van vindbare inloggegevens: Voer een lege matrix [] in voor allowCredentials. Hiermee worden vindbare inloggegevens aangevraagd, wat betekent dat de gebruiker niet eerst de gebruikersnaam hoeft in te voeren. De aanbieder van de toegangssleutel presenteert de beschikbare accounts.
  4. Gebruikersverificatieniveau: Stel userVerification: "preferred" in (of "required" als dit expliciet is vereist door het nalevingsbeleid van het bedrijf).
    • Het aangevraagde userVerification-beperkingsniveau MOET worden opgeslagen in de serversessieregistratie op het optie-eindpunt, in plaats van te worden teruggestuurd door de client via querytekenreeksen. Zo kan het verificatie-eindpunt strenge overeenkomstbeperkingen veilig afdwingen zonder risico op manipulatie door de client.
// Options generation example (discoverable flow)
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
  rpId: "example.com",
  allowCredentials: [], // Request discoverable passkeys
  userVerification: "preferred",
};

// Persist expected UV level to user session
req.session.expectedUserVerification = "preferred";

Verificatie-eindpunt

Verifieer de bewering die door de client is geretourneerd op een beveiligde manier om de gebruiker te verifiëren:

  1. Sessie-challenge valideren: Dwing strikte challenge-overeenkomst af tussen de clientreactie en de verwachte challenge die is opgeslagen in de sessie.
  2. UV-voorkeuren afdwingen:
    • Verificatiemethoden zonder gebruikersverificatie toestaan (bijv. schermvergrendelingen voor verificatie uitgezet) als de sessie expectedUserVerification heeft aangevraagd"preferred" door requireUserVerification: false door te geven aan je bibliotheek voor verificatie aan de serverzijde. Als hierom wordt gevraagd "required", dwing je biometrische gegevens/pincode strikt af.
  3. Serverfout 404 opschonen: Als de door de client geretourneerde inloggegevens-ID niet in de database wordt gevonden, retourneer je een expliciete HTTP 404-fout, zodat de client de Signal API kan activeren.

Logica aan de clientzijde

Annotatie voor HTML-formulier

Annoteer je invoer voor gebruikersnaam en wachtwoord om de voorwaardelijke UI native te gebruiken. Tokens voor automatisch aanvullen combineren de parameters van de webauthn-specificatie en autofocus activeert de pop-up voor automatisch invullen van de browser onmiddellijk wanneer de invoer wordt gefocust.

<!-- Autocomplete tokens must contain webauthn space-separated -->
<form id="signin-form">
  <input
    type="text"
    name="username"
    autocomplete="username webauthn"
    autofocus
    data-testid="username-field"
  />
  <input type="password" name="password" autocomplete="current-password" />
  <button type="submit">Sign in</button>
</form>

Expliciete knoppenflow

Activeer verificatie met toegangssleutel als een gebruiker op de knop Inloggen met toegangssleutel klikt. Breek lopende aanroepen voor automatisch invullen van formulieren (Conditional Get) af voordat je de toegangssleutelprompt aanroept.

Conditionele bemiddelingsflow (automatisch invullen van formulieren)

Activeer suggesties voor automatisch invullen van formulieren bij het laden van de pagina om verificatie met toegangssleutel standaard aan te bieden wanneer gebruikers zich richten op inlogvelden:

  1. Functiedetectie: Roep PublicKeyCredential.getClientCapabilities() aan als de pagina wordt geladen en sla inloggen met een toegangssleutel over als conditionalGet niet beschikbaar is.
  2. Decodeeropties: Decodeer het opgehaalde json-object met inloggegevens met PublicKeyCredential.parseRequestOptionsFromJSON().
  3. Voorwaardelijke Get aanroepen: Roep navigator.credentials.get() aan met mediation: "conditional" en geef een AbortController-signaal door. Hiermee wordt automatisch invullen op de achtergrond geregistreerd zonder dat er een dialoogvenster voor toegangssleutels wordt getoond.
  4. Try/Catch Exception Segregation: Verpak de navigator.credentials.get-aanroep in een try/catch-blok:
    • NotAllowedError: De gebruiker heeft de inlogprompt voor de toegangssleutel geannuleerd of de time-out is bereikt.
    • AbortError: Het verificatieverzoek is programmatisch geannuleerd.
  5. Call Signal API: Verpak de serververificatieaanroep fetch() in een try/catch-blok:
    • Toon een foutmelding zodat de gebruiker begrijpt wat er is misgegaan.
    • Roep signalUnknownCredential() ALLEEN aan als de server expliciet reageert met de HTTP-status 404 (Inloggegevens niet gevonden) en de gebruiker niet is geverifieerd.
    • De parameter credentialId die wordt doorgegeven aan signalUnknownCredential() MOET strikt de Base64URL-gecodeerde tekenreeks voor de inloggegevens-ID zijn (bijv. encoded.id), NIET het onbewerkte ArrayBuffer-object credential.rawId.
  6. De reactie coderen: Codeer de inloggegevens AuthenticatorAssertionResponse met .toJSON() voordat je ze naar de server stuurt voor verificatie.
// optionsFetch and loginVerifyFetch are app-defined HTTP methods
import { optionsFetch, loginVerifyFetch } from "./api.js";

let autofillAbortController = new AbortController();

async function initializeConditionalAutofill() {
  // Feature detect Conditional Get autofill support
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (capabilities.conditionalGet === true) {
    const loginOptionsJSON = await optionsFetch();
    const publicKey =
      PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

    try {
      // Initiate Conditional UI form autofill suggestions
      const credential = await navigator.credentials.get({
        publicKey,
        signal: autofillAbortController.signal,
        mediation: "conditional",
      });

      // Segregated verification fetch
      const encoded = credential.toJSON();
      const response = await loginVerifyFetch(encoded);
      if (!response.ok && response.status === 404) {
        // Note: this code path runs pre-authentication, satisfying the unauth precondition
        if (PublicKeyCredential.signalUnknownCredential) {
          await PublicKeyCredential.signalUnknownCredential({
            rpId, // RP ID must match the one defined on the server
            credentialId: encoded.id,
          });
        }
      }
    } catch (err) {
      // Silently swallow expected client WebAuthn exceptions
      if (["NotAllowedError", "AbortError"].includes(err.name)) {
        return;
      }
      console.error("Unexpected conditional get error:", err);
    }
  }
}

async function triggerButtonAuthentication() {
  // Abort any pending Conditional Get call to prevent passkey prompt collisions
  autofillAbortController.abort();
  autofillAbortController = new AbortController(); // Reset controller for next triggers

  const loginOptionsJSON = await optionsFetch();
  const publicKey =
    PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

  let credential;
  try {
    // Passkey explicit prompt trigger
    credential = await navigator.credentials.get({
      publicKey,
      signal: autofillAbortController.signal,
    });
  } catch (err) {
    if (err.name === "NotAllowedError") {
      console.log("User cancelled passkey login.");
    } else if (err.name === "AbortError") {
      console.log("The authentication operation was aborted.");
    }
    // Re-arm Conditional autofill Suggestions after cancelled explicit button prompts
    initializeConditionalAutofill();
    return; // Safe exit
  }

  // Segregated verification try/catch (HTTP 404 trigger)
  const encoded = credential.toJSON();
  try {
    const response = await loginVerifyFetch(encoded);
    if (!response.ok && response.status === 404) {
      // Note: this code path runs pre-authentication, satisfying the unauth precondition
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encoded.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Verification request error:", serverErr);
  }
}

// Trigger Conditional Get on load
window.addEventListener("DOMContentLoaded", initializeConditionalAutofill);

Reservestrategieën

Terugvaloptie voor detectie van toegangssleutelfunctie

Installeer altijd 'webauthn-polyfills' en importeer dit in de context. Zolang PublicKeyCredential wordt ondersteund, wordt PublicKeyCredential.getClientCapabilities ook ondersteund.

import 'webauthn-polyfills';

Terugvaloptie voor synchronisatie van Signal API

De WebAuthn Signal API (webauthn-signals) is een progressieve optimalisatie die wordt gebruikt om wachtwoordmanagers gesynchroniseerd te houden met de status van de servergegevens.

  • Fallback Experience: Gated via if (PublicKeyCredential.signalUnknownCredential). Als dit niet wordt ondersteund, wordt de synchronisatie van de achtergrondverificatie overgeslagen zonder browseruitzonderingen te genereren.

Easy JSON Serialization Fallback

Installeer altijd 'webauthn-polyfills' en importeer dit in de context. Zolang PublicKeyCredential wordt ondersteund, worden PublicKeyCredential.parseRequestOptionsFromJSON en PublicKeyCredential.prototype.toJSON ook ondersteund.

import 'webauthn-polyfills';