Implémenter des clés d'accès avec le remplissage automatique de formulaire dans une application Web

1. Avant de commencer

L'emploi de clés d'accès constitue une excellente alternative aux mots de passe pour renforcer la sécurité des comptes utilisateur sur les sites Web, tout en rendant ces comptes plus faciles à utiliser. Avec une clé d'accès, l'utilisateur peut se connecter à un site Web ou à une application par le biais d'une fonctionnalité de verrouillage de l'écran de l'appareil (par exemple, empreinte digitale, reconnaissance faciale ou code PIN). Vous devez créer une clé d'accès, l'associer à un compte utilisateur et stocker sa clé publique sur un serveur afin qu'un utilisateur puisse s'en servir pour se connecter.

Dans cet atelier de programmation, vous allez transformer une procédure de connexion standard de type formulaire (avec nom d'utilisateur et mot de passe) en une procédure qui prend en charge les clés d'accès et inclut les éléments suivants :

  • Bouton permettant de créer une clé d'accès une fois l'utilisateur connecté
  • UI affichant une liste des clés d'accès enregistrées
  • Formulaire de connexion (existant) permettant aux utilisateurs de se connecter avec une clé d'accès enregistrée via le remplissage automatique de formulaire

Prérequis

Points abordés

  • Créer une clé d'accès
  • Authentifier les utilisateurs avec une clé d'accès
  • Permettre à un formulaire de suggérer une clé d'accès comme option de connexion

Ce dont vous avez besoin

L'une des combinaisons "appareil-logiciel" suivantes :

  • Google Chrome avec un appareil Android exécutant Android 9 ou une version ultérieure, de préférence avec un capteur biométrique
  • Chrome avec un appareil Windows exécutant Windows 10 ou une version ultérieure
  • Safari 16 ou version ultérieure avec un iPhone exécutant iOS 16 ou une version ultérieure, ou avec un iPad exécutant iPadOS 16 ou une version ultérieure
  • Safari 16 ou version ultérieure ou Chrome, avec un ordinateur de bureau Apple exécutant macOS Ventura ou une version ultérieure

2. Configuration

Dans cet atelier de programmation, vous allez utiliser le service Glitch qui vous permet de modifier le code côté client et côté serveur avec JavaScript, et de le déployer exclusivement à partir du navigateur.

Ouvrir le projet

  1. Ouvrez le projet dans Glitch.
  2. Cliquez sur Remix (Remixer) pour dupliquer le projet Glitch.
  3. Dans le menu de navigation en bas de Glitch, cliquez sur Preview > Preview in a new window (Aperçu > Prévisualiser dans une nouvelle fenêtre). Un autre onglet s'ouvre dans votre navigateur.

Bouton "Preview in a new window" (Prévisualiser dans une nouvelle fenêtre) dans le menu de navigation en bas de Glitch

Examiner l'état initial du site Web

  1. Dans l'onglet d'aperçu, saisissez un nom d'utilisateur aléatoire, puis cliquez sur Next (Suivant).
  2. Saisissez un mot de passe aléatoire, puis cliquez sur Sign-in (Connexion). Le mot de passe est ignoré, mais vous êtes tout de même authentifié et accédez la page d'accueil.
  3. Vous pouvez modifier votre nom à afficher si vous le souhaitez. C'est la seule action que vous pouvez effectuer à l'état initial.
  4. Cliquez sur Sign out (Déconnexion).

Dans cet état, les utilisateurs doivent saisir un mot de passe chaque fois qu'ils se connectent. Vous devez ajouter la prise en charge des clés d'accès à ce formulaire afin que les utilisateurs puissent se connecter à l'aide de la fonctionnalité de verrouillage de l'écran de leur appareil. Vous pouvez essayer l'état final à l'adresse https://passkeys-codelab.glitch.me/.

Pour en savoir plus sur le fonctionnement des clés d'accès, consultez Comment fonctionnent les clés d'accès.

3. Ajouter la possibilité de créer une clé d'accès

Pour permettre aux utilisateurs de s'authentifier avec une clé d'accès, vous devez leur donner la possibilité de créer et d'enregistrer une clé d'accès, puis de stocker sa clé publique sur le serveur.

Une boîte de dialogue de validation de l'utilisateur de la clé d'accès s'affiche lors de la création de la clé d'accès.

Vous souhaitez autoriser la création d'une clé d'accès une fois l'utilisateur connecté via un mot de passe, et ajouter une UI qui lui permet de créer une clé d'accès et de consulter la liste de toutes les clés d'accès enregistrées sur la page /home. Dans la section suivante, vous allez développer une fonction permettant de créer et d'enregistrer une clé d'accès.

Créer la fonction registerCredential()

  1. Dans Glitch, accédez au fichier public/client.js, puis faites-le défiler jusqu'en bas.
  2. Après le commentaire approprié, ajoutez la fonction registerCredential() suivante :

public/client.js

// TODO: Add an ability to create a passkey: Create the registerCredential() function.
export async function registerCredential() {

  // TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.

  // TODO: Add an ability to create a passkey: Create a credential.

  // TODO: Add an ability to create a passkey: Register the credential to the server endpoint.

};

Cette fonction crée une clé d'accès et l'enregistre sur le serveur.

Récupérer la question d'authentification et d'autres options auprès du point de terminaison du serveur

Pour qu'il soit possible de créer une clé d'accès, vous devez demander au serveur les paramètres à transmettre à WebAuthn, y compris une question d'authentification. WebAuthn est une API de navigateur qui permet à un utilisateur de créer une clé d'accès et de s'en servir pour s'authentifier. Par chance, vous disposez déjà d'un point de terminaison de serveur qui fournit ces paramètres dans cet atelier de programmation.

  • Pour récupérer la question d'authentification et d'autres options auprès du point de terminaison du serveur, ajoutez le code suivant au corps de la fonction registerCredential() après le commentaire approprié :

public/client.js

// TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.
const options = await _fetch('/auth/registerRequest');

L'extrait de code suivant inclut des exemples des options que vous recevez du serveur :

{
  challenge: *****,
  rp: {
    id: "example.com",
  },
  user: {
    id: *****,
    name: "john78",
    displayName: "John",
  },
  pubKeyCredParams: [{
    alg: -7, type: "public-key"
  },{
    alg: -257, type: "public-key"
  }],
  excludeCredentials: [{
    id: *****,
    type: 'public-key',
    transports: ['internal', 'hybrid'],
  }],
  authenticatorSelection: {
    authenticatorAttachment: "platform",
    requireResidentKey: true,
  }
}

La spécification WebAuthn n'inclut pas le protocole d'échange entre un serveur et un client. Cependant, le serveur de cet atelier de programmation est conçu pour renvoyer un fichier JSON aussi semblable que possible au dictionnaire PublicKeyCredentialCreationOptions transmis à l'API WebAuthn navigator.credentials.create().

Le tableau suivant n'est pas exhaustif, mais contient les principaux paramètres du dictionnaire PublicKeyCredentialCreationOptions :

Paramètres

Descriptions

challenge

Question d'authentification générée par le serveur dans un objet ArrayBuffer pour cet enregistrement. Ce paramètre est obligatoire mais n'est pas utilisé lors de l'enregistrement, sauf si vous effectuez une attestation (sujet avancé non traité dans cet atelier de programmation).

user.id

Identifiant unique d'un utilisateur. Cette valeur doit être un objet ArrayBuffer n'incluant pas de données personnelles (par exemple, adresses e-mail ou noms d'utilisateur). La génération d'une valeur aléatoire de 16 octets pour chaque compte est parfaitement adaptée.

user.name

Ce champ doit contenir un identifiant unique pour le compte, reconnaissable par l'utilisateur, comme son adresse e-mail ou son nom d'utilisateur. Il s'affiche dans le sélecteur de compte. (Pour un nom d'utilisateur, spécifiez la même valeur que pour l'authentification par mot de passe.)

user.displayName

Ce champ facultatif permet de spécifier un nom convivial pour le compte. Il ne doit pas nécessairement être unique. Il peut s'agir d'un nom choisi par l'utilisateur. Si vous ne disposez pas d'une valeur appropriée à inclure dans ce champ pour votre site Web, transmettez une chaîne vide. Ce nom peut s'afficher dans le sélecteur de compte selon le navigateur.

rp.id

Un ID de partie de confiance (RP) est un domaine. Un site Web peut spécifier son propre domaine ou un suffixe enregistrable. Par exemple, si l'origine d'une RP est https://login.example.com:1337, l'ID de RP peut être login.example.com ou example.com. Si l'ID de RP est example.com, l'utilisateur peut s'authentifier sur login.example.com ou sur tout autre sous-domaine de example.com.

pubKeyCredParams

Ce champ indique les algorithmes de clé publique acceptés par la RP. Nous vous recommandons de le définir sur [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}]. Il indique la compatibilité de l'algorithme ECDSA avec P-256 et RSA PKCS#1. Leur prise en charge vous permet de bénéficier d'une couverture complète.

excludeCredentials

Fournit une liste des identifiants déjà enregistrés pour empêcher l'enregistrement du même appareil à deux reprises. Si ce paramètre est fourni, le membre transports doit contenir le résultat de l'appel à la fonction getTransports() lors de l'enregistrement de chaque identifiant.

authenticatorSelection.authenticatorAttachment

Définissez ce paramètre sur une valeur "platform". Cela signifie que vous souhaitez un authentificateur intégré à l'appareil de plate-forme, de sorte que l'utilisateur ne soit pas invité à ajouter un élément (par exemple, une clé de sécurité USB).

authenticatorSelection.requireResidentKey

Définissez ce paramètre sur une valeur booléenne true. Un identifiant détectable (clé résidente) peut être utilisé sans que le serveur n'ait à fournir d'ID. Cette approche est compatible avec la saisie automatique.

authenticatorSelection.userVerification

Définissez la valeur sur "preferred" ou omettez-la, il s'agit de la valeur par défaut. Ce paramètre indique si la validation de l'utilisateur à l'aide du verrouillage de l'écran de l'appareil est "required", "preferred" ou "discouraged". La valeur "preferred" permet de demander la validation de l'utilisateur lorsque l'appareil en est capable.

Créer un identifiant

  1. Dans le corps de la fonction registerCredential(), après le commentaire approprié, reconvertissez certains paramètres encodés en Base64URL en binaires, plus précisément les chaînes user.id et challenge, ainsi que les instances de la chaîne id incluses dans le tableau excludeCredentials :

public/client.js

// TODO: Add an ability to create a passkey: Create a credential.
// Base64URL decode some values.
options.user.id = base64url.decode(options.user.id);
options.challenge = base64url.decode(options.challenge);

if (options.excludeCredentials) {
  for (let cred of options.excludeCredentials) {
    cred.id = base64url.decode(cred.id);
  }
}
  1. Sur la ligne suivante, définissez authenticatorSelection.authenticatorAttachment sur "platform" et authenticatorSelection.requireResidentKey sur true. Ainsi, seule l'utilisation d'un authentificateur de plate-forme (l'appareil lui-même) avec une fonctionnalité d'identification détectable est autorisée.

public/client.js

// Use platform authenticator and discoverable credential.
options.authenticatorSelection = {
  authenticatorAttachment: 'platform',
  requireResidentKey: true
}
  1. Sur la ligne suivante, appelez la méthode navigator.credentials.create() pour créer un identifiant.

public/client.js

// Invoke the WebAuthn create() method.
const cred = await navigator.credentials.create({
  publicKey: options,
});

Avec cet appel, le navigateur tente de valider l'identité de l'utilisateur à l'aide du verrouillage de l'écran de l'appareil.

Enregistrer l'identifiant sur le point de terminaison du serveur

Une fois que l'utilisateur a validé son identité, une clé d'accès est créée et stockée. Le site Web reçoit un objet d'identification qui contient une clé publique que vous pouvez envoyer au serveur pour enregistrer la clé d'accès.

L'extrait de code suivant contient un exemple d'objet d'identification :

{
  "id": *****,
  "rawId": *****,
  "type": "public-key",
  "response": {
    "clientDataJSON": *****,
    "attestationObject": *****,
    "transports": ["internal", "hybrid"]
  },
  "authenticatorAttachment": "platform"
}

Le tableau suivant n'est pas exhaustif, mais contient les principaux paramètres de l'objet PublicKeyCredential :

Paramètres

Descriptions

id

ID de la clé d'accès créée, encodé en Base64URL. Cet identifiant aide le navigateur à déterminer si une clé d'accès correspondante est enregistrée sur l'appareil au moment de l'authentification. Cette valeur doit être stockée dans la base de données sur le backend.

rawId

Version de l'objet ArrayBuffer de l'ID.

response.clientDataJSON

Données client encodées sous la forme d'un objet ArrayBuffer.

response.attestationObject

Objet d'attestation encodé au format ArrayBuffer. Il contient des informations importantes telles qu'un ID de RP, des options et une clé publique.

response.transports

Liste des transports compatibles avec l'appareil. "internal" signifie que l'appareil est compatible avec une clé d'accès. "hybrid" indique qu'il accepte également l'authentification sur un autre appareil.

authenticatorAttachment

Renvoie "platform" lorsque cet identifiant est créé sur un appareil qui prend en charge les clés d'accès.

Pour envoyer l'objet d'identification au serveur, procédez comme suit :

  1. Encodez les paramètres binaires de l'identifiant en Base64URL afin qu'ils puissent être envoyés au serveur sous forme de chaîne :

public/client.js

// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;

// The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
if (cred.authenticatorAttachment) {
  credential.authenticatorAttachment = cred.authenticatorAttachment;
}

// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const attestationObject = base64url.encode(cred.response.attestationObject);

// Obtain transports.
const transports = cred.response.getTransports ? cred.response.getTransports() : [];

credential.response = {
  clientDataJSON,
  attestationObject,
  transports
};
  1. Sur la ligne suivante, envoyez l'objet au serveur :

public/client.js

return await _fetch('/auth/registerResponse', credential);

Lorsque vous exécutez le programme, le serveur renvoie HTTP code 200, ce qui indique que l'identifiant est enregistré.

Vous disposez maintenant de la fonction registerCredential() complète.

Examiner le code de solution pour cette section

public/client.js

// TODO: Add an ability to create a passkey: Create the registerCredential() function.
export async function registerCredential() {

  // TODO: Add an ability to create a passkey: Obtain the challenge and other options from server endpoint.
  const options = await _fetch('/auth/registerRequest');

  // TODO: Add an ability to create a passkey: Create a credential.
  // Base64URL decode some values.

  options.user.id = base64url.decode(options.user.id);
  options.challenge = base64url.decode(options.challenge);

  if (options.excludeCredentials) {
    for (let cred of options.excludeCredentials) {
      cred.id = base64url.decode(cred.id);
    }
  }

  // Use platform authenticator and discoverable credential.
  options.authenticatorSelection = {
    authenticatorAttachment: 'platform',
    requireResidentKey: true
  }

  // Invoke the WebAuthn create() method.
  const cred = await navigator.credentials.create({
    publicKey: options,
  });

  // TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;

  // The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
  if (cred.authenticatorAttachment) {
    credential.authenticatorAttachment = cred.authenticatorAttachment;
  }

  // Base64URL encode some values.
  const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
  const attestationObject =
  base64url.encode(cred.response.attestationObject);

  // Obtain transports.
  const transports = cred.response.getTransports ?
  cred.response.getTransports() : [];

  credential.response = {
    clientDataJSON,
    attestationObject,
    transports
  };

  return await _fetch('/auth/registerResponse', credential);
};

4. Créer une UI pour enregistrer et gérer les identifiants de type clé d'accès

Maintenant que la fonction registerCredential() est disponible, vous avez besoin d'un bouton pour l'appeler. Vous devez également afficher la liste des clés d'accès enregistrées.

Clés d'accès enregistrées répertoriées sur la page /home

Ajouter un espace réservé HTML

  1. Dans Glitch, accédez au fichier views/home.html.
  2. Après le commentaire approprié, ajoutez un espace réservé pour l'UI qui affiche un bouton permettant d'enregistrer une clé d'accès et une liste de clés d'accès :

views/home.html

​​<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
  <h3 class="mdc-typography mdc-typography--headline6"> Your registered
  passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-button>

L'élément div#list est l'espace réservé pour la liste.

Vérifier si les clés d'accès sont acceptées

Pour n'afficher l'option permettant de créer une clé d'accès que pour les utilisateurs dont les appareils sont compatibles avec les clés d'accès, vous devez d'abord vérifier si WebAuthn est disponible. Si tel est le cas, vous devez ensuite supprimer la classe hidden pour afficher le bouton Create a passkey (Créer une clé d'accès).

Pour vérifier si un environnement est compatible avec les clés d'accès, procédez comme suit :

  1. À la fin du fichier views/home.html, après le commentaire approprié, écrivez une instruction conditionnelle qui s'exécute si la valeur de window.PublicKeyCredential, PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable et PublicKeyCredential.isConditionalMediationAvailable est true.

views/home.html

// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');
// Feature detections
if (window.PublicKeyCredential &&
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  1. Dans le corps de l'instruction conditionnelle, vérifiez si l'appareil peut créer une clé d'accès, puis si la clé d'accès peut être suggérée pour le remplissage automatique de formulaire.

views/home.html

try {
  const results = await Promise.all([

    // Is platform authenticator available in this browser?
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),

    // Is conditional UI available in this browser?
    PublicKeyCredential.isConditionalMediationAvailable()
  ]);
  1. Si toutes les conditions sont remplies, affichez le bouton permettant de créer une clé d'accès. Sinon, affichez un message d'avertissement.

views/home.html

    if (results.every(r => r === true)) {

      // If conditional UI is available, reveal the Create a passkey button.
      createPasskey.classList.remove('hidden');
    } else {

      // If conditional UI isn't available, show a message.
      $('#message').innerText = 'This device does not support passkeys.';
    }
  } catch (e) {
    console.error(e);
  }
} else {

  // If WebAuthn isn't available, show a message.
  $('#message').innerText = 'This device does not support passkeys.';
}

