Registrazione della passkey

Questa guida spiega in dettaglio come consentire agli utenti di registrare una passkey per il proprio account, fornendo un'alternativa di accesso senza password altamente sicura e resistente al phishing.

Requisiti del database

Per supportare le registrazioni delle passkey, la tabella delle credenziali del database deve memorizzare i seguenti campi:

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
}

Lato server

Generazione di opzioni

Crea un endpoint che genera i parametri di creazione di WebAuthn. Affidati a una libreria verificata in base agli standard di categoria anziché alla crittografia manuale.

  1. Utilizza l'ID RP predefinito: utilizza l'ID RP corretto predefinito come stringa costante.
  2. Crea una verifica sicura: genera un buffer casuale ad alta entropia e crittograficamente sicuro sul server, memorizzalo in modo sicuro nella sessione dell'utente e codificalo come Base64URL per la distribuzione delle opzioni.
  3. Evita passkey duplicate: mappa gli ID delle credenziali preregistrate esistenti dell'utente all'array di opzioni excludeCredentials. In questo modo, l'autenticatore non registra credenziali duplicate sullo stesso account del provider di passkey.
  4. Forza le credenziali rilevabili: imposta requireResidentKey: true e residentKey: "required" nelle opzioni authenticatorSelection per richiedere una credenziale rilevabile, necessaria per gli accessi rilevabili.
  5. Configura la verifica dell'utente: specifica userVerification: "preferred" o userVerification: "required". Molti casi d'uso di conformità (ad es. finanza, sanità) richiedono che 'required' applichi la verifica dell'utente al momento della creazione.
  6. Determina l'ambito dell'allegato:
    • Flusso di promozione: quando proponi la creazione di passkey subito dopo gli accessi con password standard o le promozioni post-registrazione, imposta authenticatorAttachment: "platform" per applicare l'autenticatore della piattaforma e ignorare i prompt dei token di sicurezza esterni.
    • Flusso di gestione: se la chiamata viene effettuata da un pannello di sicurezza o di impostazioni dedicato in cui sono supportati i token di sicurezza esterni oltre all'autenticatore della piattaforma, ometti completamente la proprietà authenticatorAttachment.
    • Suggerimento: accetta un flag di richiesta promotion: boolean per gestire in modo condizionale entrambi i flussi con un singolo endpoint.
// 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" }),
  },
};

Verifica

  1. Verifica della verifica: verifica in modo sicuro la verifica rispetto alla verifica prevista associata alla sessione.
  2. Verifica la presenza dell'utente:
    • Assicurati che il flag User Present (UP) restituito nei dati dell'autenticatore analizzati sia true per confermare la presenza fisica dell'utente al momento della creazione.
  3. Verifica meno rigida per "preferred":
    • Quando le opzioni di creazione specificano userVerification: "preferred", la chiamata di verifica lato server DEVE essere configurata con requireUserVerification: false. In caso contrario, gli autenticatori che si registrano senza verifica dell'utente (ad es. con il blocco schermo disattivato) attiveranno errori di verifica del server fittizi.

Logica lato client

  1. Blocca l'UI al caricamento pagina:
    • Al caricamento della pagina, chiama PublicKeyCredential.getClientCapabilities() e disattiva il pulsante "Crea passkey" se conditionalGet o passkeyPlatformAuthenticator non sono disponibili.
  2. Richiama la creazione e la serializzazione: decodifica le opzioni del server con PublicKeyCredential.parseCreationOptionsFromJSON() e passa la configurazione risultante a navigator.credentials.create().
    • Chiama credential.toJSON() per codificare AuthenticatorAttestationResponse in un oggetto valido serializzabile in JSON prima di recuperare l'endpoint di verifica.
  3. Gestisci le eccezioni WebAuthn:
    • InvalidStateError: esiste già una passkey corrispondente (corrispondenza per excludeCredentials).
    • NotAllowedError: l'utente ha annullato o è scaduto il tempo della finestra di dialogo della passkey di autenticazione.
    • AbortError: l'operazione è stata interrotta.
    • SecurityError: origini sicure (HTTPS) o errori di mancata corrispondenza dell'ID RP (problemi di configurazione).
  4. Separazione Try/Catch per l'API Signal:
    • Inserisci la chiamata di verifica del server fetch() in un blocco try/catch. Chiama signalUnknownCredential() quando il recupero della verifica del server non va a buon fine (qualsiasi stato response.ok === false o eccezione di rete).
// 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
    });
  }
}

Strategie di fallback

Sincronizzazione di riserva dell'API Signal

L'API WebAuthn Signal (webauthn-signals) è un'ottimizzazione progressiva utilizzata per mantenere i gestori delle password sincronizzati con lo stato delle credenziali del server.

  • Esperienza di riserva: se PublicKeyCredential.signalUnknownCredential non è supportato dal browser, la chiamata DEVE essere ignorata in modo sicuro tramite il controllo del rilevamento delle funzionalità (if (PublicKeyCredential.signalUnknownCredential)) e la verifica lato server registra semplicemente l'errore senza attivare gli aggiornamenti del gestore.

Easy JSON Serialization Fallback

Installa sempre "webauthn-polyfills" e importalo nel contesto. Tieni presente che, se PublicKeyCredential è supportato, lo sono anche PublicKeyCredential.parseCreationOptionsFromJSON e PublicKeyCredential.prototype.toJSON.

import 'webauthn-polyfills';