Proteggi il tuo sito con l'autenticazione a due fattori con un token di sicurezza (WebAuthn)

1. Cosa devi creare

Inizierai con un'applicazione web di base che supporta l'accesso basato su password.

Dopodiché aggiungerai il supporto per l'autenticazione a due fattori tramite un token di sicurezza, basato su WebAuthn. A tal fine, implementerai quanto segue:

  • Un modo per consentire a un utente di registrare una credenziale WebAuthn.
  • Un flusso di autenticazione a due fattori in cui all'utente viene richiesto il secondo fattore (una credenziale WebAuthn) se ne ha registrato uno.
  • Un'interfaccia di gestione delle credenziali: un elenco di credenziali che consente agli utenti di rinominare ed eliminare le credenziali.

16ce77744061c5f7.png

Dai un'occhiata all'app web terminata e prova.

2. Informazioni su WebAuthn

Nozioni di base su WebAuthn

Perché WebAuthn?

Il phishing è un enorme problema di sicurezza sul Web: la maggior parte delle violazioni dell'account sfrutta password deboli o rubate che vengono riutilizzate nei vari siti. La risposta collettiva di questo problema è stata l'autenticazione a più fattori, ma le implementazioni sono frammentate e molte non affrontano adeguatamente il phishing.

L'API Web Authentication, o WebAuthn, è un protocollo standardizzato resistente al phishing che può essere usato da qualsiasi applicazione web.

Come funziona

Fonte: webauthn.guide

WebAuthn consente ai server di registrare e autenticare gli utenti utilizzando la crittografia della chiave pubblica anziché una password. I siti web possono creare una credenziale, costituita da una coppia di chiavi pubblica-privata.

  • La chiave privata viene archiviata in modo sicuro sul dispositivo dell'utente.
  • La chiave pubblica e l'ID della credenziale generato in modo casuale vengono inviati al server per l'archiviazione.

La chiave pubblica viene utilizzata dal server per dimostrare l'identità dell'utente. Non è segreto, perché è inutile senza la chiave privata corrispondente.

Vantaggi

WebAuthn offre due vantaggi principali:

  • Nessun segreto condiviso: il server non memorizza alcun secret. Questo rende i database meno interessanti per gli hacker, perché le chiavi pubbliche non sono utili per loro.
  • Credenziali con ambito: una credenziale registrata per site.example non può essere utilizzata su evil-site.example. In questo modo WebAuthn è a prova di phishing.

Casi d'uso

Un caso d'uso per WebAuthn è l'autenticazione a due fattori con un token di sicurezza. Ciò può essere particolarmente pertinente per le applicazioni web aziendali.

Supporto del browser

È scritto da W3C e FIDO, con la partecipazione di Google, Mozilla, Microsoft, Yubico e altri.

Glossario

  • Authenticator: un'entità software o hardware che può registrare un utente e in seguito rivendicarne la proprietà delle credenziali registrate. Esistono due tipi di autenticatori:
  • Autenticazione in roaming: un autenticatore utilizzabile con qualsiasi dispositivo da cui l'utente sta tentando di accedere. Esempio: un token di sicurezza USB, uno smartphone.
  • Platform Authenticator: un autenticatore integrato nel dispositivo di un utente. Esempio: Touch ID di Apple.
  • Credenziali: la coppia di chiavi pubblica-privata
  • Affidabile: il (server per) il sito web che sta tentando di autenticare l'utente
  • Server FIDO: il server utilizzato per l'autenticazione. FIDO è una famiglia di protocolli sviluppati dall'alleanza FIDO, uno dei quali è WebAuthn.

In questo workshop, utilizzeremo un autenticatore di roaming.

3. Prima di iniziare

Che cosa ti serve

Per completare questo codelab, avrai bisogno di:

  • Una conoscenza di base di WebAuthn.
  • Conoscenza di base di JavaScript e HTML.
  • Un browser aggiornato che supporta WebAuthn.
  • Un token di sicurezza conforme a U2F.

Puoi usare uno dei seguenti token come token di sicurezza:

  • Un telefono Android con Android>=7 (Nougat) che esegue Chrome. In questo caso, avrai anche bisogno di un computer Windows, macOS o Chrome OS con Bluetooth funzionante.
  • Una chiave USB, ad esempio YubiKey.

6539dc7ffec2538c.png

Fonte: https://www.yubico.com/products/security-key/

dd56e2cfe0f7ced2.png

Obiettivi didattici

Imparerai ✅

  • Come registrare e utilizzare un token di sicurezza come secondo fattore dell'autenticazione WebAuthn.
  • Come rendere questo processo facile da usare.

Non imparerai ❌

  • Come creare un server FIDO, il server utilizzato per l'autenticazione. Questo va bene perché, in genere, come applicazione web o sviluppatore del sito, dovresti fare affidamento sulle implementazioni del server FIDO esistenti. Assicurati sempre di verificare la funzionalità e la qualità delle implementazioni del server su cui fai affidamento. In questo codelab, il server FIDO utilizza SimpleWebAuthn. Per altre opzioni, visita la pagina ufficiale di FIDO Alliance. Per le librerie open source, visita la pagina webauthn.io o AwesomeWebAuthn.