Afficher les clés d'accès enregistrées dans une liste

  1. Définissez une fonction renderCredentials() qui extrait les clés d'accès enregistrées sur le serveur et les affiche dans une liste. Heureusement, vous disposez déjà du point de terminaison de serveur /auth/getKeys permettant d'extraire les clés d'accès enregistrées pour l'utilisateur connecté.

views/home.html

// TODO: Add an ability to create a passkey: Render registered passkeys in a list.
async function renderCredentials() {
  const res = await _fetch('/auth/getKeys');
  const list = $('#list');
  const creds = html`${res.length > 0 ? html`
    <mwc-list>
      ${res.map(cred => html`
        <mwc-list-item>
          <div class="list-item">
            <div class="entity-name">
              <span>${cred.name || 'Unnamed' }</span>
          </div>
          <div class="buttons">
            <mwc-icon-button data-cred-id="${cred.id}"
            data-name="${cred.name || 'Unnamed' }" @click="${rename}"
            icon="edit"></mwc-icon-button>
            <mwc-icon-button data-cred-id="${cred.id}" @click="${remove}"
            icon="delete"></mwc-icon-button>
          </div>
         </div>
      </mwc-list-item>`)}
  </mwc-list>` : html`
  <mwc-list>
    <mwc-list-item>No credentials found.</mwc-list-item>
  </mwc-list>`}`;
  render(creds, list);
};
  1. Sur la ligne suivante, appelez la fonction renderCredentials() pour afficher les clés d'accès enregistrées dès que l'utilisateur accède à la page /home pour l'initialisation.

