Toegangssleutelregistratie

In deze handleiding staat hoe je gebruikers een toegangssleutel voor hun account laat registreren. Zo kunnen ze inloggen zonder wachtwoord, wat een zeer veilige, phishingbestendige methode is.

Databasevereisten

Als je registraties van toegangssleutels wilt ondersteunen, moet je tabel met databasetoegangscodes de volgende velden opslaan:

export interface StoredPasskeyCredential {
  id: string; // Base64URL-encoded credential ID (unique lookup key)
  passkeyUserId: string; // Associated application user ID
  credentialPublicKey: string; // Base64URL-encoded public key used to verify assertion signatures
  credentialType: "public-key";
  credentialDeviceType: "singleDevice" | "multiDevice"; // Helps distinguish device-bound vs cloud-synced passkeys
  credentialBackedUp: boolean; // Boolean backup state reported by the authenticator
  aaguid: string; // Authenticator Attestation GUID
  providerIcon?: string; // Provider icon derived from the AAGUID registry (dark or light theme URLs)
  name: string; // Provider name derived from AAGUID registry
  transports: string[]; // Array of transport names (e.g. 'internal', 'hybrid') necessary for exclusion options
  lastUsedAt?: number; // Optional epoch timestamp of last sign-in
  registeredAt: number; // Registration epoch timestamp
  counter: number; // Authenticator sign-in signature counter used to prevent replay attacks
}

Aan de serverzijde

Opties genereren

Maak een eindpunt dat WebAuthn-parameters voor maken genereert. Vertrouw op een gecontroleerde bibliotheek volgens de categorienormen in plaats van handmatig cryptografie te schrijven.

  1. De vooraf gedefinieerde RP-ID gebruiken: Gebruik de vooraf gedefinieerde juiste RP-ID als een constante tekenreeks.
  2. Maak een beveiligde challenge: Genereer een cryptografisch beveiligde willekeurige buffer met hoge entropie op de server, sla deze beveiligd op in de sessie van de gebruiker en codeer deze als Base64URL voor de levering van opties.
  3. Dubbele toegangssleutels voorkomen: Wijs de bestaande vooraf geregistreerde inloggegevens-ID's van de gebruiker toe aan de matrix met excludeCredentials-opties. Zo voorkom je dat de authenticator dubbele inloggegevens registreert voor hetzelfde account van de toegangssleutelprovider.
  4. Vindbare inloggegevens afdwingen: Stel requireResidentKey: true en residentKey: "required" in de opties van authenticatorSelection in om vindbare inloggegevens aan te vragen. Dit is nodig voor vindbare logins.
  5. Gebruikersverificatie instellen: Geef userVerification: "preferred" of userVerification: "required" op. Voor veel use cases voor naleving (bijvoorbeeld financiën, gezondheidszorg) is 'required' vereist om gebruikersverificatie af te dwingen bij het maken.
  6. Het bereik van de bijlage bepalen:
    • Promotieflow: Als je voorstelt om een toegangssleutel te maken direct na standaard inloggen met een wachtwoord of promoties na aanmelding, stel je authenticatorAttachment: "platform" in om de platform-authenticator af te dwingen en prompts voor externe beveiligingssleutels over te slaan.
    • Beheerproces: Als de aanroep wordt uitgevoerd vanuit een speciaal instellingen- of beveiligingsvenster waarin externe beveiligingssleutels worden ondersteund naast de platformverificator, laat je de property authenticatorAttachment helemaal weg.
    • Tip: Accepteer een promotion: boolean-verzoekmarkering om beide stromen voorwaardelijk te verwerken met één eindpunt.
// Options generation example
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // Cryptographically random challenge
  rp: { id: "example.com", name: "Secure Application" },
  user: {
    id: userBase64UrlId, // Unique base64url string identifying the account
    name: "user@example.com",
    displayName: "Jane Doe",
  },
  pubKeyCredParams: [
    {
      type: "public-key",
      alg: -7,
    },
    {
      type: "public-key",
      alg: -257,
    },
  ],
  excludeCredentials: userExistingCredentials.map((cred) => ({
    type: "public-key",
    id: cred.id,
    transports: cred.transports,
  })),
  authenticatorSelection: {
    residentKey: "required",
    requireResidentKey: true,
    userVerification: "preferred",
    ...(isPromotionFlow && { authenticatorAttachment: "platform" }),
  },
};

