Autenticazione tramite passkey lato server

Panoramica

Di seguito è riportata una panoramica generale dei passaggi chiave dell'autenticazione tramite passkey:

Flusso di autenticazione con passkey

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

Le sezioni seguenti approfondiscono le specifiche di ogni passaggio.

Crea la sfida

In pratica, una sfida è 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 verifica non venga mai utilizzata più di una volta. Genera una nuova verifica a ogni tentativo di accesso. Ignora la verifica dopo ogni tentativo di accesso, riuscito o non riuscito. Ignora la sfida anche dopo un determinato 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 con crittografia sicura, ti consigliamo di utilizzare una libreria FIDO lato server attendibile. Se invece crei personalmente le verifiche, utilizza la funzionalità crittografica integrata disponibile nel tuo stack tecnico o cerca librerie progettate per casi d'uso crittografici. Alcuni esempi sono iso-crypto in Node.js o secret in Python. In base alla specifica, la verifica deve essere lunga almeno 16 byte per essere considerata sicura.

Una volta creata una verifica, salvala nella sessione dell'utente per verificarla in un secondo momento.

Crea opzioni per la richiesta di credenziali

Crea opzioni per la richiesta di credenziali come oggetto publicKeyCredentialRequestOptions.

A questo scopo, affidati alla libreria FIDO lato server. In genere offre una funzione di utilità in grado di 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 tua libreria lato server FIDO responsabile della creazione dell'oggetto publicKeyCredentialRequestOptions.

Parte di publicKeyCredentialRequestOptions campi possono essere costanti. Altre devono essere definite dinamicamente sul server:

  • rpId: l'ID parte soggetta a limitazioni a cui prevedi che venga associata la credenziale, ad esempio example.com. L'autenticazione avrà esito positivo solo se l'ID parte soggetta a limitazioni qui fornito corrisponde all'ID parte soggetta a limitazioni associato alla credenziale. Per compilare l'ID parte soggetta a limitazioni, usa lo stesso valore dell'ID parte soggetta a limitazioni impostato in publicKeyCredentialCreationOptions durante la registrazione delle credenziali.
  • challenge: un dato che il fornitore di passkey firmerà per dimostrare che l'utente è in possesso della passkey al momento della richiesta di autenticazione. Esamina i dettagli in Creare la sfida.
  • allowCredentials: un array di credenziali accettabili per questa autenticazione. Trasmetti un array vuoto per consentire all'utente di selezionare una passkey disponibile da un elenco mostrato dal browser. Per maggiori dettagli, consulta Recupera una sfida dal server RP e Approfondimento sulle credenziali rilevabili.
  • userVerification: indica se la verifica dell'utente tramite il blocco schermo del dispositivo è "obbligatoria" o "preferita" o "sconsigliato". Consulta Recuperare 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 dell'intera durata dell'challenge. Il valore predefinito consigliato è 5 minuti, ma puoi aumentarlo fino a 10 minuti, entro i limiti dell'intervallo consigliato. I timeout lunghi hanno senso se prevedi che gli utenti utilizzino il flusso di lavoro ibrido, che in genere richiede più tempo. Se l'operazione scade, viene restituito un NotAllowedError.

Dopo aver creato publicKeyCredentialRequestOptions, invialo al client.

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

Codice di esempio: opzioni per la creazione di richieste di credenziali

Nei nostri esempi utilizziamo la libreria SimpleWebAuthn. In questo caso, la creazione delle opzioni per le richieste di credenziali viene trasferita 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.

response è una AuthenticatorAssertionResponse. Rappresenta la risposta del fornitore di passkey all'istruzione del client per creare gli elementi necessari per provare ad eseguire l'autenticazione con una passkey nella parte soggetta a limitazioni. Contiene:

  • response.authenticatorDataeresponse.clientDataJSON, ad esempio al passaggio di registrazione della passkey.
  • response.signature che contiene una firma su questi valori.

Invia l'oggetto PublicKeyCredential al server.

Sul server, procedi nel seguente modo:

Schema del database
Schema del database suggerito. Scopri di più su questo design nella pagina Registrazione passkey lato server.
  • Raccogli le informazioni necessarie per verificare l'asserzione e autenticare l'utente:
      .
    • Ottieni la verifica prevista che hai memorizzato nella sessione quando hai generato le opzioni di autenticazione.
    • Ottieni i valori origin e ID RP previsti.
    • Individua nel database chi è l'utente. Nel caso di credenziali rilevabili, non conosci 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 valore 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 corrispondente alla credenziale id presente nell'oggetto PublicKeyCredential. Quindi cerca l'utente corrispondente utilizzando la chiave esterna passkey_user_id nella tabella Utenti.
    • Trova nel tuo 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 corrispondente a quella idpresente nell'oggetto PublicKeyCredential.
  • Verifica l'asserzione di autenticazione. Trasferisci questo passaggio di verifica alla tua libreria lato server FIDO, che in genere offre una funzione di utilità a questo scopo. SimpleWebAuthn offre, ad esempio, verifyAuthenticationResponse. Scopri che cosa accade dietro le quinte nell'Appendice: verifica della risposta di autenticazione.

  • Elimina la verifica indipendentemente dal fatto che la verifica abbia esito positivo o meno, per evitare attacchi di ripetizione.

  • Esegui l'accesso dell'utente. Se la verifica ha avuto esito positivo, aggiorna le informazioni della sessione per contrassegnare l'utente come connesso. 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 e accedi all'utente

Nei nostri esempi utilizziamo la libreria SimpleWebAuthn. 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 parte soggetta a limitazioni corrisponda al tuo sito.
  • Assicurati che l'origine della richiesta corrisponda all'origine di accesso del sito. Per le app per Android, consulta l'articolo Verificare l'origine.
  • Verifica che il dispositivo sia stato in grado di rispondere alla sfida che hai dato.
  • Verifica che durante l'autenticazione l'utente abbia rispettato i requisiti che richiedi come parte soggetta a limitazioni. Se richiedi la verifica utente, assicurati che il flag uv (utente verificato) in authenticatorData sia true. Controlla 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, devi avere:
    • La firma, ovvero la richiesta di verifica firmata: response.signature
    • La chiave pubblica con cui verificare la firma.
    • I dati originali firmati. Si tratta dei dati di cui verificare la firma.
    • L'algoritmo crittografico utilizzato per creare la firma.
di Gemini Advanced.

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