Enregistrement de clés d'accès côté serveur

Présentation

Voici un aperçu général des étapes clés de l'enregistrement des clés d'accès:

Flux d'enregistrement d'une clé d'accès

  • Définissez les options pour créer une clé d'accès. Envoyez-les au client afin de les transmettre à votre appel de création de clé d'accès: l'appel d'API WebAuthn navigator.credentials.create sur le Web et credentialManager.createCredential sur Android. Une fois que l'utilisateur a confirmé la création de la clé d'accès, l'appel de création de la clé d'accès est résolu et renvoie un identifiant PublicKeyCredential.
  • Vérifiez les identifiants et stockez-les sur le serveur.

Les sections suivantes présentent les spécificités de chaque étape.

<ph type="x-smartling-placeholder">

Créer des options de création d'identifiants

La première étape à effectuer sur le serveur consiste à créer un objet PublicKeyCredentialCreationOptions.

Pour ce faire, utilisez votre bibliothèque FIDO côté serveur. Il propose généralement une fonction utilitaire capable de créer ces options à votre place. SimpleWebAuthn propose, par exemple, generateRegistrationOptions.

PublicKeyCredentialCreationOptions doit inclure tous les éléments nécessaires à la création des clés d'accès: informations sur l'utilisateur, sur la RP et configuration des propriétés des identifiants que vous créez. Une fois que vous avez défini tous ces éléments, transmettez-les si nécessaire à la fonction de votre bibliothèque FIDO côté serveur chargée de créer l'objet PublicKeyCredentialCreationOptions.

Certaines de PublicKeyCredentialCreationOptions peuvent être des constantes. D'autres doivent être définies de manière dynamique sur le serveur:

  • rpId: pour renseigner l'ID de RP sur le serveur, utilisez des fonctions ou des variables côté serveur qui vous donnent le nom d'hôte de votre application Web (par exemple, example.com).
  • user.name et user.displayName:pour renseigner ces champs, utilisez les informations de la session de l'utilisateur connecté (ou les informations du nouveau compte utilisateur, si l'utilisateur crée une clé d'accès lors de l'inscription). user.name est généralement une adresse e-mail unique pour le tiers assujetti à des restrictions. user.displayName est un nom convivial. Notez que toutes les plates-formes n'utilisent pas displayName.
  • user.id: chaîne aléatoire et unique générée lors de la création du compte. Il doit être permanent, contrairement à un nom d'utilisateur qui peut être modifié. L'ID utilisateur permet d'identifier un compte, mais ne doit contenir aucune information permettant d'identifier personnellement l'utilisateur. Vous disposez probablement déjà d'un ID utilisateur dans votre système, mais si nécessaire, créez-en un spécifiquement pour les clés d'accès afin d'éviter toute information permettant d'identifier personnellement l'utilisateur.
  • excludeCredentials: liste des identifiants existants ID pour empêcher la duplication d'une clé d'accès provenant du fournisseur de clés d'accès. Pour renseigner ce champ, recherchez dans votre base de données les identifiants existants pour cet utilisateur. Pour en savoir plus, consultez Empêcher la création d'une clé d'accès s'il en existe déjà une.
  • challenge: pour l'enregistrement des identifiants, la question d'authentification n'est pas pertinente, sauf si vous utilisez l'attestation, une technique plus avancée permettant de vérifier l'identité d'un fournisseur de clés d'accès et les données qu'il émet. Toutefois, même si vous n'utilisez pas l'attestation, la question d'authentification reste un champ obligatoire. Dans ce cas, vous pouvez définir ce défi sur un seul 0 pour plus de simplicité. Pour savoir comment créer une question d'authentification sécurisée, consultez Authentification par clé d'accès côté serveur.

Encodage et décodage

<ph type="x-smartling-placeholder">
</ph> PublicKeyCredentialCreationOptions envoyé par le serveur
PublicKeyCredentialCreationOptions envoyé par le serveur. challenge, user.id et excludeCredentials.credentials doivent être encodés côté serveur en base64URL, pour que PublicKeyCredentialCreationOptions puisse être transmis via HTTPS.

PublicKeyCredentialCreationOptions incluent des champs de type ArrayBuffer. Ils ne sont donc pas compatibles avec JSON.stringify(). Cela signifie qu'à l'heure actuelle, pour diffuser PublicKeyCredentialCreationOptions via HTTPS, certains champs doivent être encodés manuellement sur le serveur à l'aide de base64URL, puis décodés sur le client.

  • Sur le serveur, l'encodage et le décodage sont généralement gérés par votre bibliothèque FIDO côté serveur.
  • Sur le client, l'encodage et le décodage doivent être effectués manuellement pour le moment. Ce sera plus facile à l'avenir: une méthode permettant de convertir les options au format JSON en PublicKeyCredentialCreationOptions sera disponible. Vérifiez l'état de l'implémentation dans Chrome.

