Registrazione tramite passkey lato server

Panoramica

Di seguito è riportata una panoramica generale dei passaggi principali della registrazione tramite passkey:

Flusso di registrazione delle passkey

  • Definisci le opzioni per creare una passkey. Inviali al client in modo da poterli passare alla chiamata di creazione della passkey: la chiamata API WebAuthn navigator.credentials.create sul web e credentialManager.createCredential su Android. Dopo che l'utente conferma la creazione della passkey, la chiamata di creazione della passkey viene risolta e restituisce una credenziale PublicKeyCredential.
  • Verifica la credenziale e memorizzala sul server.

Le sezioni seguenti approfondiscono le specifiche di ogni passaggio.

Opzioni per la creazione di credenziali

Il primo passaggio da eseguire sul server è creare un oggetto PublicKeyCredentialCreationOptions.

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, generateRegistrationOptions.

PublicKeyCredentialCreationOptions deve includere tutto il necessario per la creazione della passkey: informazioni sull'utente, sulla parte soggetta a limitazioni e una configurazione per le proprietà della credenziale che stai creando. Una volta definiti tutti questi elementi, passali come necessario alla funzione nella tua libreria lato server FIDO responsabile della creazione dell'oggetto PublicKeyCredentialCreationOptions.

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

  • rpId: per compilare l'ID RP sul server, utilizza funzioni o variabili lato server che ti forniscono il nome host della tua applicazione web, ad esempio example.com.
  • user.name e user.displayName:per compilare questi campi, utilizza le informazioni relative alla sessione dell'utente che ha eseguito l'accesso (o i dati del nuovo account utente, se l'utente sta creando una passkey al momento della registrazione). user.name è in genere un indirizzo email ed è univoco per la parte soggetta a limitazioni. user.displayName è un nome semplice. Tieni presente che non tutte le piattaforme utilizzeranno displayName.
  • user.id: una stringa univoca casuale generata al momento della creazione dell'account. Deve essere permanente, a differenza di un nome utente che potrebbe essere modificabile. L'ID utente identifica un account, ma non deve contenere informazioni che consentono l'identificazione personale (PII). È probabile che tu abbia già un ID utente nel tuo sistema, ma se necessario, creane uno specifico per le passkey in modo che non contenga PII.
  • excludeCredentials: un elenco delle credenziali esistenti. ID per evitare la duplicazione di una passkey del fornitore di passkey. Per compilare questo campo, cerca nel database le credenziali esistenti di questo utente. Esamina i dettagli nell'articolo Impedire la creazione di una nuova passkey se ne esiste già una.
  • challenge: per la registrazione delle credenziali, la verifica non è pertinente, a meno che non utilizzi l'attestazione, una tecnica più avanzata per verificare l'identità di un provider di passkey e i dati che emette. Tuttavia, anche se non utilizzi l'attestazione, la verifica è comunque un campo obbligatorio. In questo caso, per semplicità, puoi impostare questa verifica su un singolo 0. Le istruzioni per creare una richiesta di verifica sicura per l'autenticazione sono disponibili in Autenticazione tramite passkey lato server.

Codifica e decodifica

PublicKeyCredentialCreationOptions inviate dal server
PublicKeyCredentialCreationOptions inviato dal server. challenge, user.id e excludeCredentials.credentials devono essere codificati lato server in base64URL, in modo che PublicKeyCredentialCreationOptions possa essere pubblicato tramite HTTPS.

PublicKeyCredentialCreationOptions include campi ArrayBuffer, che non sono quindi supportati da JSON.stringify(). Ciò significa che, al momento, per pubblicare PublicKeyCredentialCreationOptions tramite HTTPS, alcuni campi devono essere codificati manualmente sul server utilizzando base64URL e quindi decodificati sul client.

  • Sul server, la codifica e la decodifica, in genere, vengono gestite dalla libreria FIDO lato server.
  • Sul client, la codifica e la decodifica devono essere eseguite manualmente al momento. In futuro diventerà più semplice: sarà disponibile un metodo per convertire le opzioni in formato JSON in PublicKeyCredentialCreationOptions. Controlla lo stato dell'implementazione in Chrome.

Codice di esempio: opzioni per la creazione delle credenziali

Nei nostri esempi utilizziamo la libreria SimpleWebAuthn. Qui, passiamo la creazione delle opzioni per le credenziali della chiave pubblica alla sua funzione generateRegistrationOptions.

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

router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
  const { user } = res.locals;
  // 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 {
    // `excludeCredentials` prevents users from re-registering existing
    // credentials for a given passkey provider
    const excludeCredentials = [];
    const credentials = Credentials.findByUserId(user.id);
    if (credentials.length > 0) {
      for (const cred of credentials) {
        excludeCredentials.push({
          id: isoBase64URL.toBuffer(cred.id),
          type: 'public-key',
          transports: cred.transports,
        });
      }
    }

    // Generate registration options for WebAuthn create
    const options = generateRegistrationOptions({
      rpName: process.env.RP_NAME,
      rpID: process.env.HOSTNAME,
      userID: user.id,
      userName: user.username,
      userDisplayName: user.displayName || '',
      attestationType: 'none',
      excludeCredentials,
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        requireResidentKey: true
      },
    });

    // Keep the challenge in the session
    req.session.challenge = options.challenge;

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

