Accessi rapidi con la modalità UI immediata

Se un utente non autenticato viene indirizzato a una pagina di accesso dedicata quando fa clic su un link "Accedi", avvia il pagamento, salva un elemento nei preferiti o raggiunge un paywall per un articolo, si verifica una perdita di contesto e un aumento dell'abbandono degli utenti. La modalità UI immediata (uiMode: 'immediate' in navigator.credentials.get()) consente accessi rapidi contestuali presentando un selettore account del browser unificato immediatamente quando sul dispositivo sono presenti passkey locali (publicKey) o password salvate (password: true).

A differenza dei prompt WebAuthn modali standard, che mostrano sempre l'UI del browser e utilizzano come alternativa i codici QR cross-device o i token di sicurezza esterni, la modalità UI immediata fallisce rapidamente con un NotAllowedError se sul dispositivo non sono disponibili credenziali idonee a livello locale. In questo modo, la tua applicazione può autenticare gli utenti di ritorno con un solo tocco, mentre gli utenti senza credenziali locali vengono indirizzati senza problemi alla pagina di accesso standard, al checkout come ospite o al modulo di accesso in linea.

Attiva la modalità UI immediata all'interno di un gestore di eventi avviato dall'utente (che richiede l'attivazione utente temporanea) in momenti di chiara intenzione dell'utente subito prima di uscire dalla pagina corrente. Se l'autenticazione immediata va a buon fine, completa immediatamente l'intenzione dell'utente (aggiornando la pagina corrente o procedendo direttamente alla destinazione autenticata). Se l'operazione non va a buon fine con NotAllowedError, mostra un flusso di accesso o registrazione con un percorso per completare l'intenzione dell'utente al termine.

Potenziali casi d'uso:

  • Intercettazione del link o del pulsante di accesso nell'intestazione: intercetta un clic su un link "Accedi" globale (event.preventDefault()) per autenticare immediatamente gli utenti di ritorno o continua la navigazione alla pagina di accesso dedicata su NotAllowedError.
  • Accesso dinamico prima del pagamento: autentica gli acquirenti di ritorno con un solo tocco quando fanno clic su "Procedi al pagamento" in modo che arrivino direttamente al passaggio di spedizione e pagamento o passino alla schermata di accesso standard o di checkout come ospite in caso di fallback.
  • Accesso in linea per microazioni (Preferito, Mi piace, Segnalibro, Salva): autentica l'utente quando fa clic su un pulsante di microazione senza aver eseguito l'accesso e completa immediatamente l'azione (ad esempio, attivando/disattivando aria-pressed="true") oppure apri una finestra modale di accesso di riserva o una pagina di accesso con un parametro URL di ritorno.
  • Paywall flessibili negli articoli: autentica i lettori direttamente quando fanno clic su "Continua a leggere con il tuo account" su un banner di paywall o espandi un modulo di registrazione o di accesso in linea in caso di fallback.

Anti-pattern da evitare:

  • NON utilizzare la modalità UI immediata in una pagina di accesso dedicata: non collegare mai uiMode: 'immediate' al pulsante principale "Accedi" o "Accedi con una passkey" nella pagina di accesso della destinazione finale. Gli utenti raggiungono la pagina di accesso dedicata in particolare quando non hanno credenziali locali sul dispositivo corrente o quando intendono utilizzare una passkey cross-device (scansionando un codice QR con uno smartphone) o una chiave di sicurezza hardware esterna. Poiché uiMode: 'immediate' sopprime le opzioni di codice QR e token di sicurezza cross-device e genera immediatamente NotAllowedError quando non esistono credenziali locali, l'utilizzo su una pagina di accesso dedicata intrappola l'utente in un vicolo cieco in cui il clic sul pulsante non produce alcun effetto. In una pagina di accesso dedicata, utilizza la UI condizionale (mediation: 'conditional' con autocomplete="username webauthn") sugli input dei moduli abbinati a un pulsante navigator.credentials.get({ publicKey }) modale standard (consulta Autenticazione con passkey).
  • NON utilizzare la modalità UI immediata per la riautenticazione step-up: non utilizzare mai uiMode: 'immediate' per riautenticare un utente che ha già eseguito l'accesso prima di operazioni sensibili (come la modifica di una password, l'aggiornamento di un indirizzo di spedizione, la modifica dei metodi di pagamento salvati o l'aggiunta di una nuova passkey). La riautenticazione deve limitare la richiesta alle credenziali dell'utente che ha eseguito l'accesso tramite un elenco allowCredentials non vuoto, che la modalità UI immediata rifiuta con NotAllowedError, e l'omissione di allowCredentials consentirebbe all'utente di selezionare inavvertitamente un account diverso da quello attualmente connesso. Per la riautenticazione step-up, segui la procedura di riautenticazione con passkey.

Vincoli di privacy e sicurezza

I browser applicano regole di privacy e sicurezza rigorose quando viene richiesto uiMode: 'immediate':

  • È richiesta l'attivazione utente temporanea: DEVI chiamare navigator.credentials.get({ uiMode: 'immediate' }) all'interno di un gestore di eventi avviato dall'utente (ad esempio un listener click su un elemento <button> o <a>) per impedire il rilevamento silenzioso delle credenziali. Non richiamarla mai automaticamente al caricamento pagina (DOMContentLoaded). La chiamata all'API non consuma l'attivazione temporanea.
  • Non passare un elenco allowCredentials non vuoto: NON compilare publicKey.allowCredentials quando è specificato uiMode: 'immediate'. I browser rifiutano immediatamente qualsiasi richiesta in modalità UI immediata con un elenco allowCredentials non vuoto generando NotAllowedError, in modo che le relying party non possano cercare ID credenziali specifici per dedurre visite precedenti o monitorare gli utenti tra le sessioni.
  • Non passare un AbortSignal (signal): NON impostare la proprietà signal su navigator.credentials.get() quando è specificato uiMode: 'immediate', poiché i browser non consentono la chiusura programmatica della finestra di dialogo di accesso immediato. Se la tua pagina esegue anche una richiesta di UI condizionale (mediation: 'conditional') in background, chiama abort() su quella UI condizionale AbortController prima di richiamare la modalità UI immediata e ometti signal dalla richiesta immediata stessa.
  • Sessioni di navigazione in incognito e privata: le richieste effettuate in finestre di navigazione in incognito o privata vengono sempre rifiutate con NotAllowedError, in modo che le sessioni private rimangano indistinguibili dalle sessioni senza credenziali locali.

Configurazione lato server

Endpoint di generazione delle opzioni

Esponi un endpoint che genera opzioni di richiesta WebAuthn rilevabili per i visitatori non autenticati:

  1. Genera una verifica associata alla sessione: crea un buffer di verifica casuale dal punto di vista crittografico, memorizzalo nella sessione del server e codificalo come Base64URL.
  2. Rendi persistente la preferenza di verifica dell'utente: imposta userVerification: 'preferred' e memorizza expectedUserVerification: 'preferred' nella sessione.
  3. Ometti allowCredentials: non includere un elenco allowCredentials non vuoto nelle opzioni per la modalità UI immediata, poiché i browser rifiutano le liste consentite non vuote con NotAllowedError.
// 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);
});