Exemple de code: créer des options de création d'identifiants

Nous utilisons la bibliothèque SimpleWebAuthn dans nos exemples. Ici, nous transférons la création d'options d'identifiants de clé publique à sa fonction generateRegistrationOptions.

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

router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
  const { user } = res.locals;
  // Ensure you nest verification function calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // `excludeCredentials` prevents users from re-registering existing
    // credentials for a given passkey provider
    const excludeCredentials = [];
    const credentials = Credentials.findByUserId(user.id);
    if (credentials.length > 0) {
      for (const cred of credentials) {
        excludeCredentials.push({
          id: isoBase64URL.toBuffer(cred.id),
          type: 'public-key',
          transports: cred.transports,
        });
      }
    }

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

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

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

Stocker la clé publique

<ph type="x-smartling-placeholder">
</ph> PublicKeyCredentialCreationOptions envoyé par le serveur
navigator.credentials.create renvoie un objet PublicKeyCredential.

Lorsque navigator.credentials.create est résolu correctement sur le client, cela signifie qu'une clé d'accès a bien été créée. Un objet PublicKeyCredential est renvoyé.

L'objet PublicKeyCredential contient un objet AuthenticatorAttestationResponse, qui représente la réponse du fournisseur de clé d'accès à l'instruction du client pour créer une clé d'accès. Il contient des informations sur les nouveaux identifiants dont vous avez besoin en tant que RP pour authentifier l'utilisateur ultérieurement. Pour en savoir plus sur AuthenticatorAttestationResponse, consultez l'annexe AuthenticatorAttestationResponse.

Envoyez l'objet PublicKeyCredential au serveur. Une fois que vous l'avez reçu, validez-le.

Transmettez cette étape de validation à votre bibliothèque FIDO côté serveur. Il offre généralement une fonction utilitaire à cet effet. SimpleWebAuthn propose, par exemple, verifyRegistrationResponse. Pour en savoir plus, consultez l'annexe: Vérification de la réponse apportée à l'inscription.

Une fois la validation effectuée, stockez les informations d'identification dans votre base de données afin que l'utilisateur puisse s'authentifier ultérieurement avec la clé d'accès associée à ces identifiants.

Utilisez une table dédiée aux identifiants de clé publique associés aux clés d'accès. Un utilisateur ne peut avoir qu'un seul mot de passe, mais peut avoir plusieurs clés d'accès (par exemple, une clé d'accès synchronisée via le trousseau Apple iCloud et une autre via le Gestionnaire de mots de passe de Google).

Voici un exemple de schéma que vous pouvez utiliser pour stocker les informations d'identification:

Schéma de base de données pour les clés d&#39;accès

  • Users (Utilisateurs) : <ph type="x-smartling-placeholder">
      </ph>
    • user_id: ID de l'utilisateur principal. ID aléatoire, unique et permanent de l'utilisateur. Utilisez-la comme clé primaire pour votre table Utilisateurs.
    • username Nom d'utilisateur défini par l'utilisateur, potentiellement modifiable.
    • passkey_user_id: ID utilisateur sans informations permettant d'identifier personnellement l'utilisateur propre aux clés d'accès, représenté par user.id dans vos options d'enregistrement. Lorsque l'utilisateur tentera ultérieurement de s'authentifier, l'authentificateur mettra passkey_user_id à disposition dans sa réponse d'authentification dans userHandle. Nous vous recommandons de ne pas définir passkey_user_id comme clé primaire. Les clés primaires ont tendance à devenir de facto des informations permettant d'identifier personnellement l'utilisateur dans les systèmes, car elles sont très utilisées.
  • Public key credentials (Identifiants de clé publique) : <ph type="x-smartling-placeholder">
      </ph>
    • id: ID de l'identifiant. Utilisez-le comme clé primaire pour votre tableau Identifiants de clé publique.
    • public_key: clé publique de l'identifiant.
    • passkey_user_id: utilisez-la comme clé étrangère pour établir un lien avec la table Users.
    • backed_up: une clé d'accès est sauvegardée si elle est synchronisée par le fournisseur de la clé d'accès. Le stockage de l'état de sauvegarde est utile si vous souhaitez envisager de supprimer les mots de passe à l'avenir pour les utilisateurs qui détiennent des clés d'accès backed_up. Vous pouvez vérifier si la clé d'accès est sauvegardée en examinant les indicateurs dans authenticatorData ou en utilisant une fonctionnalité de bibliothèque côté serveur FIDO généralement disponible pour vous permettre d'accéder facilement à ces informations. L'enregistrement de l'éligibilité à la sauvegarde peut être utile pour répondre aux demandes potentielles des utilisateurs.
    • name : (facultatif) nom à afficher pour l'identifiant afin de permettre aux utilisateurs d'attribuer des noms personnalisés aux identifiants.
    • transports: tableau de transports. Le stockage des transports est utile pour l'expérience utilisateur liée à l'authentification. Lorsque des transports sont disponibles, le navigateur peut se comporter en conséquence et afficher une UI correspondant au transport utilisé par le fournisseur de clés d'accès pour communiquer avec les clients, en particulier pour les cas d'utilisation de la réauthentification où allowCredentials n'est pas vide.

