Sécuriser votre site grâce à l'authentification à deux facteurs avec clé de sécurité (WebAuthn)

1. Ce que vous allez faire

Dans un premier temps, vous allez utiliser une application Web de base compatible avec la connexion par mot de passe.

Ensuite, vous allez ajouter l'authentification à deux facteurs avec clé de sécurité, basée sur WebAuthn. Pour cela, vous allez implémenter les éléments suivants :

  • Un moyen pour un utilisateur d'enregistrer des identifiants WebAuthn
  • Un flux d'authentification à deux facteurs dans lequel l'utilisateur est invité à saisir son deuxième facteur (un identifiant WebAuthn) s'il en a enregistré un
  • Une interface de gestion des identifiants : une liste d'identifiants permettant aux utilisateurs de renommer et supprimer des identifiants

16ce77744061c5f7.png

Testez l'application Web terminée.

2. À propos de WebAuthn

Principes de base de WebAuthn

Pourquoi WebAuthn ?

L'hameçonnage est un problème de sécurité majeur sur le Web : la plupart des piratages de compte profitent du manque de sécurité des mots de passe ou de leur réutilisation sur plusieurs sites. La réponse collective du secteur à ce problème : l'authentification multifacteur. Toutefois, son implémentation est fragmentée et nombre d'acteurs ne luttent toujours pas de façon adéquate contre l'hameçonnage.

L'API Web Authentication (WebAuthn) est un protocole standard anti-hameçonnage qui peut être utilisé par n'importe quelle application Web.

Fonctionnement

Source : webauthn.guide

WebAuthn permet aux serveurs d'enregistrer et d'authentifier des utilisateurs à l'aide d'une méthode de chiffrement par clé publique au lieu d'un mot de passe. Les sites Web peuvent créer des identifiants composés d'une paire de clés publique-privée.

  • La clé privée est stockée de manière sécurisée sur l'appareil de l'utilisateur.
  • La clé publique et l'identifiant généré de manière aléatoire sont envoyés au serveur pour stockage.

La clé publique permet au serveur de prouver l'identité de l'utilisateur. Elle n'est pas secrète, car elle ne sert à rien sans la clé privée correspondante.

Avantages

WebAuthn offre deux avantages principaux :

  • Aucun secret partagé : le serveur ne stocke aucun secret. De ce fait, les bases de données sont moins attrayantes pour les pirates informatiques, car les clés publiques ne leur sont pas utiles.
  • Identifiants restreints : un identifiant enregistré pour site.example ne peut pas être utilisé sur evil-site.example. WebAuthn résiste ainsi à l'hameçonnage.

Cas d'utilisation

Un cas d'utilisation de WebAuthn est l'authentification à deux facteurs avec clé de sécurité. Cette méthode peut être particulièrement utile pour les applications Web d'entreprise.

Navigateurs compatibles

Il est écrit par W3C et FIDO, avec la participation de Google, Mozilla, Microsoft et Yubico, entre autres.

Glossaire

  • Authentificateur : entité logicielle ou matérielle capable d'enregistrer un utilisateur et de revendiquer plus tard la possession des identifiants enregistrés. Il existe deux types d'authentificateurs :
  • Authentificateur itinérant : authentificateur utilisable avec n'importe quel appareil depuis lequel l'utilisateur tente de se connecter. Exemple : clé de sécurité USB, smartphone.
  • Authentificateur de plate-forme : authentificateur intégré à l'appareil d'un utilisateur. Exemple : Touch ID d'Apple.
  • Identifiants : la paire de clés publique-privée.
  • Partie de confiance : le serveur pour le site Web qui essaie d'authentifier l'utilisateur.
  • Serveur FIDO : serveur utilisé pour l'authentification. FIDO est une famille de protocoles développée par l'alliance FIDO. L'un de ces protocoles est WebAuthn.

Dans cet atelier, nous allons utiliser un authentificateur itinérant.

3. Avant de commencer

Ce dont vous avez besoin

Prérequis pour cet atelier de programmation :

  • Connaissances de base de WebAuthn
  • Connaissances de base en JavaScript et en HTML
  • Navigateur à jour compatible WebAuthn
  • Clé de sécurité compatible U2F