Verificatie

  1. Challengeverificatie: Verifieer de challenge op een beveiligde manier aan de hand van de verwachte sessiegebonden challenge.
  2. Aanwezigheid van gebruiker verifiëren:
    • Zorg dat de UP-markering (User Present) die wordt geretourneerd in de geparseerde verificatorgegevens true is om de fysieke aanwezigheid van de gebruiker op het moment van maken te bevestigen.
  3. Verificatie versoepelen voor 'preferred':
    • Als de aanmaakopties userVerification: "preferred" hebben opgegeven, MOET de aanroep voor verificatie aan de serverzijde worden ingesteld met requireUserVerification: false. Anders veroorzaken authenticators die zich registreren zonder gebruikersverificatie (bijvoorbeeld als schermvergrendeling uitstaat) onterechte fouten bij de serververificatie.

Logica aan de clientzijde

  1. De UI blokkeren bij het laden van de pagina:
    • Roep bij het laden van de pagina PublicKeyCredential.getClientCapabilities() aan en zet de knop Toegangssleutel maken uit als conditionalGet of passkeyPlatformAuthenticator niet beschikbaar is.
  2. Aanroepen en serialiseren: Decodeer serveropties met PublicKeyCredential.parseCreationOptionsFromJSON() en geef de resulterende configuratie door aan navigator.credentials.create().
    • Roep credential.toJSON() aan om de AuthenticatorAttestationResponse te coderen in een geldig, json-serialiseerbaar object voordat u het verificatie-eindpunt ophaalt.
  3. WebAuthn-uitzonderingen verwerken:
    • InvalidStateError: Er bestaat al een overeenkomende toegangssleutel (overeenkomst op basis van excludeCredentials).
    • NotAllowedError: De gebruiker heeft het dialoogvenster voor de verificatietoegangssleutel geannuleerd of er is een time-out opgetreden.
    • AbortError: De bewerking is afgebroken.
    • SecurityError: Fouten met beveiligde herkomsten (HTTPS) of niet-overeenkomende RP-ID's (configuratieproblemen).
  4. Try/Catch-scheiding voor Signal API:
    • Plaats de aanroep voor serververificatie fetch() in een try/catch-blok. Roep signalUnknownCredential() aan als het ophalen van de serververificatie mislukt (elke status response.ok === false of netwerkfout).
// optionsFetch and registerVerifyFetch are app-defined HTTP methods
import { optionsFetch, registerVerifyFetch } from "./api.js";

async function registerPasskey(isPromotion = false) {
  // Verify passkey capability and conditional UI are available
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (
    !capabilities.passkeyPlatformAuthenticator ||
    !capabilities.conditionalGet
  ) {
    // Hide "Create passkey" buttons and fall back to password flows instead
    showStandardPasswordFallbackUI();
    return;
  }

  const creationOptionsJSON = await optionsFetch({ promotion: isPromotion });
  const publicKey =
    PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);

  let credential;
  try {
    // passkey prompt execution
    credential = await navigator.credentials.create({ publicKey });
  } catch (err) {
    if (err.name === "InvalidStateError") {
      console.log("A passkey already exists for this account.");
      alert("A passkey already exists for this account.");
    } else if (err.name === "SecurityError") {
      console.error("Configuration RP ID or Secure Context error.");
      alert("Configuration RP ID or Secure Context error.");
    } else if (err.name === "NotAllowedError") {
      console.log("User cancelled the passkey dialog.");
    } else if (err.name === "AbortError") {
      console.log("The creation operation has been aborted.");
    }
    return; // Safe API exit, do not signal unknown for standard WebAuthn cancels
  }

  // Server Verification phase (Segregated Try/Catch)
  let encodedResponse = credential.toJSON();
  try {
    const response = await registerVerifyFetch(encodedResponse);
    if (!response.ok) {
      // Server verification failed to verify/authenticate the credential (orphaned)
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encodedResponse.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Server verification network failure:", serverErr);
    await publickeycredential.signalunknowncredential({
      rpId, // RP ID must match the one defined on the server
      credentialid: encodedresponse.id, // base64url-encoded credential id
    });
  }
}

Reservestrategieën

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-functionaliteit: Als PublicKeyCredential.signalUnknownCredential niet wordt ondersteund door de browser, MOET het gesprek veilig worden omzeild via functieherkenning (if (PublicKeyCredential.signalUnknownCredential)). De verificatie aan de serverzijde registreert de fout dan zonder managerupdates te activeren.

Easy JSON Serialization Fallback

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

import 'webauthn-polyfills';