Archivia la chiave pubblica

PublicKeyCredentialCreationOptions inviate dal server
navigator.credentials.create restituisce un oggetto PublicKeyCredential.

Quando navigator.credentials.create si risolve correttamente sul client, significa che una passkey è stata creata. Viene restituito un oggetto PublicKeyCredential.

L'oggetto PublicKeyCredential contiene un oggetto AuthenticatorAttestationResponse, che rappresenta la risposta del fornitore di passkey all'istruzione del cliente di creare una passkey. Contiene informazioni sulle nuove credenziali necessarie come parte soggetta a limitazioni per autenticare l'utente in un secondo momento. Scopri di più su AuthenticatorAttestationResponse nell'Appendice: AuthenticatorAttestationResponse.

Invia l'oggetto PublicKeyCredential al server. Dopo averlo ricevuto, verificalo.

Consegna questo passaggio di verifica alla tua libreria lato server FIDO. A questo scopo, offre in genere una funzione di utilità. SimpleWebAuthn offre, ad esempio, verifyRegistrationResponse. Scopri che cosa accade dietro le quinte nell'Appendice: verifica della risposta alla registrazione.

Una volta completata la verifica, memorizza le informazioni sulle credenziali nel database in modo che l'utente possa autenticarsi in un secondo momento con la passkey associata alla credenziale.

Usa una tabella dedicata per le credenziali della chiave pubblica associate alle passkey. Un utente può avere una sola password, ma può avere più passkey, ad esempio una passkey sincronizzata tramite il portachiavi iCloud di Apple e una tramite Gestore delle password di Google.

Di seguito è riportato uno schema di esempio che puoi utilizzare per memorizzare le informazioni sulle credenziali:

Schema del database per le passkey

  • Tabella Utenti:
      .
    • user_id: l'ID utente principale. Un ID casuale univoco e permanente dell'utente. Utilizzala come chiave primaria per la tabella Utenti.
    • username Un nome utente definito dall'utente, potenzialmente modificabile.
    • passkey_user_id: l'ID utente privo di PII specifico per la passkey, rappresentato da user.id nelle opzioni di registrazione. Quando in un secondo momento l'utente tenterà di eseguire l'autenticazione, l'autenticatore renderà disponibile passkey_user_id nella sua risposta di autenticazione in userHandle. Ti consigliamo di non impostare passkey_user_id come chiave primaria. Le chiavi primarie tendono a diventare PII de facto nei sistemi, perché sono ampiamente utilizzate.
  • Tabella delle credenziali della chiave pubblica:
      .
    • id: ID credenziale. Utilizzala come chiave primaria per la tabella Credenziali chiave pubblica.
    • public_key: chiave pubblica della credenziale.
    • passkey_user_id: utilizzala come chiave esterna per stabilire un collegamento con la tabella Utenti.
    • backed_up: viene effettuato il backup di una passkey se è sincronizzata dal fornitore della passkey. La memorizzazione dello stato del backup è utile se vuoi prendere in considerazione la possibilità di perdere le password in futuro per gli utenti che detengono le passkey backed_up. Puoi verificare se il backup della passkey viene eseguito esaminando i flag in authenticatorData o utilizzando una funzionalità della libreria lato server FIDO, in genere disponibile per consentirti di accedere facilmente a queste informazioni. Memorizzare l'idoneità al backup può essere utile per rispondere alle potenziali richieste degli utenti.
    • name: facoltativamente, un nome visualizzato della credenziale per consentire agli utenti di assegnare nomi personalizzati alle credenziali.
    • transports: una serie di trasporti. L'archiviazione dei trasporti è utile per l'autenticazione dell'utente. Quando i trasporti sono disponibili, il browser può comportarsi di conseguenza e visualizzare una UI corrispondente al trasporto utilizzato dal provider di passkey per comunicare con i client, in particolare per i casi d'uso di riautenticazione in cui il campo allowCredentials non è vuoto.