Vous pouvez utiliser l'un des appareils suivants comme clé de sécurité :

  • Un téléphone Android avec Android 7 (Nougat) ou version ultérieure fonctionnant sous Chrome. Dans ce cas, vous aurez également besoin d'une machine Windows, macOS ou Chrome OS avec Bluetooth.
  • Une clé USB, par exemple YubiKey.

6539dc7ffec2538c.png

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

dd56e2cfe0f7ced2.png

Ce que vous allez apprendre

Ce que vous allez apprendre ✅

  • Comment enregistrer et utiliser une clé de sécurité comme deuxième facteur pour l'authentification WebAuthn
  • Comment rendre ce processus convivial

Ce que vous n'allez pas apprendre ❌

  • Comment créer un serveur FIDO (le serveur utilisé pour l'authentification). Ce n'est pas très grave, car en tant que développeur d'applications Web ou de sites, vous utilisez généralement des serveurs FIDO existants. Veillez à toujours vérifier la fonctionnalité et la qualité des implémentations de serveur sur lesquelles vous vous reposez. Dans cet atelier de programmation, le serveur FIDO utilise SimpleWebAuthn. Pour découvrir d'autres options, consultez la page officielle de la FIDO Alliance. Pour accéder à des bibliothèques Open Source, consultez webauthn.io ou AwesomeWebAuthn.

Attention

L'utilisateur doit saisir un mot de passe pour se connecter. Toutefois, par souci de simplicité dans cet atelier de programmation, le mot de passe n'est ni stocké, ni vérifié. Dans une application réelle, vous vérifieriez qu'il est correct côté serveur.

Cet atelier de programmation intègre des contrôles de sécurité de base, tels que les vérifications CSRF, la validation de session et le nettoyage des entrées. Cependant, de nombreuses mesures de sécurité ne sont pas intégrées. Par exemple, aucune limite n'est fixée au nombre de mots de passe saisis pour éviter les attaques par force brute. Ce n'est pas important ici, car les mots de passe ne sont pas stockés. Veillez toutefois à ne pas utiliser ce code tel quel en production.

4. Configurer votre authentificateur

Si vous utilisez un téléphone Android comme authentificateur

  • Assurez-vous que Chrome est à jour sur votre ordinateur et sur votre téléphone.
  • Sur votre ordinateur et votre téléphone, ouvrez Chrome et connectez-vous avec le profil que vous souhaitez utiliser pour cet atelier.
  • Activez la synchronisation pour ce profil sur votre ordinateur et votre téléphone. Pour cela, utilisez chrome://settings/syncSetup.
  • Activez le Bluetooth sur votre ordinateur et sur votre téléphone.
  • Sur l'ordinateur, dans Chrome, connectez-vous avec le même profil et accédez au site webauthn.io.
  • Saisissez un nom d'utilisateur simple. Laissez les valeurs Attestation type (Type d'attestation) et Authenticator type (Type d'authentificateur) sur None (Aucun) et Unspecified (Non spécifié) (par défaut). Cliquez sur Register (Enregistrer).

6b49ff0298f5a0af.png

  • Une fenêtre de navigateur s'ouvre, vous demandant de valider votre identité. Sélectionnez votre téléphone dans la liste.

ffebe58ac826eaf2.png 852de328fcd4eb42.png

  • Sur votre téléphone, vous devriez recevoir une notification intitulée Verify your identity (Valider votre identité). Appuyez dessus.
  • Sur votre téléphone, vous serez invité à saisir le code PIN de votre téléphone (ou à scanner votre empreinte digitale). Saisissez-le.
  • Sur le site webauthn.io (sur votre ordinateur), un indicateur de réussite s'affiche.

fc0acf00a4d412fa.png

  • Sur le site webauthn.io (sur votre ordinateur), cliquez sur le bouton de connexion.
  • Là encore, une fenêtre de navigateur s'ouvre. Sélectionnez votre téléphone dans la liste.
  • Sur votre téléphone, appuyez sur la notification qui s'affiche et saisissez votre code PIN (ou scannez votre empreinte digitale).
  • Le site webauthn.io doit vous indiquer que vous êtes connecté. Votre téléphone fonctionne correctement comme clé de sécurité. Vous êtes prêt pour l'atelier !

Si vous utilisez une clé de sécurité USB comme authentificateur

  • Sur votre ordinateur, dans Chrome, accédez au site webauthn.io.
  • Saisissez un nom d'utilisateur simple. Laissez les valeurs Attestation type (Type d'attestation) et Authenticator type (Type d'authentificateur) sur None (Aucun) et Unspecified (Non spécifié) (par défaut). Cliquez sur Register (Enregistrer).
  • Une fenêtre de navigateur s'ouvre, vous demandant de valider votre identité. Sélectionnez USB security key (Clé de sécurité USB) dans la liste.

ffebe58ac826eaf2.png 9fe75f04e43da035.png

  • Insérez votre clé de sécurité dans votre ordinateur et appuyez dessus.

923d5adb8aa8286c.png

  • Sur le site webauthn.io (sur votre ordinateur), un indicateur de réussite s'affiche.

fc0acf00a4d412fa.png

  • Sur le site webauthn.io, cliquez sur le bouton Login (Connexion).
  • Là encore, une fenêtre de navigateur s'ouvre. Sélectionnez USB security key (Clé de sécurité USB) dans la liste.
  • Appuyez sur la clé de sécurité USB.
  • Le site webauthn.io doit vous indiquer que vous êtes connecté. Votre clé de sécurité USB fonctionne correctement. Vous êtes prêt pour l'atelier !

7e1c0bb19c9f3043.png

5. Configuration

Dans cet atelier de programmation, vous allez utiliser Glitch, un éditeur de code en ligne qui déploie automatiquement et instantanément votre code.

Dupliquer le code de départ

Ouvrez le projet de départ.

Cliquez sur le bouton Remix (Remixer).

Une copie du code de départ est alors créée. Vous disposez maintenant de votre propre code à modifier. Vous allez effectuer toutes les tâches de cet atelier de programmation dans votre copie (appelée "remix" dans Glitch).

cf2b9f552c9809b6.png

Explorer le code de départ

Explorez le code de départ que vous venez de dupliquer.

Notez que sous libs, une bibliothèque appelée auth.js est déjà fournie. Il s'agit d'une bibliothèque personnalisée qui gère la logique d'authentification côté serveur. Elle utilise la bibliothèque fido comme dépendance.

6. Implémenter l'enregistrement des identifiants

Implémenter l'enregistrement des identifiants

Pour configurer l'authentification à deux facteurs avec clé de sécurité, nous devons d'abord permettre à l'utilisateur de créer des identifiants.

Pour commencer, ajoutons une fonction qui effectue cette opération dans notre code côté client.

Dans public/auth.client.js, notez qu'il existe une fonction appelée registerCredential() qui ne fait rien pour l'instant. Ajoutez le code suivant à celle-ci :

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);
}