Disclaimer

L'utente deve inserire una password per accedere. Tuttavia, per semplicità in questo codelab, la password non viene archiviata né selezionata. In un'applicazione reale, devi verificare che sia lato lato server corretto.

In questo codelab sono implementati i controlli di sicurezza di base, come i controlli CSRF, la convalida delle sessioni e l'igienizzazione degli input. Tuttavia, molte misure di sicurezza non lo sono, ad esempio non esiste un limite di input per le password che impediscono gli attacchi di forza bruta. Non è importante qui perché le password non sono memorizzate, ma assicurati di non utilizzare questo codice così com'è in produzione.

4. Configura l'autenticatore

Se utilizzi un telefono Android come autenticatore

  • Assicurati che Chrome sia aggiornato sia sul desktop che sul telefono.
  • Sul desktop e sul telefono, apri Chrome e accedi con lo stesso profilo<br> del profilo che vuoi utilizzare per il workshop.
  • Attiva la sincronizzazione per questo profilo sul desktop e sul telefono. Utilizza chrome://settings/syncSetup per eseguire questa operazione.
  • Attiva il Bluetooth sia sul desktop sia sul telefono.
  • Apri Chrome webauthn.io sul computer desktop su cui è stato eseguito l'accesso con lo stesso profilo.
  • Inserisci un nome utente semplice. Lascia i valori Type (Tipo attestatore) e Authenticator sui valori None e Non specificato (valore predefinito). Fai clic su Registrati.

6b49ff0298f5a0af.png

  • Si aprirà una finestra del browser, in cui ti verrà chiesto di verificare la tua identità. Seleziona il tuo telefono nell'elenco.

ffebe58ac826eaf2.png 852de328fcd4eb42.png

  • Sul tuo telefono, dovresti ricevere una notifica chiamata Verifica la tua identità. Toccala.
  • Sul telefono ti verrà chiesto il codice PIN del telefono (o di toccare il sensore di impronte digitali). Inseriscilo.
  • Su webauthn.io sul tuo desktop, dovrebbe essere visualizzato un indicatore "Success".

fc0acf00a4d412fa.png

  • Su webauthn.io sul tuo desktop, fai clic sul pulsante di accesso.
  • Anche in questo caso, dovrebbe aprirsi una finestra del browser; seleziona il tuo telefono nell'elenco.
  • Sul telefono, tocca la notifica che viene visualizzata e inserisci il PIN (o tocca il sensore di impronte digitali).
  • webauthn.io dovrebbe dirti che hai eseguito l'accesso. Il tuo telefono funziona correttamente come token di sicurezza ed è tutto pronto per il workshop.

Se utilizzi un token di sicurezza USB come autenticatore

  • In Chrome per desktop, apri webauthn.io.
  • Inserisci un nome utente semplice. Lascia i valori Type (Tipo attestatore) e Authenticator sui valori None e Non specificato (valore predefinito). Fai clic su Registrati.
  • Si aprirà una finestra del browser, in cui ti verrà chiesto di verificare la tua identità. Seleziona Token di sicurezza USB nell'elenco.

ffebe58ac826eaf2.png 9fe75f04e43da035.png

  • Inserisci il token di sicurezza nel desktop e toccalo.

923d5adb8aa8286c.png

  • Su webauthn.io sul tuo desktop, dovrebbe essere visualizzato un indicatore "Success".

fc0acf00a4d412fa.png

  • Su webauthn.io sul desktop, fai clic sul pulsante Login (Accedi).
  • Dovrebbe aprirsi di nuovo una finestra del browser; seleziona Token di sicurezza USB nell'elenco.
  • Tocca il tasto.
  • Webauthn.io dovrebbe comunicarti che hai eseguito l'accesso. Il tuo token di sicurezza USB funziona correttamente; è tutto pronto per l'officina.

7e1c0bb19c9f3043.png

5. Configura

In questo codelab, utilizzerai Glitch, un editor di codice online che esegue il deployment del codice automaticamente e istantaneamente.

Crea un fork del codice di avvio

Apri il progetto di avvio.

Fai clic sul pulsante Remix.

Viene creata una copia del codice di avvio. Ora hai il tuo codice da modificare. La tua forchetta (chiamata "remix" in Glitch) è il posto in cui svolgerai tutto il lavoro per questo codelab.

cf2b9f552c9809b6.png

Esplora il codice di avvio

Esplora il codice di avvio che hai appena creato.

Tieni presente che sotto libs è già stata fornita una biblioteca denominata auth.js. È una libreria personalizzata che si occupa della logica di autenticazione lato server. Utilizza la libreria fido come dipendenza.

6. Implementare la registrazione delle credenziali

Implementare la registrazione delle credenziali

