Autenticazione tramite passkey lato server

Panoramica

Ecco una panoramica generale dei passaggi principali dell'autenticazione tramite passkey:

Flusso di autenticazione della passkey

  • Definisci la verifica e altre opzioni necessarie per l'autenticazione con una passkey. Inviale al client in modo da poterle passare alla chiamata di autenticazione tramite passkey (navigator.credentials.get sul web). Dopo che l'utente conferma l'autenticazione tramite passkey, la chiamata di autenticazione tramite passkey viene risolta e restituisce una credenziale (PublicKeyCredential). La credenziale contiene un'asserzione di autenticazione.
  • Verifica l'asserzione di autenticazione.
  • Se l'asserzione di autenticazione è valida, esegui l'autenticazione dell'utente.

Le sezioni seguenti illustrano le specifiche di ciascun passaggio.

Crea la sfida

In pratica, una challenge è un array di byte casuali, rappresentati come un oggetto ArrayBuffer.

// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8

Per assicurarti che la sfida soddisfi il suo scopo, devi:

  1. Assicurati che la stessa sfida non venga mai utilizzata più di una volta. Genera una nuova verifica a ogni tentativo di accesso. Ignorare la verifica dopo ogni tentativo di accesso, che sia andato a buon fine o meno. Ignora la sfida anche dopo un certo periodo di tempo. Non accettare mai la stessa sfida in una risposta più di una volta.
  2. Assicurati che la verifica sia crittograficamente sicura. Una sfida dovrebbe essere praticamente impossibile da indovinare. Per creare una verifica lato server di verifica con sicurezza crittografica, è preferibile utilizzare una libreria lato server FIDO che ritieni attendibile. Se invece crei sfide personalizzate, usa la funzionalità crittografica integrata disponibile nel tuo stack tecnico oppure cerca librerie progettate per casi d'uso crittografici. Gli esempi includono iso-crypto in Node.js o secrets in Python. In base alla specifica, la verifica deve avere una lunghezza di almeno 16 byte per essere considerata sicura.

Una volta creata una sfida, salvala nella sessione dell'utente per verificarla in seguito.

Opzioni per la richiesta di creazione di credenziali

Crea opzioni di richiesta delle credenziali come oggetto publicKeyCredentialRequestOptions.

Per farlo, utilizza la tua libreria lato server FIDO. In genere offre una funzione di utilità che può creare queste opzioni per te. SimpleWebAuthn offre, ad esempio, generateAuthenticationOptions.

publicKeyCredentialRequestOptions deve contenere tutte le informazioni necessarie per l'autenticazione tramite passkey. Passa queste informazioni alla funzione nella libreria lato server FIDO responsabile della creazione dell'oggetto publicKeyCredentialRequestOptions.

Alcuni dei campi di publicKeyCredentialRequestOptions possono essere costanti. Altre devono essere definite in modo dinamico sul server:

  • rpId: a quale ID parte soggetta a limitazioni ti aspetti che vengano associate la credenziale, ad esempio example.com. L'autenticazione avrà esito positivo solo se l'ID della parte soggetta a limitazioni fornito qui corrisponde all'ID della parte soggetta a limitazioni associato alla credenziale. Per compilare l'ID parte soggetta a limitazioni, utilizza lo stesso valore dell'ID parte soggetta a limitazioni impostato in publicKeyCredentialCreationOptions durante la registrazione delle credenziali.
  • challenge: un dato che il fornitore della passkey firmerà per dimostrare che l'utente è in possesso della passkey al momento della richiesta di autenticazione. Rivedi i dettagli in Creare la sfida.
  • allowCredentials: un array di credenziali accettabili per questa autenticazione. Passa un array vuoto per consentire all'utente di selezionare una passkey disponibile da un elenco mostrato dal browser. Per informazioni dettagliate, consulta Recupero di una verifica dal server RP e Approfondimento sulle credenziali rilevabili.
  • userVerification: indica se la verifica dell'utente tramite il blocco schermo del dispositivo è "obbligatoria", "preferita" o "scoraggiata". Consulta la sezione Recupero di una verifica dal server RP.
  • timeout: il tempo (in millisecondi) che l'utente può impiegare per completare l'autenticazione. Deve essere ragionevolmente generoso e più breve rispetto al periodo di validità di challenge. Il valore predefinito consigliato è 5 minuti, ma puoi aumentarlo fino a un massimo di 10 minuti, che rientra nell'intervallo consigliato. I timeout lunghi sono utili se prevedi che gli utenti utilizzino il flusso di lavoro ibrido, che in genere richiede un po' più di tempo. In caso di timeout dell'operazione, verrà generato un valore NotAllowedError.

Dopo aver creato publicKeyCredentialRequestOptions, invialo al client.

PublicKeyCredentialCreationOptions inviate dal server
Opzioni inviate dal server. La decodifica di challenge avviene lato client.

Codice di esempio: crea opzioni per la richiesta di credenziali

Utilizziamo la libreria SimpleWebAuthn nei nostri esempi. Qui passiamo la creazione delle opzioni per le richieste di credenziali alla relativa funzione generateAuthenticationOptions.

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} from '@simplewebauthn/server';

router.post('/signinRequest', csrfCheck, async (req, res) => {

  // Ensure you nest calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // Use the generateAuthenticationOptions function from SimpleWebAuthn
    const options = await generateAuthenticationOptions({
      rpID: process.env.HOSTNAME,
      allowCredentials: [],
    });
    // Save the challenge in the user session
    req.session.challenge = options.challenge;

    return res.json(options);
  } catch (e) {
    console.error(e);
    return res.status(400).json({ error: e.message });
  }
});

Verifica e accedi all'utente

Quando navigator.credentials.get viene risolto correttamente sul client, restituisce un oggetto PublicKeyCredential.