views/home.html

renderCredentials();

Créer et enregistrer une clé d'accès

Pour créer et enregistrer une clé d'accès, vous devez appeler la fonction registerCredential() que vous avez implémentée précédemment.

Pour déclencher la fonction registerCredential() lorsque l'utilisateur clique sur le bouton Create a passkey (Créer une clé d'accès), procédez comme suit :

  1. Dans le fichier, après l'espace réservé HTML, recherchez l'instruction import suivante :

views/home.html

import {
  $,
  _fetch,
  loading,
  updateCredential,
  unregisterCredential,
} from '/client.js';
  1. À la fin du corps de l'instruction import, ajoutez la fonction registerCredential().

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
import {
  $,
  _fetch,
  loading,
  updateCredential,
  unregisterCredential,
  registerCredential
} from '/client.js';
  1. À la fin du fichier, après le commentaire approprié, définissez une fonction register() qui appelle la fonction registerCredential() et une UI de chargement, puis qui appelle renderCredentials() après un enregistrement. Cela permet de confirmer que le navigateur crée une clé d'accès et affiche un message d'erreur en cas de problème.

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
async function register() {
  try {

    // Start the loading UI.
    loading.start();

    // Start creating a passkey.
    await registerCredential();

    // Stop the loading UI.
    loading.stop();

    // Render the updated passkey list.
    renderCredentials();
  1. Dans le corps de la fonction register(), interceptez les exceptions. La méthode navigator.credentials.create() génère une erreur InvalidStateError lorsqu'une clé d'accès existe déjà sur l'appareil. Cet examen est effectué avec le tableau excludeCredentials. Dans ce cas, vous affichez un message pertinent à l'utilisateur. Une erreur NotAllowedError est également générée lorsque l'utilisateur ferme la boîte de dialogue d'authentification. Dans ce cas, vous l'ignorez de façon silencieuse.

views/home.html

  } catch (e) {

    // Stop the loading UI.
    loading.stop();

    // An InvalidStateError indicates that a passkey already exists on the device.
    if (e.name === 'InvalidStateError') {
      alert('A passkey already exists for this device.');

    // A NotAllowedError indicates that the user canceled the operation.
    } else if (e.name === 'NotAllowedError') {
      Return;

    // Show other errors in an alert.
    } else {
      alert(e.message);
      console.error(e);
    }
  }
};
  1. Sur la ligne qui suit la fonction register(), associez la fonction register() à un événement click pour le bouton Create a passkey (Créer une clé d'accès).

views/home.html

createPasskey.addEventListener('click', register);

Examiner le code de solution pour cette section

views/home.html

​​<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
  <h3 class="mdc-typography mdc-typography--headline6"> Your registered
  passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-button>

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
import {
  $,
  _fetch,
  loading,
  updateCredential,
  unregisterCredential,
  registerCredential
} from '/client.js';

views/home.html

// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');

// Feature detections
if (window.PublicKeyCredential &&
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  try {
    const results = await Promise.all([

      // Is platform authenticator available in this browser?
      PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),

      // Is conditional UI available in this browser?
      PublicKeyCredential.isConditionalMediationAvailable()
    ]);
    if (results.every(r => r === true)) {

      // If conditional UI is available, reveal the Create a passkey button.
      createPasskey.classList.remove('hidden');
    } else {

      // If conditional UI isn't available, show a message.
      $('#message').innerText = 'This device does not support passkeys.';
    }
  } catch (e) {
    console.error(e);
  }
} else {

  // If WebAuthn isn't available, show a message.
  $('#message').innerText = 'This device does not support passkeys.';
}