Notez que cette fonction est déjà exportée pour vous.

Voici ce que fait la fonction registerCredential :

  • Elle récupère les options de création d'identifiants à partir du serveur (/auth/credential-options).
  • Comme les options du serveur reviennent encodées, la fonction utilitaire decodeServerOptions les décode.
  • Elle crée des identifiants en appelant l'API Web navigator.credential.create. Lorsque l'API navigator.credential.create est appelée, le navigateur prend le relais et invite l'utilisateur à choisir une clé de sécurité.
  • Elle décode les identifiants que vous venez de créer.
  • Elle enregistre les nouveaux identifiants côté serveur en envoyant une requête à /auth/credential qui contient les identifiants encodés.

Note : examinez le code du serveur

registerCredential() passe deux appels au serveur. Voyons ce qui se passe dans le backend.

Options de création d'identifiants

Lorsque le client envoie une requête à (/auth/credential-options), le serveur génère un objet d'options et le renvoie au client.

Cet objet est ensuite utilisé par le client dans l'appel de création d'identifiants proprement dit :

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

Qu'est-ce qui, dans credentialCreationOptions, est finalement utilisé dans la fonction registerCredential côté client que vous avez implémentée à l'étape précédente ?

Examinez le code du serveur sous router.post("/credential-options", ....

Nous n'allons pas voir chaque propriété, mais en voici quelques-unes intéressantes que vous pouvez voir dans l'objet d'options du code du serveur, généré à l'aide de la bibliothèque fido2 et au final renvoyé au client :

  • rpName et rpId décrivent l'organisation qui enregistre et authentifie l'utilisateur. N'oubliez pas que dans WebAuthn, les identifiants sont limités à un domaine spécifique, un avantage en termes de sécurité. rpName et rpId permettent ici de définir la portée des identifiants. Par exemple, un rpId valide est le nom d'hôte de votre site. Notez comment ces identifiants se mettent à jour automatiquement lorsque vous dupliquez le projet de départ 🧘🏻 ♀️
  • excludeCredentials est une liste d'identifiants. Les nouveaux identifiants ne peuvent pas être créés sur un authentificateur contenant également l'un des identifiants répertoriés dans excludeCredentials. Dans notre atelier de programmation, excludeCredentials est une liste d'identifiants existants pour cet utilisateur. Avec cette liste et user.id, nous nous assurons que chaque identifiant qu'un utilisateur crée est associé à un authentificateur différent (clé de sécurité). Cette pratique est recommandée, car si un utilisateur a enregistré plusieurs identifiants, ceux-ci seront rattachés à différents authentificateurs (clés de sécurité). Par conséquent, perdre une clé de sécurité n'empêchera pas l'utilisateur d'accéder à son compte.
  • authenticatorSelection définit le type d'authentificateur que vous souhaitez autoriser dans votre application Web. Examinons authenticatorSelection de plus près :
    • residentKey: preferred signifie que cette application n'applique pas les identifiants visibles côté client. Un identifiant visible côté client est un type spécial d'identifiant qui permet d'authentifier un utilisateur sans avoir à l'identifier au préalable. Ici, nous avons configuré preferred, car nous nous concentrons dans cet atelier de programmation sur l'implémentation de base. Les identifiants visibles sont destinés à des processus plus avancés.
    • requireResidentKey n'est présent que pour la rétrocompatibilité avec WebAuthn v1.
    • userVerification: preferred signifie que si l'authentificateur est compatible avec la validation de l'utilisateur (par exemple, s'il s'agit d'une clé de sécurité biométrique ou d'une clé avec fonctionnalité intégrée de code PIN), la partie de confiance demandera celle-ci lors de la création des identifiants. Si l'authentificateur n'est pas compatible (clé de sécurité de base), le serveur ne demande pas la validation de l'utilisateur.
  • ​​pubKeyCredParam décrit, par ordre de préférence, les propriétés cryptographiques souhaitées des identifiants.

Toutes ces options sont des décisions que l'application Web doit prendre pour son modèle de sécurité. Notez que, sur le serveur, ces options sont définies dans un seul objet authSettings.

Défi

Autre point intéressant : req.session.challenge = options.challenge;.

WebAuthn est un protocole cryptographique. Il dépend donc de défis aléatoires pour éviter les attaques par relecture, lorsqu'un pirate informatique vole une charge utile pour relire l'authentification, lorsqu'il n'est pas propriétaire de la clé privée qui activerait l'authentification.

Pour limiter ce problème, un défi est généré sur le serveur, signé à la volée. Cette signature est ensuite comparée à celle attendue. Cela permet de s'assurer que l'utilisateur détient la clé privée au moment de générer les identifiants.

Code d'enregistrement des identifiants

Examinez le code du serveur sous router.post("/credential", ...).

C'est ici que l'identifiant est enregistré côté serveur.

Que se passe-t-il ?

L'un des éléments les plus importants de ce code est l'appel de validation, via fido2.verifyAttestationResponse :

  • Le défi signé est vérifié, afin de s'assurer que les identifiants ont été créés par une personne qui détenait bien la clé privée au moment de leur création.
  • L'identifiant du tiers de confiance, qui est associé à son origine, est également validé. Cela permet de s'assurer que les identifiants sont liés à cette application Web (et uniquement à celle-ci).

Ajouter cette fonctionnalité à l'UI

Maintenant que votre fonction registerCredential(),, permettant de créer un identifiant, est prête, mettons-la à la disposition de l'utilisateur.

Vous allez réaliser cette opération depuis la page Compte, l'emplacement habituel pour gérer l'authentification.

Dans le balisage de account.html, sous le nom d'utilisateur, se trouve un objet div vide, avec la classe de mise en page class="flex-h-between". Nous utiliserons cet objet div pour les éléments d'interface utilisateur liés à l'authentification à deux facteurs.

Dans ce div, ajoutez les éléments suivants :

  • Le titre "Two-factor authentication" (Authentification à deux facteurs)
  • Un bouton permettant de créer des identifiants
 <div class="flex-h-between">
    <h3>
        Two-factor authentication
    </h3>
    <button class="create" id="registerButton" raised>
        ➕ Add a credential
    </button>
</div>

Sous le div, ajoutez un div d'identifiants, dont nous aurons besoin plus tard :

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

Dans le script Inline account.html, importez la fonction que vous venez de créer et ajoutez une fonction register qui l'appelle, ainsi qu'un gestionnaire d'événements associé au bouton que vous venez de créer.

// 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}`);
    }
  }
}