Oggetto PublicKeyCredential inviato dal server
navigator.credentials.get restituisce PublicKeyCredential.

Il response è un AuthenticatorAssertionResponse. Rappresenta la risposta del provider di passkey alle istruzioni del client per creare ciò che è necessario per provare ad autenticarsi con una passkey nella parte soggetta a limitazioni. Contiene:

  • response.authenticatorDataeresponse.clientDataJSON, ad esempio nel passaggio per la registrazione della passkey.
  • response.signature che contiene una firma sopra questi valori.

Invia l'oggetto PublicKeyCredential al server.

Sul server, procedi nel seguente modo:

Schema del database
Schema di database suggerito. Scopri di più su questo design nella pagina Registrazione tramite passkey lato server.
  • Raccogli le informazioni necessarie per verificare l'asserzione e autenticare l'utente:
    • Ricevi la richiesta di verifica prevista che hai archiviato nella sessione quando hai generato le opzioni di autenticazione.
    • Recupera l'origine e l'ID RP previsti.
    • Individua nel database l'identità dell'utente. Nel caso di credenziali rilevabili, non sai chi sia l'utente che effettua una richiesta di autenticazione. Per scoprirlo, hai due opzioni:
      • Opzione 1: utilizza response.userHandle nell'oggetto PublicKeyCredential. Nella tabella Utenti, cerca il passkey_user_id che corrisponde a userHandle.
      • Opzione 2: utilizza la credenziale id presente nell'oggetto PublicKeyCredential. Nella tabella Credenziali chiave pubblica, cerca la credenziale id che corrisponde alla credenziale id presente nell'oggetto PublicKeyCredential. Cerca quindi l'utente corrispondente utilizzando la chiave esterna passkey_user_id nella tabella Utenti.
    • Trova nel database le informazioni sulle credenziali della chiave pubblica che corrispondono all'asserzione di autenticazione che hai ricevuto. Per farlo, nella tabella Credenziali chiave pubblica, cerca la credenziale id che corrisponde alla credenziale idpresente nell'oggetto PublicKeyCredential.
  • Verifica l'asserzione di autenticazione. Passa questo passaggio di verifica alla tua libreria lato server FIDO, che in genere ti offrirà una funzione di utilità per questo scopo. SimpleWebAuthn offre, ad esempio, verifyAuthenticationResponse. Scopri cosa sta succedendo in dettaglio nell'Appendice: verifica della risposta di autenticazione.

  • Elimina la verifica indipendentemente dal fatto che la verifica sia andata a buon fine o meno, per evitare attacchi che si ripetono.

  • Eseguire l'accesso all'utente. Se la verifica è andata a buon fine, aggiorna le informazioni della sessione per contrassegnare l'utente come che ha eseguito l'accesso. Potresti anche voler restituire un oggetto user al client, in modo che il frontend possa utilizzare le informazioni associate all'utente che ha appena eseguito l'accesso.

Codice di esempio: verifica l'utente e consenti l'accesso

Utilizziamo la libreria SimpleWebAuthn nei nostri esempi. Qui passiamo la verifica della risposta di autenticazione alla sua funzione verifyAuthenticationResponse.

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';

router.post('/signinResponse', csrfCheck, async (req, res) => {
  const response = req.body;
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;

  // Ensure you nest verification function calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // Find the credential stored to the database by the credential ID
    const cred = Credentials.findById(response.id);
    if (!cred) {
      throw new Error('Credential not found.');
    }
    // Find the user - Here alternatively we could look up the user directly
    // in the Users table via userHandle
    const user = Users.findByPasskeyUserId(cred.passkey_user_id);
    if (!user) {
      throw new Error('User not found.');
    }
    // Base64URL decode some values
    const authenticator = {
      credentialPublicKey: isoBase64URL.toBuffer(cred.publicKey),
      credentialID: isoBase64URL.toBuffer(cred.id),
      transports: cred.transports,
    };

    // Verify the credential
    const { verified, authenticationInfo } = await verifyAuthenticationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      authenticator,
      requireUserVerification: false,
    });

    if (!verified) {
      throw new Error('User verification failed.');
    }

    // Kill the challenge for this session.
    delete req.session.challenge;

    req.session.username = user.username;
    req.session['signed-in'] = 'yes';

    return res.json(user);
  } catch (e) {
    delete req.session.challenge;

    console.error(e);
    return res.status(400).json({ error: e.message });
  }
});

Appendice: verifica della risposta di autenticazione

La verifica della risposta di autenticazione prevede i seguenti controlli:

  • Assicurati che l'ID della parte soggetta a limitazioni corrisponda al tuo sito.
  • Assicurati che l'origine della richiesta corrisponda a quella di accesso del tuo sito. Per le app per Android, consulta l'articolo Verificare l'origine.
  • Verifica che il dispositivo sia stato in grado di suggerire la sfida che hai proposto.
  • Verifica che durante l'autenticazione l'utente abbia seguito i requisiti da te stabiliti in qualità di parte soggetta a limitazioni. Se richiedi la verifica dell'utente, assicurati che il flag uv (utente verificato) in authenticatorData sia true. Verifica che il flag up (utente presente) in authenticatorData sia true, poiché la presenza dell'utente è sempre obbligatoria per le passkey.
  • Verifica la firma. Per verificare la firma, sono necessari:
    • La firma, che rappresenta la sfida firmata: response.signature
    • La chiave pubblica con cui verificare la firma.
    • I dati originali firmati. Si tratta dei dati di cui deve essere verificata la firma.
    • L'algoritmo crittografico utilizzato per creare la firma.

Per saperne di più su questi passaggi, consulta il codice sorgente di SimpleWebAuthn per verifyAuthenticationResponse o consulta l'elenco completo delle verifiche nella specifica.