// TODO: Add an ability to create a passkey: Render registered passkeys in a list.
async function renderCredentials() {
  const res = await _fetch('/auth/getKeys');
  const list = $('#list');
  const creds = html`${res.length > 0 ? html`
  <mwc-list>
    ${res.map(cred => html`
      <mwc-list-item>
        <div class="list-item">
          <div class="entity-name">
            <span>${cred.name || 'Unnamed' }</span>
          </div>
          <div class="buttons">
            <mwc-icon-button data-cred-id="${cred.id}" data-name="${cred.name || 'Unnamed' }" @click="${rename}" icon="edit"></mwc-icon-button>
            <mwc-icon-button data-cred-id="${cred.id}" @click="${remove}" icon="delete"></mwc-icon-button>
          </div>
        </div>
      </mwc-list-item>`)}
  </mwc-list>` : html`
  <mwc-list>
    <mwc-list-item>No credentials found.</mwc-list-item>
  </mwc-list>`}`;
  render(creds, list);
};

renderCredentials();

// TODO: Add an ability to create a passkey: Create and register a passkey.
async function register() {
  try {

    // Start the loading UI.
    loading.start();

    // Start creating a passkey.
    await registerCredential();

    // Stop the loading UI.
    loading.stop();

    // Render the updated passkey list.
    renderCredentials();
  } catch (e) {

    // Stop the loading UI.
    loading.stop();

    // An InvalidStateError indicates that a passkey already exists on the device.
    if (e.name === 'InvalidStateError') {
      alert('A passkey already exists for this device.');

    // A NotAllowedError indicates that the user canceled the operation.
    } else if (e.name === 'NotAllowedError') {
      Return;

    // Show other errors in an alert.
    } else {
      alert(e.message);
      console.error(e);
    }
  }
};

