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

Présentation

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

Flux d'authentification par clé d'accès

  • Définissez la question d'authentification et les autres options nécessaires pour l'authentification avec une clé d'accès. Envoyez-les au client afin de pouvoir les transmettre à votre appel d'authentification par clé d'accès (navigator.credentials.get sur le Web). Une fois que l'utilisateur a confirmé l'authentification par clé d'accès, l'appel d'authentification par clé d'accès est résolu et renvoie un identifiant (PublicKeyCredential). L'identifiant contient une assertion d'authentification.
  • Vérifiez l'assertion d'authentification.
  • Si l'assertion d'authentification est valide, authentifiez l'utilisateur.

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

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

Créer le défi

En pratique, un défi est un tableau d'octets aléatoires, représenté sous la forme d'un objet ArrayBuffer.

// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8

Pour vous assurer que le défi remplit son objectif, vous devez:

  1. Assurez-vous que le même test ne doit jamais être utilisé plusieurs fois. Générez un nouveau test à chaque tentative de connexion. Annulez le défi après chaque tentative de connexion, qu'elle ait réussi ou échoué. Vous pouvez aussi supprimer le défi après un certain temps. N'acceptez jamais plus d'une fois la même question d'authentification dans une réponse.
  2. Assurez-vous que le test est sécurisé de manière cryptographique. Un défi doit être pratiquement impossible à deviner. Pour créer un test sécurisé de manière cryptographique côté serveur, il est préférable de vous fier à une bibliothèque côté serveur FIDO de confiance. Si vous créez vos propres tests, utilisez la fonctionnalité cryptographique intégrée disponible dans votre pile technologique ou recherchez des bibliothèques conçues pour les cas d'utilisation de cryptographie. Exemples : iso-crypto en Node.js ou secrets en Python. Conformément à la spécification, la question d'authentification doit comporter au moins 16 octets pour être considérée comme sécurisée.

Une fois que vous avez créé un test, enregistrez-le dans la session de l'utilisateur pour le vérifier plus tard.

Créer des options de demande d'identifiants

Créez des options de demande d'identifiants en tant qu'objet publicKeyCredentialRequestOptions.

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

publicKeyCredentialRequestOptions doit contenir toutes les informations nécessaires à l'authentification par clé d'accès. Transmettez ces informations à la fonction de votre bibliothèque FIDO côté serveur chargée de créer l'objet publicKeyCredentialRequestOptions.

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

  • rpId: ID de tiers assujetti à des restrictions auquel l'identifiant doit être associé (par exemple, example.com). L'authentification ne réussira que si l'ID de RP que vous fournissez ici correspond à l'ID de RP associé au justificatif. Pour renseigner l'ID de RP, utilisez la même valeur que l'ID de RP que vous avez défini dans publicKeyCredentialCreationOptions lors de l'enregistrement des identifiants.
  • challenge: donnée que le fournisseur de clé d'accès signe pour prouver que l'utilisateur détient la clé d'accès au moment de la requête d'authentification. Consultez les détails dans Créer le défi.
  • allowCredentials: tableau des identifiants acceptés pour cette authentification. Transmettez un tableau vide pour permettre à l'utilisateur de sélectionner une clé d'accès disponible dans une liste affichée par le navigateur. Pour en savoir plus, consultez Récupérer une question d'authentification sur le serveur de tiers assujettis à des restrictions et Informations détaillées sur les identifiants détectables.
  • userVerification: indique si la validation de l'utilisateur à l'aide du verrouillage de l'écran de l'appareil est "obligatoire" ou "recommandée" ou "déconseillé". Consultez Récupérer une question d'authentification sur le serveur du tiers assujetti à des restrictions.
  • timeout: délai (en millisecondes) requis par l'utilisateur pour procéder à l'authentification. Il doit être raisonnablement généreux et plus court que la durée de vie de l'challenge. La valeur par défaut recommandée est 5 minutes, mais vous pouvez l'augmenter. Jusqu'à 10 minutes, ce qui reste dans la plage recommandée. Des délais avant expiration longs sont utiles si vous prévoyez que les utilisateurs utilisent le workflow hybride, qui prend généralement un peu plus de temps. Si l'opération expire, une erreur NotAllowedError est générée.

Une fois que vous avez créé publicKeyCredentialRequestOptions, envoyez-le au client.

<ph type="x-smartling-placeholder">
</ph> publicKeyCredentialCreationOptions envoyé par le serveur
Options envoyées par le serveur. Le décodage challenge s'effectue côté client.

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

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

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