Afficher les identifiants visibles par l'utilisateur

Maintenant que vous avez ajouté une fonctionnalité permettant de créer des identifiants, vous devez permettre aux utilisateurs de voir les identifiants qu'ils ont créés.

La page Account (Compte) est un bon endroit pour cela.

Dans account.html, recherchez la fonction appelée updateCredentialList().

Ajoutez le code suivant, qui appelle le backend pour récupérer tous les identifiants enregistrés pour l'utilisateur actuellement connecté et qui affiche les identifiants renvoyés :

// 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);
}

Pour l'instant, ignorez removeEl et renameEl. Nous les verrons plus tard dans cet atelier de programmation.

Ajoutez un appel à updateCredentialList au début de votre script Inline, dans account.html. Avec cet appel, les identifiants disponibles sont récupérés lorsque l'utilisateur accède à la page de son compte.

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

À présent, appelez updateCredentialList une fois que registerCredential s'est exécuté, pour que les listes affichent les nouveaux identifiants :

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

Faites l'essai ! 👩🏻‍💻

Vous avez terminé l'enregistrement des identifiants ! Les utilisateurs peuvent désormais créer des identifiants basés sur une clé de sécurité et les consulter dans leur page Account (Compte).

Essayez :

  • Déconnectez-vous.
  • Connectez-vous avec n'importe quel nom d'utilisateur et mot de passe. Comme indiqué précédemment, le mot de passe n'est pas vérifié, pour simplifier cet atelier de programmation. Saisissez un mot de passe non vide.
  • Sur la page Account (Compte), cliquez sur Add a credential (Ajouter des identifiants).
  • Vous devriez être invité à insérer et à appuyer sur une clé de sécurité. Faites-le.
  • Une fois les identifiants créés, ils doivent s'afficher sur la page du compte.
  • Actualisez la page Account (Compte). Vous devriez voir les identifiants.
  • Si vous possédez deux clés de sécurité, essayez de les ajouter comme identifiants. Les deux devraient s'afficher.
  • Essayez de créer deux identifiants avec le même authentificateur (clé). Vous remarquerez que ce n'est pas possible. C'est normal, car nous avons utilisé excludeCredentials dans le backend.