createPasskey.addEventListener('click', register);

Tester la solution

Si vous avez suivi toutes les étapes précédentes, vous avez implémenté la création, l'enregistrement et l'affichage des clés d'accès sur le site Web.

Pour tester la solution, procédez comme suit :

  1. Dans l'onglet d'aperçu, connectez-vous avec un nom d'utilisateur et un mot de passe aléatoires.
  2. Cliquez sur Create a passkey (Créer une clé d'accès).
  3. Validez votre identité à l'aide du verrouillage de l'écran de l'appareil.
  4. Vérifiez qu'une clé d'accès est enregistrée et affichée dans la section Your registered passkeys (Vos clés d'accès enregistrées) de la page Web.

Clés d'accès enregistrées répertoriées sur la page /home.

Renommer et supprimer des clés d'accès enregistrées

Vous devriez être en mesure de renommer ou de supprimer les clés d'accès enregistrées de la liste. Vous pouvez vérifier comment cela fonctionne dans le code fourni dans l'atelier de programmation.

Pour Chrome, vous pouvez supprimer les clés d'accès enregistrées en accédant à la page chrome://settings/passkeys sur un ordinateur ou via le gestionnaire de mots de passe dans les paramètres sur Android.

Pour savoir comment renommer et supprimer les clés d'accès enregistrées sur d'autres plates-formes, consultez les pages d'assistance respectives de ces plates-formes.

5. Ajouter la possibilité de s'authentifier avec une clé d'accès

Les utilisateurs peuvent désormais créer et enregistrer une clé d'accès, et sont prêts à l'utiliser pour s'authentifier de manière sécurisée sur votre site Web. Vous devez maintenant ajouter une fonctionnalité d'authentification par clé d'accès à votre site Web.

Créer la fonction authenticate()

  • Dans le fichier public/client.js, après le commentaire approprié, créez une fonction appelée authenticate() qui valide l'utilisateur localement, puis sur le serveur :

public/client.js

// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {

  // TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.

  // TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.

  // TODO: Add an ability to authenticate with a passkey: Verify the credential.

};

Récupérer la question d'authentification et d'autres options auprès du point de terminaison du serveur

Avant de demander à l'utilisateur de s'authentifier, vous devez demander au serveur les paramètres à transmettre à WebAuthn, y compris une question d'authentification.

  • Dans le corps de la fonction authenticate(), après le commentaire approprié, appelez la fonction _fetch() pour envoyer une requête POST au serveur :

public/client.js

// TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.
const options = await _fetch('/auth/signinRequest');

Le serveur de cet atelier de programmation est conçu pour renvoyer un fichier JSON aussi semblable que possible au dictionnaire PublicKeyCredentialRequestOptions transmis à l'API WebAuthn navigator.credentials.get(). L'extrait de code suivant inclut des exemples des options que vous devriez recevoir :

{
  "challenge": *****,
  "rpId": "passkeys-codelab.glitch.me",
  "allowCredentials": []
}

Le tableau suivant n'est pas exhaustif, mais contient les principaux paramètres du dictionnaire PublicKeyCredentialRequestOptions :

Paramètres

Descriptions

challenge

Question d'authentification générée par le serveur dans un objet ArrayBuffer. Ce paramètre est nécessaire pour éviter les attaques par rejeu. N'acceptez jamais deux fois la même question d'authentification dans une réponse. Considérez-la comme un jeton CSRF.

rpId

Un ID de RP est un domaine. Un site Web peut spécifier son propre domaine ou un suffixe enregistrable. Cette valeur doit correspondre au paramètre rp.id utilisé lors de la création de la clé d'accès.

allowCredentials

Cette propriété permet de rechercher les authentificateurs éligibles pour cette authentification. Transmettez un tableau vide ou ne spécifiez pas ce paramètre pour permettre au navigateur d'afficher un sélecteur de compte.

userVerification

Définissez la valeur sur "preferred" ou omettez-la, il s'agit de la valeur par défaut. Ce paramètre indique si la validation de l'utilisateur à l'aide du verrouillage de l'écran de l'appareil est "required", "preferred" ou "discouraged". La valeur "preferred" permet de demander la validation de l'utilisateur lorsque l'appareil en est capable.

Valider l'utilisateur localement et obtenir des identifiants

  1. Dans le corps de la fonction authenticate(), après le commentaire approprié, reconvertissez le paramètre challenge en binaire :

public/client.js

// TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.
// Base64URL decode the challenge.
options.challenge = base64url.decode(options.challenge);
  1. Transmettez un tableau vide au paramètre allowCredentials pour ouvrir un sélecteur de compte lorsqu'un utilisateur s'authentifie :

public/client.js

// An empty allowCredentials array invokes an account selector by discoverable credentials.
options.allowCredentials = [];

Le sélecteur de compte utilise les informations de l'utilisateur stockées avec la clé d'accès.

  1. Appelez la méthode navigator.credentials.get() avec une option mediation: 'conditional' :

public/client.js

// Invoke the WebAuthn get() method.
const cred = await navigator.credentials.get({
  publicKey: options,

  // Request a conditional UI.
  mediation: 'conditional'
});

Cette option demande au navigateur de suggérer des clés d'accès de manière conditionnelle pour le remplissage automatique du formulaire.

Valider l'identifiant

Une fois que l'utilisateur a validé son identité localement, vous devriez recevoir un objet d'identification contenant une signature que vous pouvez vérifier sur le serveur.

L'extrait de code suivant inclut un exemple d'objet PublicKeyCredential :

{
  "id": *****,
  "rawId": *****,
  "type": "public-key",
  "response": {
    "clientDataJSON": *****,
    "authenticatorData": *****,
    "signature": *****,
    "userHandle": *****
  },
  authenticatorAttachment: "platform"
}

Le tableau suivant n'est pas exhaustif, mais contient les principaux paramètres de l'objet PublicKeyCredential :

Paramètres

Descriptions

id

ID de la clé d'accès authentifiée, encodé en Base64URL.

rawId

Version de l'objet ArrayBuffer de l'ID.

response.clientDataJSON

Objet ArrayBuffer des données client. Ce champ contient des informations telles que la question d'authentification et l'origine que le serveur de la RP doit vérifier.

response.authenticatorData

Objet ArrayBuffer des données de l'authentificateur. Ce champ contient des informations telles que l'ID de RP.

response.signature

Objet ArrayBuffer de la signature. Cette valeur est l'élément central de l'identifiant et doit être validée sur le serveur.

response.userHandle

Objet ArrayBuffer contenant l'ID utilisateur défini au moment de la création. Cette valeur peut être utilisée à la place de l'identifiant si le serveur a besoin de choisir les valeurs d'identification qu'il utilise ou si le backend souhaite éviter la création d'un index sur les identifiants.

authenticatorAttachment

Renvoie une chaîne "platform" lorsque cet identifiant provient de l'appareil local. Si ce n'est pas le cas, renvoie une chaîne "cross-platform", en particulier lorsque l'utilisateur se connecte à l'aide d'un téléphone. Si l'utilisateur doit se servir d'un téléphone pour se connecter, invitez-le à créer une clé d'accès sur l'appareil local.

Pour envoyer l'objet d'identification au serveur, procédez comme suit :

  1. Dans le corps de la fonction authenticate(), après le commentaire approprié, encodez les paramètres binaires de l'identifiant afin qu'ils puissent être transmis au serveur sous forme de chaîne :

public/client.js

// TODO: Add an ability to authenticate with a passkey: Verify the credential.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;

// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const authenticatorData = base64url.encode(cred.response.authenticatorData);
const signature = base64url.encode(cred.response.signature);
const userHandle = base64url.encode(cred.response.userHandle);

credential.response = {
  clientDataJSON,
  authenticatorData,
  signature,
  userHandle,
};
  1. Envoyez l'objet au serveur :

public/client.js

return await _fetch(`/auth/signinResponse`, credential);

Lorsque vous exécutez le programme, le serveur renvoie HTTP code 200, ce qui indique que l'identifiant est validé.

Vous disposez maintenant de la fonction authentication() complète.

Examiner le code de solution pour cette section

public/client.js

// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {

  // TODO: Add an ability to authenticate with a passkey: Obtain the
  challenge and other options from the server endpoint.
  const options = await _fetch('/auth/signinRequest');

  // TODO: Add an ability to authenticate with a passkey: Locally verify
  the user and get a credential.
  // Base64URL decode the challenge.
  options.challenge = base64url.decode(options.challenge);

  // The empty allowCredentials array invokes an account selector
  by discoverable credentials.
  options.allowCredentials = [];

  // Invoke the WebAuthn get() function.
  const cred = await navigator.credentials.get({
    publicKey: options,

    // Request a conditional UI.
    mediation: 'conditional'
  });

  // TODO: Add an ability to authenticate with a passkey: Verify the credential.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;

  // Base64URL encode some values.
  const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
  const authenticatorData =
  base64url.encode(cred.response.authenticatorData);
  const signature = base64url.encode(cred.response.signature);
  const userHandle = base64url.encode(cred.response.userHandle);

  credential.response = {
    clientDataJSON,
    authenticatorData,
    signature,
    userHandle,
  };

  return await _fetch(`/auth/signinResponse`, credential);
};

