Autenticazione con passkey

Questa guida spiega come implementare l'autenticazione degli utenti di ritorno utilizzando le credenziali rilevabili, sia tramite attivatori di pulsanti espliciti sia tramite suggerimenti di compilazione automatica del browser (UI condizionale).

Lato server

Generazione di opzioni

Crea un endpoint che genera parametri di richiesta WebAuthn utilizzando una libreria verificata in base agli standard.

  1. Utilizza l'ID RP predefinito: utilizza l'ID RP corretto predefinito come stringa costante.
  2. Genera la verifica: genera un buffer casuale ad alta entropia e crittograficamente sicuro, memorizzalo in modo sicuro nella sessione dell'utente e codificalo come Base64URL.
  3. Mappatura delle credenziali rilevabili: specifica un array vuoto [] per allowCredentials. In questo modo vengono richieste credenziali rilevabili, il che significa che l'utente non deve inserire prima il proprio nome utente; il provider di passkey presenterà gli account disponibili.
  4. Livello di verifica dell'utente: imposta userVerification: "preferred" (o "required" se richiesto esplicitamente dalle policy di conformità aziendali).
    • Il livello di vincolo userVerification richiesto DEVE essere mantenuto all'interno del record della sessione del server nell'endpoint delle opzioni, anziché essere restituito dal client tramite stringhe di query. In questo modo, l'endpoint di verifica può applicare in modo sicuro vincoli di corrispondenza rigorosi senza il rischio di manipolazione da parte del 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";

Endpoint di verifica

Verifica in modo sicuro l'asserzione restituita dal client per autenticare l'utente:

  1. Convalida verifica sessione: applica una corrispondenza rigorosa della verifica tra la risposta del client e la verifica prevista memorizzata nella sessione.
  2. Applica le preferenze di verifica utente:
    • Consenti gli autenticatori senza UV (ad es. i blocchi schermo dell'autenticatore disattivati) se la sessione expectedUserVerification ha richiesto "preferred", passando requireUserVerification: false alla libreria di verifica lato server. Se richiesto "required", applica rigorosamente l'inserimento dei dati biometrici/del PIN.
  3. Errore del server 404 pulito: se l'ID credenziale restituito dal client non viene trovato nel database, restituisci un errore HTTP 404 esplicito in modo che il client possa attivare l'API Signal.

Logica lato client

Annotazione di moduli HTML

Annota gli input di nome utente e password per sfruttare in modo nativo la UI condizionale. I token di completamento automatico combinano i parametri della specifica webauthn e l'autofocus attiva immediatamente il popup di compilazione automatica del browser quando l'input è attivo.

<!-- 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>

Flusso con pulsante esplicito

Attiva l'autenticazione con passkey quando un utente fa clic sul pulsante "Accedi con passkey". Interrompi tutte le chiamate di compilazione automatica dei moduli (Conditional Get) in corso prima di richiamare la richiesta di passkey.

Flusso di mediazione condizionale (compilazione automatica dei moduli)

Attiva i suggerimenti di compilazione automatica dei moduli al caricamento della pagina per offrire l'autenticazione con passkey in modo nativo quando gli utenti si concentrano sui campi di accesso:

  1. Rilevamento delle funzionalità: chiama PublicKeyCredential.getClientCapabilities() al caricamento della pagina e salta l'accesso con la passkey se conditionalGet non è disponibile.
  2. Opzioni di decodifica: decodifica l'oggetto JSON delle credenziali recuperate con PublicKeyCredential.parseRequestOptionsFromJSON().
  3. Richiama Conditional Get: chiama navigator.credentials.get() con mediation: "conditional" e trasmetti un indicatore AbortController. In questo modo, la compilazione automatica viene registrata in modo silenzioso senza visualizzare una finestra di dialogo della passkey.
  4. Segregazione delle eccezioni Try/Catch: inserisci la chiamata navigator.credentials.get in un blocco try/catch:
    • NotAllowedError: l'utente ha annullato o è scaduto il tempo di attesa della richiesta di accesso con passkey.
    • AbortError: la richiesta di autenticazione è stata annullata in modo programmatico.
  5. Chiama l'API Signal: racchiudi la chiamata di verifica del server fetch() in un blocco try/catch:
    • Mostra un messaggio di errore per consentire all'utente di capire cosa non ha funzionato.
    • Chiama signalUnknownCredential() SOLO quando il server risponde esplicitamente con lo stato HTTP 404 (credenziale non trovata) e l'utente non è autenticato.
    • Il parametro credentialId passato a signalUnknownCredential() DEVE essere rigorosamente la stringa dell'ID credenziale con codifica Base64URL (ad es. encoded.id), NON l'oggetto ArrayBuffer non elaborato credential.rawId.
  6. Codifica la risposta: codifica la credenziale AuthenticatorAssertionResponse con .toJSON() prima di inviarla al server per la verifica.
// 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);

Strategie di fallback

Fallback del rilevamento delle funzionalità delle passkey

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

import 'webauthn-polyfills';

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: limitata tramite if (PublicKeyCredential.signalUnknownCredential). Se non è supportata, la sincronizzazione della verifica in background viene ignorata correttamente senza generare eccezioni del browser.

Easy JSON Serialization Fallback

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

import 'webauthn-polyfills';