7. Activer l'authentification à deux facteurs

Les utilisateurs peuvent enregistrer et désenregistrer des identifiants, mais ceux-ci sont simplement affichés ; ils ne sont pas encore utilisés.

Vous allez maintenant les utiliser et configurer l'authentification à deux facteurs.

Dans cette section, vous allez modifier le flux d'authentification de votre application Web de ce flux de base :

6ff49a7e520836d0.png

À ce flux à deux facteurs :

e7409946cd88efc7.png

Implémenter l'authentification à deux facteurs

Commençons par ajouter la fonctionnalité dont nous avons besoin et par implémenter la communication avec le backend. Ensuite, nous ajouterons cela à l'interface à l'étape suivante.

Ici, vous devez implémenter une fonction qui authentifie l'utilisateur grâce à des identifiants.

Dans public/auth.client.js, recherchez la fonction vide authenticateTwoFactor et ajoutez-y le code suivant :

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
  });
}

Notez que cette fonction est déjà exportée pour vous. Nous en aurons besoin à l’étape suivante.

Voici ce que fait la fonction authenticateTwoFactor :

  • Elle demande au serveur les options d'authentification à deux facteurs. Tout comme les options de création d'identifiants vues précédemment, ces options sont définies sur le serveur et dépendent du modèle de sécurité de l'application Web. Pour en savoir plus, reportez-vous au code du serveur dans router.post("/two-factors-options", ....
  • L'appel de navigator.credentials.get permet au navigateur de prendre le relais et d'inviter l'utilisateur à insérer une clé enregistrée précédemment et à appuyer dessus. Des identifiants sont alors sélectionnés pour cette opération d'authentification à deux facteurs spécifique.
  • Les identifiants sélectionnés sont ensuite transmis dans une requête backend afin de récupérer "/auth/authenticate-two-factor". Si les identifiants sont valides pour cet utilisateur, l'utilisateur est authentifié.