Endpoint di verifica con doppia credenziale

Poiché navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' }) può essere risolto con una passkey (PublicKeyCredential) o una password salvata (PasswordCredential), fornisci gestori di verifica per entrambi i tipi di credenziali:

  1. Verifica dell'asserzione WebAuthn (/api/login/verify): verifica l'asserzione rispetto alla verifica della sessione e alla chiave pubblica memorizzata. Se l'ID credenziale non viene trovato nel database, restituisci lo stato HTTP 404 in modo che il client possa inviare una notifica al gestore delle password tramite PublicKeyCredential.signalUnknownCredential().
  2. Verifica della password (/api/login/password): verifica username (cred.id) e password (cred.password) rispetto al database degli utenti e stabilisci una sessione autenticata.

Implementazione lato client

Richiesta di credenziali unificata

  1. Richiedi sia le passkey sia le password: decodifica le opzioni del server utilizzando PublicKeyCredential.parseRequestOptionsFromJSON() e chiama navigator.credentials.get() all'interno di un gestore dei clic avviato dall'utente con password: true, publicKey e uiMode: 'immediate' (senza passare signal).
  2. Verifica della filiale da parte di cred.type:
    • Se cred.type === 'public-key', serializza l'asserzione con cred.toJSON() e inviala tramite POST al tuo endpoint di verifica WebAuthn. Quando il server risponde con HTTP 404, chiama PublicKeyCredential.signalUnknownCredential({ rpId: publicKey.rpId, credentialId: encoded.id }).
    • Se cred.type === 'password', POST { username: cred.id, password: cred.password } al tuo endpoint di verifica della password.
  3. Gestisci NotAllowedError quando non esistono credenziali locali: intercetta NotAllowedError (generato immediatamente quando non esistono passkey locali o password salvate sul dispositivo o quando l'utente chiude il selettore) e passa al flusso di accesso standard (ad esempio aprendo una finestra di dialogo di accesso in linea o andando a /signin con un URL di ritorno).
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;
  }
});