6. Ajouter des clés d'accès à la saisie automatique du navigateur

Lorsque l'utilisateur revient, vous souhaitez que la procédure de connexion soit aussi facile et sécurisée que possible. Si vous ajoutez un bouton Se connecter avec une clé d'accès à la page de connexion, l'utilisateur peut appuyer sur le bouton, sélectionner une clé d'accès dans le sélecteur de compte du navigateur, puis valider son identité à l'aide du verrouillage de l'écran.

Toutefois, la transition d'un mot de passe à une clé d'accès ne se produit pas pour tous les utilisateurs en même temps. Cela implique que vous ne pouvez pas vous débarrasser des mots de passe tant que tous les utilisateurs ne sont pas passés aux clés d'accès. Vous devez donc conserver le formulaire de connexion basé sur un mot de passe en attendant. Toutefois, si vous laissez un formulaire basé sur un mot de passe et un bouton de clé d'accès, les utilisateurs devront faire un choix superflu entre les deux pour se connecter. Idéalement, vous aimeriez un processus de connexion simple.

C'est là qu'une UI conditionnelle entre en jeu. Une UI conditionnelle est une fonctionnalité WebAuthn qui vous permet de créer un champ de formulaire pour suggérer une clé d'accès comme élément de saisie automatique en plus des mots de passe. Si un utilisateur appuie sur une clé d'accès dans les suggestions de saisie automatique, il est invité à utiliser le verrouillage de l'écran de l'appareil pour valider son identité localement. Cette expérience utilisateur est fluide, car l'action de l'utilisateur est presque identique à celle d'une connexion par mot de passe.