Note : examinez le code du serveur

Comme vous pouvez le constater, server.js se charge déjà d'une partie de la navigation et de l'accès : il garantit que la page Account (Compte) est uniquement accessible par les utilisateurs authentifiés, et effectue les redirections nécessaires.

Examinez à présent le code du serveur sous router.post("/initialize-authentication", ....

Deux points intéressants à noter ici :

  • Le mot de passe et les identifiants sont vérifiés simultanément à cette étape. C'est une mesure de sécurité : pour les utilisateurs qui ont configuré l'authentification à deux facteurs, nous ne souhaitons pas que les flux de l'interface utilisateur diffèrent selon que le mot de passe soit valide ou non. C'est pourquoi nous vérifions le mot de passe et les identifiants simultanément à cette étape.
  • Si le mot de passe et les identifiants sont valides, nous finalisons l'authentification en appelant completeAuthentication(req, res);. En pratique, cela consiste à changer de session et à passer d'une session auth temporaire où l'utilisateur n'est pas encore authentifié à la session principale main où l'utilisateur est authentifié.

Inclure la page d'authentification à deux facteurs dans le parcours utilisateur

Dans le dossier views, notez la nouvelle page second-factor.html.

Elle contient un bouton Use security key (Utiliser une clé de sécurité), qui n'est pas actif pour le moment.

Configurez ce bouton pour qu'il appelle authenticateTwoFactor() lorsque l'utilisateur clique dessus.

  • Si l'opération authenticateTwoFactor() réussit, redirigez l'utilisateur vers sa page Account (Compte).
  • Sinon, informez l'utilisateur qu'une erreur s'est produite. Dans une application réelle, vous implémenteriez des messages d'erreur plus utiles. Par souci de simplicité dans cette démonstration, nous utiliserons uniquement une fenêtre d'alerte.
    <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>

Utiliser l'authentification à deux facteurs

Vous êtes maintenant prêt à ajouter une étape d'authentification à deux facteurs.

Vous devez maintenant ajouter cette étape depuis index.html, pour les utilisateurs qui ont configuré l'authentification à deux facteurs.

322a5c49d865a0d8.png

Dans index.html, sous location.href = "/account";, ajoutez le code qui redirige l'utilisateur de manière conditionnelle vers la page d'authentification à deux facteurs s'il a configuré cette méthode.

Dans cet atelier de programmation, la création d'un identifiant active automatiquement l'authentification à deux facteurs.

Notez que server.js implémente également la vérification de session côté serveur, ce qui garantit que seuls les utilisateurs authentifiés peuvent accéder à 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';
}

Faites l'essai ! 👩🏻‍💻

  • Connectez-vous avec un nouveau nom d'utilisateur, johndoe.
  • Déconnectez-vous.
  • Connectez-vous à votre compte en tant que johndoe. Vous remarquerez que seul un mot de passe est requis.
  • Créez un identifiant. Concrètement, cela signifie que vous avez activé l'authentification à deux facteurs en tant que johndoe.
  • Déconnectez-vous.
  • Saisissez votre nom d'utilisateur johndoe et votre mot de passe.
  • Comme vous pouvez le constater, vous êtes automatiquement redirigé vers la page d'authentification à deux facteurs.
  • Essayez d'accéder à la page Account (Compte) sur /account. Comme vous pouvez le constater, vous êtes redirigé vers la page d'index, car vous n'êtes pas complètement authentifié : il manque le deuxième facteur.
  • Revenez à la page d'authentification à deux facteurs et cliquez sur Use security key (Utiliser une clé de sécurité) pour utiliser l'authentification à deux facteurs.
  • Vous êtes maintenant connecté, et la page Account (Compte) devrait s'afficher.

8. Simplifier l'utilisation des identifiants

Vous venez de voir les fonctionnalités de base de l'authentification à deux facteurs avec clé de sécurité 🚀

Mais… Avez-vous remarqué ?

Pour le moment, notre liste d'identifiants n'est pas vraiment pratique. La clé publique et l'ID des identifiants sont des chaînes longues peu commodes pour gérer les identifiants. L'être humain n'est pas très doué pour gérer des chaînes longues et des chiffres 🤖