router.post('/signinRequest', csrfCheck, async (req, res) => {

  // Ensure you nest 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 {
    // Use the generateAuthenticationOptions function from SimpleWebAuthn
    const options = await generateAuthenticationOptions({
      rpID: process.env.HOSTNAME,
      allowCredentials: [],
    });
    // Save the challenge in the user session
    req.session.challenge = options.challenge;

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

Valider et connecter l'utilisateur

Lorsque navigator.credentials.get est résolu correctement sur le client, il renvoie un objet PublicKeyCredential.

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

Le response est un AuthenticatorAssertionResponse. Il représente la réponse du fournisseur de clé d'accès à l'instruction du client pour créer les éléments nécessaires pour essayer de s'authentifier avec une clé d'accès sur la RP. Il comprend :

  • response.authenticatorDataet response.clientDataJSON, comme lors de l'enregistrement d'une clé d'accès.
  • response.signature qui contient une signature sur ces valeurs.

Envoyez l'objet PublicKeyCredential au serveur.

Sur le serveur, procédez comme suit:

<ph type="x-smartling-placeholder">
</ph> Schéma de base de données
Schéma de base de données suggéré. Pour en savoir plus sur cette conception, consultez Enregistrement d'une clé d'accès côté serveur.
  • Réunissez les informations dont vous aurez besoin pour vérifier l'assertion et authentifier l'utilisateur: <ph type="x-smartling-placeholder">
      </ph>
    • Récupérez la question d'authentification attendue stockée dans la session lorsque vous avez généré les options d'authentification.
    • Obtenez l'origine et l'ID de RP attendus.
    • Identifiez l'utilisateur dans votre base de données. Dans le cas des identifiants détectables, vous ne savez pas qui est l'utilisateur à l'origine de la demande d'authentification. Pour le savoir, deux possibilités s'offrent à vous: <ph type="x-smartling-placeholder">
        </ph>
      • Option 1: Utilisez response.userHandle dans l'objet PublicKeyCredential. Dans la table Utilisateurs, recherchez le passkey_user_id qui correspond à userHandle.
      • Option 2: Utilisez l'identifiant id présent dans l'objet PublicKeyCredential. Dans le tableau Identifiants de clé publique, recherchez l'identifiant id qui correspond à l'identifiant id présent dans l'objet PublicKeyCredential. Recherchez ensuite l'utilisateur correspondant à l'aide de la clé étrangère passkey_user_id dans votre table Users.
    • Recherchez dans votre base de données les informations d'identification de clé publique correspondant à l'assertion d'authentification que vous avez reçue. Pour ce faire, dans le tableau Identifiants de clé publique, recherchez l'identifiant id qui correspond à l'identifiant id présent dans l'objet PublicKeyCredential.
  • Vérifiez l'assertion d'authentification. Transférez cette étape de validation à votre bibliothèque FIDO côté serveur, qui propose généralement une fonction utilitaire permettant d'atteindre cet objectif. SimpleWebAuthn propose, par exemple, verifyAuthenticationResponse. Pour en savoir plus, consultez l'annexe: Vérification de la réponse d'authentification.

  • Supprimez la question d'authentification, qu'elle aboutisse ou non, pour éviter les attaques par rejeu.

  • Connectez l'utilisateur. Si la validation a réussi, mettez à jour les informations de session pour indiquer que l'utilisateur est connecté. Vous pouvez également renvoyer un objet user au client, afin que le frontend puisse utiliser les informations associées à l'utilisateur nouvellement connecté.

Exemple de code: valider l'utilisateur et se connecter

Nous utilisons la bibliothèque SimpleWebAuthn dans nos exemples. Ici, nous transférons la vérification de la réponse d'authentification à sa fonction verifyAuthenticationResponse.

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

router.post('/signinResponse', csrfCheck, async (req, res) => {
  const response = req.body;
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;

  // 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 {
    // Find the credential stored to the database by the credential ID
    const cred = Credentials.findById(response.id);
    if (!cred) {
      throw new Error('Credential not found.');
    }
    // Find the user - Here alternatively we could look up the user directly
    // in the Users table via userHandle
    const user = Users.findByPasskeyUserId(cred.passkey_user_id);
    if (!user) {
      throw new Error('User not found.');
    }
    // Base64URL decode some values
    const authenticator = {
      credentialPublicKey: isoBase64URL.toBuffer(cred.publicKey),
      credentialID: isoBase64URL.toBuffer(cred.id),
      transports: cred.transports,
    };

    // Verify the credential
    const { verified, authenticationInfo } = await verifyAuthenticationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      authenticator,
      requireUserVerification: false,
    });

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

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

    req.session.username = user.username;
    req.session['signed-in'] = 'yes';

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

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

Annexe: Vérification de la réponse d'authentification

La vérification de la réponse d'authentification comprend les vérifications suivantes:

Pour en savoir plus sur ces étapes, consultez le code source pour verifyAuthenticationResponse de SimpleWebAuthn ou consultez la liste complète des vérifications dans les spécifications.