Clé d'accès suggérée pour le remplissage automatique de formulaire.

Activer une UI conditionnelle

Pour activer une UI conditionnelle, il vous suffit d'ajouter un jeton webauthn dans l'attribut autocomplete d'un champ de saisie. Une fois le jeton défini, vous pouvez appeler la méthode navigator.credentials.get() avec la chaîne mediation: 'conditional' pour déclencher l'UI de verrouillage de l'écran de manière conditionnelle.

  • Pour activer une UI conditionnelle, remplacez les champs de saisie de nom d'utilisateur existants par le code HTML suivant après le commentaire approprié dans le fichier view/index.html :

view/index.html

<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<input
  type="text"
  id="username"
  class="mdc-text-field__input"
  aria-labelledby="username-label"
  name="username"
  autocomplete="username webauthn"
  autofocus />

Détecter des fonctionnalités, appeler WebAuthn et activer une UI conditionnelle

  1. Dans le fichier view/index.html, après le commentaire approprié, remplacez l'instruction import existante par le code suivant :

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import {
  $,
  _fetch,
  loading,
  authenticate
} from "/client.js";

Ce code importe la fonction authenticate() que vous avez implémentée précédemment.

  1. Vérifiez que l'objet window.PulicKeyCredential est disponible et que la méthode PublicKeyCredential.isConditionalMediationAvailable() renvoie une valeur true, puis appelez la fonction authenticate() :

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
if (
  window.PublicKeyCredential &&
  PublicKeyCredential.isConditionalMediationAvailable
) {
  try {

    // Is conditional UI available in this browser?
    const cma =
      await PublicKeyCredential.isConditionalMediationAvailable();
    if (cma) {

      // If conditional UI is available, invoke the authenticate() function.
      const user = await authenticate();
      if (user) {

        // Proceed only when authentication succeeds.
        $("#username").value = user.username;
        loading.start();
        location.href = "/home";
      } else {
        throw new Error("User not found.");
      }
    }
  } catch (e) {
    loading.stop();

    // A NotAllowedError indicates that the user canceled the operation.
    if (e.name !== "NotAllowedError") {
      console.error(e);
      alert(e.message);
    }
  }
}