Améliorons cela en ajoutant une fonctionnalité permettant de nommer et renommer les identifiants à l'aide de chaînes compréhensibles.

Examiner renameCredential

Pour vous faire gagner du temps dans l'implémentation de cette fonction dont l'action n'a rien de révolutionnaire, nous avons ajouté une fonction permettant de renommer des identifiants dans le code de départ, dans auth.client.js :

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

Il s'agit d'un appel de mise à jour de base de données standard. Le client envoie une requête PUT au backend, avec un ID et un nouveau nom pour ces identifiants.

Implémenter des noms d'identifiant personnalisés

Comme vous pouvez le constater, account.html contient une fonction vide, rename.

Ajoutez-y le code suivant :

// 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();
  }
}

Il peut être plus judicieux de ne nommer un identifiant qu'une fois celui-ci créé. Vous allez à présent créer un identifiant sans nom, puis lui attribuer un nom une fois créé. Toutefois, cette opération va passer deux appels au backend.

Utilisez la fonction rename dans register() pour que les utilisateurs puissent nommer les identifiants lors de l'enregistrement :

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();
}

Notez que les entrées utilisateur sont validées et nettoyées dans le backend :

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

Afficher le nom des identifiants

Accédez à getCredentialHtml dans templates.js.

Notez qu'il existe déjà un code pour afficher le nom des identifiants en haut de la fiche des identifiants :