D'autres informations peuvent être utiles à stocker pour améliorer l'expérience utilisateur, y compris des éléments tels que le fournisseur de la clé d'accès, l'heure de création des identifiants et l'heure de la dernière utilisation. Pour en savoir plus, consultez Conception de l'interface utilisateur des clés d'accès.

Exemple de code: stocker les identifiants

Nous utilisons la bibliothèque SimpleWebAuthn dans nos exemples. Ici, nous transférons la validation de la réponse d'inscription à sa fonction verifyRegistrationResponse.

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


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

  // Ensure you nest verification function calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // Verify the credential
    const { verified, registrationInfo } = await verifyRegistrationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      requireUserVerification: false,
    });

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

    const { credentialPublicKey, credentialID } = registrationInfo;

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

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

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

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

Annexe: AuthenticatorAttestationResponse

AuthenticatorAttestationResponse contient deux objets importants:

  • response.clientDataJSON est une version JSON des données client, qui sur le Web correspond aux données visibles par le navigateur. Il contient l'origine de la RP, le défi et androidPackageName si le client est une application Android. En tant que tiers assujetti à des restrictions, la lecture clientDataJSON vous donne accès aux informations que le navigateur a vues au moment de la requête create.
  • response.attestationObjectcontient deux informations: <ph type="x-smartling-placeholder">
      </ph>
    • attestationStatement, ce qui n'est pas pertinent, sauf si vous utilisez une attestation.
    • authenticatorData correspond aux données vues par le fournisseur de clés d'accès. En tant que RP, lire authenticatorData vous donne accès aux données vues par le fournisseur de clé d'accès et renvoyées au moment de la requête create.

authenticatorData contient des informations essentielles sur les identifiants de clé publique associés à la clé d'accès nouvellement créée:

  • Les identifiants de la clé publique proprement dit et un identifiant unique correspondant.
  • ID de RP associé au justificatif.
  • Indicateurs qui décrivent l'état de l'utilisateur lors de la création de la clé d'accès, c'est-à-dire s'il était réellement présent et s'il a bien été validé (voir userVerification).
  • AAGUID, qui identifie le fournisseur de clés d'accès. Il peut être utile d'afficher le fournisseur de clés d'accès pour vos utilisateurs, en particulier s'ils ont enregistré une clé d'accès pour votre service auprès de plusieurs fournisseurs de clés d'accès.

Même si authenticatorData est imbriqué dans attestationObject, les informations qu'il contient sont nécessaires à l'implémentation de votre clé d'accès, que vous utilisiez ou non l'attestation. authenticatorData est encodé et contient des champs encodés au format binaire. Votre bibliothèque côté serveur se charge généralement de l'analyse et du décodage. Si vous n'utilisez pas de bibliothèque côté serveur, envisagez d'utiliser getAuthenticatorData() côté client pour vous éviter des tâches d'analyse et de décodage côté serveur.

Annexe: vérification de la réponse à l'inscription

En arrière-plan, la vérification de la réponse d'enregistrement consiste à effectuer les vérifications suivantes:

  • Assurez-vous que l'ID du tiers assujetti à des restrictions correspond à votre site.
  • Assurez-vous que l'origine de la requête est une origine attendue pour votre site (URL du site principal, application Android).
  • Si vous exigez la validation de l'utilisateur, assurez-vous que l'indicateur de validation authenticatorData.uv est true. Vérifiez que l'indicateur de présence de l'utilisateur authenticatorData.up est défini sur true, car la présence de l'utilisateur est toujours requise pour les clés d'accès.
  • Vérifiez que le client a pu relever le défi que vous lui avez posé. Si vous n'utilisez pas l'attestation, cette vérification n'est pas importante. Toutefois, l'implémentation de cette vérification est une bonne pratique. Elle garantit que votre code est prêt si vous décidez d'utiliser l'attestation à l'avenir.
  • Assurez-vous que l'ID d'identification n'est encore enregistré pour aucun utilisateur.
  • Vérifiez que l'algorithme utilisé par le fournisseur de clé d'accès pour créer l'identifiant est un algorithme que vous avez listé (dans chaque champ alg de publicKeyCredentialCreationOptions.pubKeyCredParams, qui est généralement défini dans votre bibliothèque côté serveur et n'est pas visible par vous). Ainsi, les utilisateurs ne peuvent s'inscrire qu'avec les algorithmes que vous avez choisi d'autoriser.

Pour en savoir plus, consultez le code source pour verifyRegistrationResponse de SimpleWebAuthn ou consultez la liste complète des validations dans les spécifications.

Étape suivante

Authentification par clé d'accès côté serveur