Examiner le code de solution pour cette section

view/index.html

<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<input
  type="text"
  id="username"
  class="mdc-text-field__input"
  aria-labelledby="username-label"
  name="username"
  autocomplete="username webauthn"
  autofocus
/>

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import {
  $,
  _fetch,
  loading,
  authenticate
} from '/client.js';

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
// Is WebAuthn avaiable in this browser?
if (window.PublicKeyCredential &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  try {

    // Is a conditional UI available in this browser?
    const cma= await PublicKeyCredential.isConditionalMediationAvailable();
    if (cma) {

      // If a conditional UI is available, invoke the authenticate() function.
      const user = await authenticate();
      if (user) {

        // Proceed only when authentication succeeds.
        $('#username').value = user.username;
        loading.start();
        location.href = '/home';
      } else {
        throw new Error('User not found.');
      }
    }
  } catch (e) {
    loading.stop();

    // A NotAllowedError indicates that the user canceled the operation.
    if (e.name !== 'NotAllowedError') {
      console.error(e);
      alert(e.message);
    }
  }
}

Tester la solution

Vous avez implémenté la création, l'enregistrement, l'affichage et l'authentification de clés d'accès sur votre site Web.

Pour tester la solution, procédez comme suit :

  1. Accédez à l'onglet d'aperçu.
  2. Si nécessaire, déconnectez-vous.
  3. Cliquez sur la zone de texte du nom d'utilisateur. Une boîte de dialogue s'affiche.
  4. Sélectionnez le compte avec lequel vous souhaitez vous connecter.
  5. Validez votre identité à l'aide du verrouillage de l'écran de l'appareil. Vous êtes redirigé vers la page /home et vous êtes connecté.

Une boîte de dialogue vous invite à valider votre identité à l'aide du mot de passe ou de la clé d'accès enregistré.

7. Félicitations !

Vous avez terminé cet atelier de programmation. Si vous avez des questions, posez-les sur la liste de diffusion FIDO-DEV ou sur StackOverflow en ajoutant un tag passkey.

En savoir plus