// 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>
  `;
};

Faites l'essai ! 👩🏻‍💻

  • Créez un identifiant.
  • Vous êtes invité à lui attribuer un nom.
  • Saisissez un nouveau nom et cliquez sur OK.
  • L'identifiant est à présent renommé.
  • Répétez l'opération et vérifiez que tout fonctionne correctement lorsque vous laissez le champ de nom vide.

Activer le changement de nom des identifiants

Les utilisateurs peuvent avoir besoin de renommer leurs identifiants. Par exemple, s'ils ajoutent une deuxième clé et souhaitent renommer la première pour les différencier plus facilement.

Dans account.html, recherchez la fonction vide renameEl et ajoutez-y le code suivant :

// 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();
}

À présent, dans getCredentialHtml de templates.js, dans le div class="flex-end", ajoutez le code ci-dessous. Celui-ci permet d'ajouter un bouton Rename (Renommer) au modèle de fiche d'identifiants. Lorsque vous cliquez dessus, il appelle la fonction renameEl que vous venez de créer :

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

Faites l'essai ! 👩🏻‍💻

  • Cliquez sur Rename (Renommer).
  • Lorsque vous y êtes invité, saisissez un nouveau nom.
  • Cliquez sur OK.
  • L'identifiant doit être renommé et la liste devrait se mettre à jour automatiquement.
  • Si vous actualisez la page, le nouveau nom doit toujours s'afficher. Cela montre que le nouveau nom est conservé côté serveur.

Afficher la date de création des identifiants

La date de création n'est pas indiquée dans les identifiants créés via navigator.credential.create().

Toutefois, comme ces informations peuvent aider les utilisateurs à différencier leurs identifiants, nous avons modifié la bibliothèque côté serveur dans le code de départ. Nous avons également ajouté un champ creationDate égal à Date.now() lors du stockage de nouveaux identifiants.

Dans templates.js, dans le div class="creation-date", ajoutez le code suivant pour que l'utilisateur puisse voir la date de création :

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

9. Préparer le code pour l'avenir

Jusqu'à présent, nous n'avons demandé à l'utilisateur que d'enregistrer un simple authentificateur itinérant, qui est ensuite utilisé comme second facteur pour se connecter.

Une approche plus sophistiquée consisterait à s'appuyer sur un type d'authentificateur plus puissant : un authentificateur itinérant avec validation de l'utilisateur (UVRA). Un tel authentificateur peut fournir deux facteurs d'authentification et protéger l'utilisateur contre l'hameçonnage lors des flux de connexion en une seule étape.

Dans l'idéal vous devriez accepter ces deux approches. Pour cela, vous devez personnaliser l'expérience utilisateur :

  • Si un utilisateur dispose uniquement d'un authentificateur itinérant simple (sans validation de l'utilisateur), autorisez-le à s'en servir pour se connecter à son compte tout en se protégeant contre l'hameçonnage. Il devra toutefois également saisir un nom d'utilisateur et un mot de passe. C'est ce que nous avons vu jusqu'à présent dans cet atelier de programmation.
  • Si un autre utilisateur dispose d'un authentificateur itinérant plus avancé avec validation de l'utilisateur, il peut ignorer l'étape de saisie du mot de passe (voire aussi celle de saisie du nom d'utilisateur) lors de la connexion au compte.

En savoir plus sur l'accès au compte anti-hameçonnage avec connexion sans mot de passe facultative

Dans cet atelier de programmation, nous ne personnaliserons pas l'expérience utilisateur. Toutefois, nous configurerons votre codebase afin que vous disposiez des données nécessaires pour cela.

Vous avez besoin de deux choses :

  • Définir residentKey: preferred dans les paramètres de votre backend. Cette opération a déjà été effectuée pour vous.
  • Configurer un moyen de savoir si des identifiants visibles (également appelés "clés résidentes") ont été créés.

Pour savoir si un identifiant visible a été créé :

  • Interrogez la valeur de credProps lors de la création des identifiants (credProps: true).
  • Interrogez la valeur de transports lors de la création des identifiants. Vous pourrez ainsi déterminer si la plate-forme sous-jacente est compatible avec la fonctionnalité UVRA, c'est-à-dire s'il s'agit vraiment d'un téléphone mobile, par exemple.
  • Stockez la valeur de credProps et de transports dans le backend. Cette opération a déjà été effectuée pour vous dans le code de départ. Examinez auth.js si vous voulez en savoir plus.

Récupérez les valeurs de credProps et de transports, puis transmettez-les au backend. Dans auth.client.js, modifiez registerCredential comme suit :

  • Ajoutez un champ extensions lors de l'appel de navigator.credentials.create.
  • Définissez encodedCredential.transports et encodedCredential.credProps avant d'envoyer les identifiants au backend pour les stocker.

registerCredential doit ressembler à ceci :

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. Accepter plusieurs navigateurs

Accepter les navigateurs autres que Chromium

Dans la fonction registerCredential de public/auth.client.js, nous appelons credential.response.getTransports() pour les identifiants que nous venons de créer afin d'enregistrer ces informations dans le backend sous forme de hint pour le serveur.

Cependant, getTransports() n'est pas implémenté dans tous les navigateurs, contrairement à getClientExtensionResults, qui est compatible avec tous les navigateurs. L'appel getTransports() générera une erreur dans Firefox et Safari, empêchant la création d'identifiants dans ces navigateurs.

Pour vous assurer que votre code s'exécutera dans les principaux navigateurs, encapsulez l'appel encodedCredential.transports dans une condition :

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

Notez que transports est défini sur transports || [] sur le serveur. Dans Firefox et Safari, la liste transports ne sera pas undefined, mais une liste vide [], ce qui évite les erreurs.

Avertir les utilisateurs qui utilisent des navigateurs non compatibles avec WebAuthn

1e9c1be837d66ce8.png

Bien que WebAuthn soit compatible avec tous les principaux navigateurs, il est recommandé d'afficher un avertissement dans les navigateurs non compatibles.

Dans index.html, vous pouvez remarquer la présence de ce div :

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

Dans le script Inline de index.html, ajoutez le code suivant pour afficher la bannière dans les navigateurs non compatibles avec WebAuthn :

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

Dans une application Web réelle, vous réaliseriez une opération plus élaborée et disposeriez d'un mécanisme de remplacement approprié pour ces navigateurs. Mais vous pouvez voir ici comment vérifier la compatibilité avec WebAuthn.

11. Bravo !

✨ Vous avez terminé !

Vous avez implémenté l'authentification à deux facteurs avec clé de sécurité.

Dans cet atelier de programmation, nous avons vu les principes de base. Voici quelques idées pour explorer plus avant WebAuthn pour l'authentification à deux facteurs :

  • Ajoutez les informations "Last used" (Dernière utilisation) à la fiche d'identifiants. Ces informations aident les utilisateurs à déterminer si une clé de sécurité donnée est activement utilisée ou non, en particulier s'ils ont enregistré plusieurs clés.
  • Appliquez une gestion des erreurs plus robuste et des messages d'erreur plus précis.
  • Examinez auth.js et découvrez ce qui se passe lorsque vous modifiez certains des authSettings, en particulier lorsque vous utilisez une clé compatible avec la validation de l'utilisateur.