Possono essere utili altre informazioni da memorizzare ai fini dell'esperienza utente, tra cui elementi come il fornitore di passkey, l'ora di creazione delle credenziali e l'ora dell'ultimo utilizzo. Scopri di più nella pagina Design dell'interfaccia utente delle passkey.

Codice di esempio: memorizza la credenziale

Nei nostri esempi utilizziamo la libreria SimpleWebAuthn. Qui, passiamo la verifica della risposta della registrazione alla relativa funzione verifyRegistrationResponse.

import { isoBase64URL } from '@simplewebauthn/server/helpers';


router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;
  const response = req.body;
  // This sample code is for registering a passkey for an existing,
  // signed-in user

  // 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 {
    // Verify the credential
    const { verified, registrationInfo } = await verifyRegistrationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      requireUserVerification: false,
    });

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

    const { credentialPublicKey, credentialID } = registrationInfo;

    // Existing, signed-in user
    const { user } = res.locals;
    
    // Save the credential
    await Credentials.update({
      id: base64CredentialID,
      publicKey: base64PublicKey,
      // Optional: set the platform as a default name for the credential
      // (example: "Pixel 7")
      name: req.useragent.platform, 
      transports: response.response.transports,
      passkey_user_id: user.passkey_user_id,
      backed_up: registrationInfo.credentialBackedUp
    });

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

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

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

Appendice: AuthenticatorAttestationResponse

AuthenticatorAttestationResponse contiene due oggetti importanti:

  • response.clientDataJSON è una versione JSON dei dati client, che sul web rappresentano i dati visti dal browser. Contiene l'origine RP, la sfida e androidPackageName se il client è un'app per Android. In qualità di parte soggetta a limitazioni, la lettura clientDataJSON ti consente di accedere alle informazioni visualizzate dal browser al momento della richiesta create.
  • response.attestationObjectcontiene due informazioni:
    • attestationStatement, che non è pertinente, a meno che non utilizzi l'attestazione.
    • authenticatorData: dati visualizzati dal provider di passkey. In qualità di parte soggetta a limitazioni, la lettura authenticatorData ti consente di accedere ai dati visualizzati dal fornitore di passkey e restituiti al momento della richiesta create.

authenticatorDatacontiene informazioni essenziali sulla credenziale della chiave pubblica associata alla passkey appena creata:

  • La credenziale della chiave pubblica e un ID credenziale univoco.
  • L'ID parte soggetta a limitazioni associato alla credenziale.
  • Flag che descrivono lo stato dell'utente al momento della creazione della passkey: se un utente era effettivamente presente e se l'utente è stato verificato (vedi userVerification).
  • AAGUID, che identifica il provider di passkey. La visualizzazione del fornitore di passkey può essere utile per i tuoi utenti, soprattutto se hanno una passkey registrata per il tuo servizio su più fornitori di passkey.

Anche se authenticatorData è nidificato all'interno di attestationObject, le informazioni che contiene sono necessarie per l'implementazione della passkey, indipendentemente dal fatto che utilizzi o meno l'attestazione. authenticatorDataè codificato e contiene campi codificati in formato binario. In genere la libreria lato server gestisce l'analisi e la decodifica. Se non utilizzi una libreria lato server, valuta la possibilità di utilizzare il lato client getAuthenticatorData() per evitare di eseguire l'analisi e la decodifica del lavoro lato server.

Appendice: verifica della risposta alla registrazione

Di base, la verifica della risposta alla registrazione prevede i seguenti controlli:

  • Assicurati che l'ID parte soggetta a limitazioni corrisponda al tuo sito.
  • Assicurati che l'origine della richiesta sia un'origine prevista per il tuo sito (URL del sito principale, app per Android).
  • Se richiedi la verifica dell'utente, assicurati che il flag di verifica dell'utente authenticatorData.uv sia true. Controlla che il flag della presenza dell'utente authenticatorData.up sia true, poiché la presenza dell'utente è sempre obbligatoria per le passkey.
  • Verifica che il cliente sia stato in grado di rispondere alla sfida che gli hai dato. Se non utilizzi l'attestazione, questo controllo non è importante. Tuttavia, l'implementazione di questo controllo è una best practice in quanto garantisce che il codice sia pronto se decidi di utilizzare l'attestazione in futuro.
  • Assicurati che l'ID credenziali non sia ancora registrato per nessun utente.
  • Verifica che l'algoritmo utilizzato dal provider di passkey per creare la credenziale sia un algoritmo elencato da te (in ogni campo alg di publicKeyCredentialCreationOptions.pubKeyCredParams, che in genere viene definito all'interno della libreria lato server e non puoi vederti). In questo modo ti assicuri che gli utenti possano registrarsi solo con gli algoritmi che hai scelto di consentire.

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

Successivo

Autenticazione tramite passkey lato server