La prima cosa di cui abbiamo bisogno per configurare l'autenticazione a due fattori con un token di sicurezza è consentire all'utente di creare una credenziale.

Innanzitutto, aggiungiamo una funzione che esegua questa operazione nel nostro codice lato client.

In public/auth.client.js, tieni presente che esiste una funzione chiamata registerCredential()che non fa ancora nulla. Aggiungi il seguente codice:

async function registerCredential() {
  // Fetch the credential creation options from the backend
  const credentialCreationOptionsFromServer = await _fetch(
    "/auth/credential-options",
    "POST"
  );
  // Decode the credential creation options
  const credentialCreationOptions = decodeServerOptions(
    credentialCreationOptionsFromServer
  );
  // Create a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
  const credential = await navigator.credentials.create({
    publicKey: {
      ...credentialCreationOptions,
    }
  });
  // Encode the newly created credential to send it to the backend
  const encodedCredential = encodeCredential(credential);
  // Send the encoded credential to the backend for storage
  return await _fetch("/auth/credential", "POST", encodedCredential);
}

Tieni presente che questa funzione è già stata esportata per te.

Ecco cosa fa registerCredential:

  • Recupera le opzioni di creazione delle credenziali dal server (/auth/credential-options)
  • Poiché le opzioni del server vengono codificate di nuovo, utilizza la funzione di utilità decodeServerOptions per decodificarle.
  • Crea una credenziale chiamando l'API web navigator.credential.create. Quando viene chiamato navigator.credential.create, il browser prende il controllo e chiede all'utente di scegliere un token di sicurezza.
  • Decodifica la credenziale appena creata
  • Registra la nuova credenziale lato server inviando una richiesta a /auth/credential contenente la credenziale codificata.

A parte questo, dai un'occhiata al codice del server

registerCredential() effettua due chiamate al server, quindi diamo un'occhiata a cosa sta succedendo nel backend.

Opzioni di creazione delle credenziali

Quando il client invia una richiesta a (/auth/credential-options), il server genera un oggetto opzioni e lo invia al client.

Questo oggetto viene quindi utilizzato dal client nella chiamata di creazione delle credenziali effettiva:

navigator.credentials.create({
    publicKey: {
    // Options generated server-side
    ...credentialCreationOptions
// ...
}

Quindi, cosa c'è in questa credentialCreationOptions che alla fine viene utilizzata nel registerCredential lato client che hai implementato nel passaggio precedente?

Dai un'occhiata al codice del server in router.post("/credential-options", ...).

Non esaminiamo tutte le singole proprietà, ma ecco alcune interessanti cose che puoi vedere nell'oggetto opzioni del codice del server, generato utilizzando la libreria fido2 e infine restituito al client:

  • rpName e rpId descrivono l'organizzazione che si registra e autentica l'utente. Ricorda che in WebAuthn le credenziali sono limitate a un determinato dominio, che costituisce un vantaggio per la sicurezza; rpName e rpId, in questo caso, vengono utilizzati per definire l'ambito della credenziale. Un rpId valido è, ad esempio, il nome host del tuo sito. Tieni presente come vengono aggiornati automaticamente mentre forzi il progetto iniziale 🧘🏻 ♀️
  • excludeCredentials è un elenco di credenziali; non è possibile creare la nuova credenziale su un autenticatore che contiene anche una delle credenziali elencate in excludeCredentials. Nel nostro codelab, excludeCredentials contiene un elenco delle credenziali esistenti per questo utente. Con questo e user.id, ci assicuriamo che ogni credenziale creata da un utente possa essere pubblicata su un altro autenticatore (token di sicurezza). Questa è una buona prassi perché significa che se un utente ha registrato più credenziali, avrà accesso a diversi autenticatori (token di sicurezza), quindi perdere un token di sicurezza non impedirà all'utente di accedere al proprio account.
  • authenticatorSelection definisce il tipo di autenticatori che vuoi consentire nella tua applicazione web. Diamo un'occhiata più da vicino a authenticatorSelection:
    • residentKey: preferred significa che questa applicazione non impone le credenziali rilevabili sul lato client. Una credenziale lato client è un tipo speciale di credenziale che consente di autenticare un utente senza dover prima identificare l'utente. Abbiamo configurato preferred perché questo codelab è incentrato sull'implementazione di base; le credenziali rilevabili sono destinate a flussi più avanzati.
    • requireResidentKey è presente solo per la compatibilità con le versioni precedenti con WebAuthn v1.
    • userVerification: preferred indica che, se l'autenticatore supporta la verifica utente, ad esempio se si tratta di un token di sicurezza biometrico o di un token con funzionalità PIN integrata, il richiedente si occuperà di richiederlo al momento della creazione della credenziale. Se l'autenticatore non è un token di sicurezza di base, il server non richiederà la verifica dell'utente.
  • ​​pubKeyCredParam descrive, in ordine di preferenza, le proprietà crittografiche desiderate della credenziale.

Tutte queste opzioni sono decisioni che l'applicazione web deve prendere per il suo modello di sicurezza. Tieni presente che sul server queste opzioni sono definite in un singolo oggetto authSettings.

Sfida

Ecco un'altra cosa più interessante: req.session.challenge = options.challenge;.

Poiché WebAuthn è un protocollo crittografico, dipende da verifiche casuali per evitare attacchi di replica, ovvero quando un utente malintenzionato ruba un payload per riprodurre l'autenticazione, quando non è il proprietario della chiave privata che abilita l'autenticazione.

Per mitigare questo problema, viene generata una verifica sul server che verrà firmata al momento; la firma verrà confrontata con ciò che ci si aspetta. Questa operazione consente di verificare che l'utente contenga la chiave privata al momento della generazione delle credenziali.

Codice di registrazione delle credenziali

Dai un'occhiata al codice del server in router.post("/credential", ...).

Qui viene registrata la credenziale lato server.

Ma cosa sta succedendo?

Uno dei bit più degni di nota in questo codice è la chiamata di verifica, tramite fido2.verifyAttestationResponse:

  • La verifica firmata viene controllata per far sì che la credenziale sia stata creata da qualcuno che aveva effettivamente la chiave privata al momento della creazione.
  • Viene verificato anche l'ID della parte richiedente, associata alla sua origine. Questo garantisce che la credenziale sia associata a questa applicazione web (e solo a questa applicazione web).

Aggiungi questa funzionalità alla UI

Ora che la tua funzione di creazione delle credenziali, ``registerCredential(),è pronta, è disponibile per l'utente.

Stai per eseguire questa operazione dalla pagina Account, perché questa è la procedura abituale per la gestione dell'autenticazione.

Nel markup di account.html, sotto il nome utente, c'è un div vuoto finora con una classe di layout class="flex-h-between". Useremo questo div per gli elementi dell'interfaccia utente correlati alla funzionalità 2FA.

Aggiungi ino questo div:

  • Il titolo "Autenticazione a due fattori".
  • Un pulsante per creare una credenziale
 <div class="flex-h-between">
    <h3>
        Two-factor authentication
    </h3>
    <button class="create" id="registerButton" raised>
        ➕ Add a credential
    </button>
</div>

Sotto questo div, aggiungi un div credenziale di cui avremo bisogno in un secondo momento:

<div class="flex-h-between">
(HTML you've just added)
</div>
<div id="credentials"></div>

Nello script incorporato di account.html, importa la funzione appena creata e aggiungi una funzione register che la chiami, oltre a un gestore di eventi associato al pulsante che hai appena creato.

// Set up the handler for the button that registers credentials
const registerButton = document.querySelector('#registerButton');
registerButton.addEventListener('click', register);

// Register a credential
async function register() {
  let user = {};
  try {
    const user = await registerCredential();
  } catch (e) {
    // Alert the user that something went wrong
    if (Array.isArray(e)) {
      alert(
        // `msg` not `message`, this is the key's name as per the express validator API
        `Registration failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
      );
    } else {
      alert(`Registration failed. ${e}`);
    }
  }
}

Mostra le credenziali che l'utente può visualizzare

Ora che hai aggiunto la funzionalità per creare una credenziale, gli utenti hanno bisogno di un modo per vedere le credenziali aggiunte.

A questo scopo, la pagina Account è la scelta migliore.

In account.html, cerca la funzione chiamata updateCredentialList().

Aggiungi il seguente codice che effettua una chiamata di backend per recuperare tutte le credenziali registrate per l'utente che ha attualmente eseguito l'accesso e che mostra le credenziali restituite:

// Update the list that displays credentials
async function updateCredentialList() {
  // Fetch the latest credential list from the backend
  const response = await _fetch('/auth/credentials', 'GET');
  const credentials = response.credentials || [];
  // Generate the credential list as HTML and pass remove/rename functions as args
  const credentialListHtml = getCredentialListHtml(
    credentials,
    removeEl,
    renameEl
  );
  // Display the list of credentials in the DOM
  const list = document.querySelector('#credentials');
  render(credentialListHtml, list);
}    

Per ora, non ti preoccupare di removeEl e renameEl; te ne occuperemo più avanti in questo codelab.

Aggiungi una chiamata a updateCredentialList all'inizio dello script in linea, entro account.html. Con questa chiamata, le credenziali disponibili vengono recuperate quando l'utente arriva alla pagina del suo account.

<script type="module">
    // ... (imports)
    // Initialize the credential list by updating it once on page load
    updateCredentialList();

Chiamata a updateCredentialList al termine di registerCredential, per far sì che l'elenco mostri le credenziali appena create:

async function register() {
  let user = {};
  try {
    // ...
  } catch (e) {
    // ...
  }
  // Refresh the credential list to display the new credential
  await updateCredentialList();
}

Fate una prova! 👩🏻 💪

Hai completato la registrazione delle credenziali. Ora gli utenti possono creare credenziali basate sulla chiave di sicurezza e visualizzarle nella pagina del loro account.

Prova:

  • Esci.
  • Accedi con qualsiasi utente e password. Come accennato sopra, la correttezza della verifica della password non viene controllata per semplificare le cose in questo codelab. Inserisci una password non vuota.
  • Nella pagina Account, fai clic su Aggiungi una credenziale.
  • Ti verrà chiesto di inserire e toccare un token di sicurezza. Assicurati di farlo.
  • Una volta creata la credenziale, la credenziale dovrebbe essere visualizzata nella pagina dell'account.
  • Ricarica la pagina Account. Dovresti visualizzare le credenziali.
  • Se hai a disposizione due token, prova ad aggiungere due token di sicurezza diversi come credenziali. Devono essere entrambi visualizzati.
  • Prova a creare due credenziali con lo stesso autenticatore (chiave). Noterai che non sarà supportato. Questo è intenzionale: è dovuto al nostro utilizzo di excludeCredentials nel backend.

7. Abilita autenticazione a due fattori

I tuoi utenti possono registrare e annullare la registrazione, ma le credenziali vengono visualizzate e non vengono ancora utilizzate.

Ora è il momento di sfruttarli e configurare l'autenticazione a due fattori.

In questa sezione modificherai il flusso di autenticazione nella tua applicazione web da questo flusso di base:

6ff49a7e520836d0.png

Per questo flusso a due fattori:

e7409946cd88efc7.png

Implementare l'autenticazione a due fattori

Innanzitutto, aggiungiamo la funzionalità di cui abbiamo bisogno e implementiamo la comunicazione con il backend; aggiungeremo questa funzione nel frontend in un passaggio successivo.

Ciò che devi implementare qui è una funzione che autentica l'utente con una credenziale.

In public/auth.client.js, cerca la funzione vuota authenticateTwoFactor e aggiungi al codice il seguente codice:

async function authenticateTwoFactor() {
  // Fetch the 2F options from the backend
  const optionsFromServer = await _fetch("/auth/two-factor-options", "POST");
  // Decode them
  const decodedOptions = decodeServerOptions(optionsFromServer);
  // Get a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
  const credential = await navigator.credentials.get({
    publicKey: decodedOptions
  });
  // Encode the credential
  const encodedCredential = encodeCredential(credential);
  // Send it to the backend for verification
  return await _fetch("/auth/authenticate-two-factor", "POST", {
    credential: encodedCredential
  });
}

Tieni presente che questa funzione è già stata esportata per tuo conto, sarà necessario nel passaggio successivo.

Ecco cosa fa authenticateTwoFactor:

  • Richiede le opzioni di autenticazione a due fattori al server. Proprio come le opzioni di creazione delle credenziali che hai visto in precedenza, queste sono definite sul server e dipendono dal modello di sicurezza dell'applicazione web. Per informazioni dettagliate, vai al codice del server in router.post("/two-factors-options", ....
  • Richiamando navigator.credentials.get, il browser prende il controllo e chiede all'utente di inserire e toccare una chiave registrata in precedenza. Come risultato, viene selezionata una credenziale per questa specifica operazione di autenticazione a due fattori.
  • La credenziale selezionata viene quindi trasmessa in una richiesta di backend per recuperare("/auth/authentication-three-factor"". Se la credenziale è valida per quell'utente, l'utente viene quindi autenticato.

A parte questo, dai un'occhiata al codice del server

Tieni presente che server.js si occupa già della navigazione e dell'accesso: garantisce che la pagina Account sia accessibile solo agli utenti autenticati e che esegua alcuni reindirizzamenti necessari.

Ora diamo un'occhiata al codice del server in router.post("/initialize-authentication", ....

Ci sono due punti interessanti da osservare:

  • In questa fase vengono verificate contemporaneamente sia la password sia la credenziale. Questa è una misura di sicurezza: per gli utenti che hanno configurato l'autenticazione a due fattori, non vogliamo che le procedure dell'interfaccia utente abbiano un aspetto diverso a seconda che la password sia corretta o meno. Per questo motivo, controlliamo contemporaneamente la password e le credenziali, in questo passaggio.
  • Se sia la password sia le credenziali sono valide, completiamo l'autenticazione chiamando completeAuthentication(req, res);. Ciò significa in pratica che passiamo le sessioni da una sessione auth temporanea in cui l'utente non è ancora autenticato alla sessione principale main in cui l'utente è autenticato.

Includi la pagina di autenticazione a due fattori nel flusso utente

Nella cartella views, nota la nuova pagina second-factor.html.

Ha un pulsante Usa token di sicurezza, ma per ora non fa nulla.

Fai in modo che il pulsante chiami authenticateTwoFactor() al clic.

  • Se authenticateTwoFactor() ha esito positivo, reindirizza l'utente alla sua pagina Account.
  • Se non ha esito positivo, avvisa l'utente che si è verificato un errore. In un'applicazione reale, dovresti implementare messaggi di errore più utili: per semplicità, in questa demo utilizzeremo solo un avviso relativo alle finestre.
    <main>
...
    </main>
    <script type="module">
      import { authenticateTwoFactor, authStatuses } from "/auth.client.js";

      const button = document.querySelector("#authenticateButton");
      button.addEventListener("click", async e => {
        try {
          // Ask the user to authenticate with the second factor; this will trigger a browser prompt
          const response = await authenticateTwoFactor();
          const { authStatus } = response;
          if (authStatus === authStatuses.COMPLETE) {
            // The user is properly authenticated => Navigate to the Account page
            location.href = "/account";
          } else {
            throw new Error("Two-factor authentication failed");
          }
        } catch (e) {
          // Alert the user that something went wrong
          alert(`Two-factor authentication failed. ${e}`);
        }
      });
    </script>
  </body>
</html>

Utilizza l'autenticazione a due fattori

Ora è tutto pronto per aggiungere un passaggio di autenticazione a due fattori.

Ora è necessario aggiungere questo passaggio da index.html per gli utenti che hanno configurato l'autenticazione a due fattori.

322a5c49d865a0d8.png

In index.html, sotto location.href = "/account";, aggiungi il codice che indirizza l'utente alla pagina di autenticazione del secondo fattore se ha configurato 2FA.

In questo codelab, la creazione di una credenziale attiva automaticamente l'autenticazione a due fattori per l'utente.

Tieni presente che server.js implementa anche il controllo della sessione lato server, che garantisce che solo gli utenti autenticati possano accedere a account.html.

const { authStatus } = response;
if (authStatus === authStatuses.COMPLETE) {
  // The user is properly authenticated => navigate to account
  location.href = '/account';
} else if (authStatus === authStatuses.NEED_SECOND_FACTOR) {
  // Navigate to the two-factor-auth page because two-factor-auth is set up for this user
  location.href = '/second-factor';
}

Fate una prova! 👩🏻 💪

  • Accedi con il nuovo utente mariorossi.
  • Esci.
  • Accedi al tuo account come mariorossi. Tieni presente che è richiesta solo una password.
  • Crea una credenziale. Ciò significa che hai attivato l'autenticazione a due fattori come mariorossi.
  • Esci.
  • Inserisci il tuo nome utente mariorossi e la password.
  • Scopri come ti stai reindirizzando automaticamente alla pagina di autenticazione a due fattori.
  • Prova ad accedere alla pagina Account alla pagina /account; tieni presente che il sistema ti ha reindirizzato alla pagina Indice perché non hai eseguito l'autenticazione completa: non disponi di un secondo fattore.
  • Torna alla pagina di autenticazione a due fattori e fai clic su Utilizza token di sicurezza per l'autenticazione a due fattori.
  • Hai eseguito l'accesso e dovresti vedere la pagina Account.

8. Semplificare l'utilizzo delle credenziali

Hai terminato la funzionalità di base dell'autenticazione a due fattori con un token di sicurezza 🚀

Ma… Hai notato?

Al momento, il nostro elenco di credenziali non è molto pratico: l'ID delle credenziali e la chiave pubblica sono lunghe stringhe che non sono utili durante la gestione delle credenziali. Le persone non sono troppo belle con lunghe stringhe e numeri 🤖

Quindi, possiamo migliorare questa funzionalità e aggiungere funzionalità al nome e alla ridenominazione delle stringhe con stringhe leggibili.

Dai un'occhiata alla ridenominazione delle credenziali

Per farti risparmiare tempo nell'implementazione di questa funzione che non esegue operazioni all'avanguardia, è stata aggiunta una funzione per rinominare una credenziale nel codice di avvio, in auth.client.js:

async function renameCredential(credId, newName) {
  const params = new URLSearchParams({
    credId,
    name: newName
  });
  return _fetch(
    `/auth/credential?${params}`,
    "PUT"
  );
}

Si tratta di una normale chiamata di aggiornamento del database: il client invia una richiesta PUT al backend, con un ID credenziale e un nuovo nome per quella credenziale.

Implementare nomi di credenziali personalizzati

In account.html, nota la funzione vuota rename.

Aggiungi il seguente codice:

// Rename a credential
async function rename(credentialId) {
  // Let the user input a new name
  const newName = window.prompt(`Name this credential:`);
  // Rename only if the user didn't cancel AND didn't enter an empty name
  if (newName && newName.trim()) {
    try {
      // Make the backend call to rename the credential (the name is sanitized) server-side
      await renameCredential(credentialId, newName);
    } catch (e) {
      // Alert the user that something went wrong
      if (Array.isArray(e)) {
        alert(
          // `msg` not `message`, this is the key's name as per the express validator API
          `Renaming failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
        );
      } else {
        alert(`Renaming failed. ${e}`);
      }
    }
    // Refresh the credential list to display the new name
    await updateCredentialList();
  }
}

Una volta creata la credenziale, potrebbe essere meglio nominarla. Creiamo quindi una credenziale senza nome e, una volta creata, rinominala. Tuttavia, questo comporta due chiamate di backend.

Utilizza la funzione rename in register() per consentire agli utenti di assegnare un nome alle credenziali al momento della registrazione:

async function register() {
  let user = {};
  try {
    const user = await registerCredential();
    // Get the latest credential's ID (newly created credential)
    const allUserCredentials = user.credentials;
    const newCredential = allUserCredentials[allUserCredentials.length - 1];
    // Rename it
    await rename(newCredential.credId);
  } catch (e) {
    // ...
  }
  // Refresh the credential list to display the new credential
  await updateCredentialList();
}

Tieni presente che l'input utente verrà convalidato e purificato nel backend:

  check("name")
    .trim()
    .escape()

Mostra nomi credenziali

Vai a getCredentialHtml a templates.js.

Tieni presente che esiste già il codice per visualizzare il nome della credenziale nella parte superiore della scheda delle credenziali:

// Register credential
const getCredentialHtml = (credential, removeEl, renameEl) => {
 const { name, credId, publicKey } = credential;
 return html`
    <div class="credential-card">
      <div class="credential-name">
        ${name
          ? html`
              ${name}
            `
          : html`
              <span class="unnamed">(Unnamed)</span>
            `}
      </div>
     // ...
    </div>
  `;
};

Fate una prova! 👩🏻 💪

  • Crea una credenziale.
  • Ti verrà chiesto di assegnargli un nome.
  • Inserisci un nuovo nome e fai clic su OK.
  • Le credenziali sono state rinominate.
  • Ripeti e verifica che anche gli elementi funzionino senza problemi, lasciando vuoto il campo del nome.

Abilita ridenominazione delle credenziali

Gli utenti potrebbero dover rinominare le credenziali, ad esempio aggiungendo una seconda chiave e vogliono rinominare la prima chiave per distinguerle meglio.

In account.html, cerca la funzione finora vuota renameEl e aggiungi al codice il seguente codice:

// Rename a credential via HTML element
async function renameEl(el) {
  // Define the ID of the credential to update
  const credentialId = el.srcElement.dataset.credentialId;
  // Rename the credential
  await rename(credentialId);
  // Refresh the credential list to display the new name
  await updateCredentialList();
}

Ora, nel tag getCredentialHtml di templates.js, nel tag div di class="flex-end", aggiungi il codice seguente, il codice aggiunge un pulsante Rinomina al modello di scheda delle credenziali. Quando fai clic, il pulsante chiama la funzione renameEl che abbiamo appena creato:

const getCredentialHtml = (credential, removeEl, renameEl) => {
// ...
 <div class="flex-end">
  <button
    data-credential-id="${credId}"
    @click="${renameEl}"
    class="secondary right"
  >
   Rename
  </button>
 </div>
 // ...
  `;
};

Fate una prova! 👩🏻 💪

  • Fai clic su Rinomina.
  • Inserisci un nuovo nome quando richiesto.
  • Fai clic su OK.
  • La credenziale deve essere rinominata correttamente e l'elenco deve essere aggiornato automaticamente.
  • Se la pagina viene ricaricata, il nuovo nome dovrebbe essere ancora visualizzato, ovvero indica che il nuovo nome è permanente sul lato server.

Visualizza la data di creazione delle credenziali

La data di creazione non è presente nelle credenziali create tramite navigator.credential.create().

Tuttavia, poiché queste informazioni possono essere utili all'utente per distinguere le credenziali, abbiamo modificato automaticamente la libreria lato server nel codice di avvio e abbiamo aggiunto un campo creationDate uguale a Date.now() al momento dell'archiviazione delle nuove credenziali.

In templates.js all'interno di class="creation-date" div, aggiungi quanto segue per mostrare all'utente le informazioni sulla data di creazione:

<div class="creation-date">
  <label>Created:</label>
  <div class="info">
    ${new Date(creationDate).toLocaleDateString()}
    ${new Date(creationDate).toLocaleTimeString()}
  </div>
</div>

9. Rendi il tuo codice adatto al futuro

Finora abbiamo chiesto all'utente solo di registrare un semplice autenticatore di roaming che verrà poi utilizzato come secondo fattore durante l'accesso.

Un approccio più avanzato potrebbe essere quello di affidarsi a un tipo di autenticatore più potente, ovvero l'UVRA, che verifica l'utente. Un filtro UVRA può fornire due fattori di autenticazione e resistenza al phishing nei flussi di accesso in un solo passaggio.

Idealmente, dovresti supportare entrambi gli approcci. A tale scopo, devi personalizzare l'esperienza utente:

  • Se un utente ha solo un autenticatore di roaming semplice (che non consente la verifica degli utenti), consentigli di utilizzarlo per ottenere un bootstrap dell'account anti-phishing, ma dovrà anche digitare nome utente e password. Ed è proprio ciò che fa il nostro codelab.
  • Se un altro utente ha un metodo di autenticazione di roaming con verifica più avanzata per l'utente, potrà saltare il passaggio della password (e potenzialmente anche quello del nome utente) durante il bootstrap dell'account.

Scopri di più sull'avvio di account anti-phishing con accesso facoltativo senza password.

In questo codelab, non personalizziamo l'effettiva esperienza dell'utente, ma configureremo il tuo codebase in modo che tu abbia i dati necessari per personalizzare l'esperienza utente.

Ti occorrono due cose:

  • Configura residentKey: preferred nelle impostazioni del backend. Questa operazione è già stata eseguita.
  • Configura un modo per scoprire se è stata creata o meno una credenziale rilevabile (chiamata anche chiave residente).

Per scoprire se è stata creata o meno una credenziale rilevabile:

  • Esegui una query sul valore di credProps al momento della creazione delle credenziali (credProps: true).
  • Esegui una query sul valore di transports al momento della creazione delle credenziali. In questo modo, puoi stabilire se la piattaforma sottostante supporta la funzionalità UVRA, ad esempio se è davvero un cellulare.
  • Archivia il valore di credProps e transports nel backend. Questa operazione è già stata eseguita nel codice di avvio. Se sei curioso, dai un'occhiata a auth.js.

Riceviamo il valore di credProps e transports e li invii al backend. In auth.client.js, modifica registerCredential come segue:

  • Aggiungi un campo extensions durante la chiamata a navigator.credentials.create
  • Imposta encodedCredential.transports e encodedCredential.credProps prima di inviare le credenziali al backend per l'archiviazione.

registerCredential dovrebbe avere il seguente aspetto:

async function registerCredential() {
  // Fetch the credential creation options from the backend
  const credentialCreationOptionsFromServer = await _fetch(
    '/auth/credential-options',
    'POST'
  );
  // Decode the credential creation options
  const credentialCreationOptions = decodeServerOptions(
    credentialCreationOptionsFromServer
  );
  // Create a credential via the browser API; this will prompt the user
  const credential = await navigator.credentials.create({
    publicKey: {
      ...credentialCreationOptions,
      extensions: {
        credProps: true,
      },
    },
  });
  // Encode the newly created credential to send it to the backend
  const encodedCredential = encodeCredential(credential);
  // Set transports and credProps for more advanced user flows
  encodedCredential.transports = credential.response.getTransports();
  encodedCredential.credProps =
    credential.getClientExtensionResults().credProps;
  // Send the encoded credential to the backend for storage
  return await _fetch('/auth/credential', 'POST', encodedCredential);
}

10. Assicurare il supporto per più browser

Supporto per i browser diversi da Chromium

Nella funzione registerCredential di public/auth.client.js, stiamo chiamando credential.response.getTransports() la credenziale appena creata per salvare queste informazioni nel backend come suggerimento al server.

Tuttavia, al momento getTransports() non è implementato in tutti i browser (a differenza di getClientExtensionResults supportato da tutti i browser): la chiamata getTransports() genererà un errore in Firefox e Safari, impedendo la creazione delle credenziali in questi browser.

Per assicurarti che il tuo codice funzioni su tutti i principali browser, aggrega la chiamata a encodedCredential.transports in una condizione:

if (credential.response.getTransports) {
  encodedCredential.transports = credential.response.getTransports();
}

Sul server, transports è impostato su transports || []. In Firefox e Safari l'elenco transports non sarà undefined, ma un elenco vuoto [], che impedisce errori.

Avvisa gli utenti che utilizzano browser che non supportano WebAuthn

1e9c1be837d66ce8.png

Anche se WebAuthn è supportato in tutti i principali browser, è buona norma mostrare un avviso nei browser che non supportano WebAuthn.

In index.html, osserva la presenza di questo div:

<div id="warningbanner" class="invisible">
⚠️ Your browser doesn't support WebAuthn. Open this demo in Chrome, Edge, Firefox or Safari.
</div>

Nello script incorporato di index.html, aggiungi il codice seguente per visualizzare il banner nei browser che non supportano WebAuthn:

// Display a banner in browsers that don't support WebAuthn
if (!window.PublicKeyCredential) {
  document.querySelector('#warningbanner').classList.remove('invisible');
}

In un'applicazione web reale, puoi fare qualcosa di più elaborato e avere un meccanismo di riserva appropriato per questi browser, ma questo mostra come verificare il supporto di WebAuthn.

11. Ottimo lavoro!

✨Fatto!

Hai implementato l'autenticazione a due fattori con un token di sicurezza.

In questo codelab abbiamo trattato i concetti di base. Se vuoi approfondire WebAuthn per l'autenticazione a 2 fattori (2FA), ecco alcune idee per queste soluzioni:

  • Aggiungi le informazioni "Ultime utilizzate" alla scheda credenziali. Si tratta di informazioni utili per determinare se un determinato token di sicurezza viene usato attivamente o meno, soprattutto se ha registrato più token.
  • Implementa una gestione degli errori più efficace e messaggi di errore più precisi.
  • Esamina auth.js ed esplora ciò che accade quando modifichi alcune authSettings, in particolare quando utilizzi una chiave che supporta la verifica degli utenti.