Strategie di riserva

Fallback immediato della modalità UI

  • Esperienza di potenziamento progressivo: considera uiMode: 'immediate' come un potenziamento progressivo. Non dedurre il supporto dal nome o dalla versione di un browser; l'unico indicatore di supporto affidabile è il controllo di runtime capabilities.immediateGet tramite PublicKeyCredential.getClientCapabilities(). OBBLIGATORIO: controlla sempre capabilities.immediateGet prima di chiamare navigator.credentials.get({ uiMode: 'immediate' }). I browser che non riconoscono uiMode: 'immediate' ignorano la chiave del dizionario sconosciuta e aprono una finestra di dialogo modale di blocco con passkey e codice QR. Quando capabilities.immediateGet è falso o quando navigator.credentials.get() rifiuta con NotAllowedError, torna all'esperienza di accesso standard della tua applicazione (ad esempio aprendo una finestra modale di accesso incorporata o andando alla pagina di accesso dedicata con un parametro URL di ritorno), in modo che gli utenti con browser non supportati vedano il flusso di accesso esistente invariato:
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 del rilevamento delle funzionalità del client

Installa sempre 'webauthn-polyfills' e importalo nel modulo client in modo che PublicKeyCredential.getClientCapabilities() sia disponibile ogni volta che PublicKeyCredential è supportato:

import 'webauthn-polyfills';

Fallback delle credenziali della password

Il passaggio di password: true insieme a publicKey in navigator.credentials.get() consente ai browser supportati di includere le password salvate dal Gestore delle password del browser nel selettore account immediato unificato.

  • Esperienza di riserva: i browser che non supportano PasswordCredential ignorano l'opzione password: true o tornano, tramite il controllo della funzionalità capabilities.immediateGet, al modulo di accesso standard, in cui <input autocomplete="username webauthn"> e <input type="password" autocomplete="current-password"> forniscono la compilazione automatica nativa della password.

Facile fallback della serializzazione JSON

Installa sempre 'webauthn-polyfills' e importalo nel bundle client in modo che PublicKeyCredential.parseRequestOptionsFromJSON() e PublicKeyCredential.prototype.toJSON() funzionino in modo coerente nei browser che supportano PublicKeyCredential:

import 'webauthn-polyfills';

Fallback della sincronizzazione dell'API Signal

L'API WebAuthn Signal (webauthn-signals) è un potenziamento progressivo che rimuove le passkey obsolete dal gestore delle password dell'utente quando il server risponde con HTTP 404.

  • Esperienza di riserva: blocca le chiamate dietro if (PublicKeyCredential.signalUnknownCredential). Se non è supportata, ignora la chiamata di segnale e mostra un messaggio chiaro che invita l'utente ad accedere con un metodo alternativo.