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

Présentation

Voici une présentation générale des principales étapes 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). Celui-ci contient une assertion d'authentification.
  • Vérifiez l'assertion d'authentification.
  • Si l'assertion d'authentification est valide, authentifiez l'utilisateur.

Les sections suivantes décrivent les spécificités de chaque étape.

Créer le défi

En pratique, une question d'authentification est un tableau d'octets aléatoires représenté par un objet ArrayBuffer.

// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8

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

  1. Veillez à ce que la même question d'authentification ne soit jamais utilisée plusieurs fois. Générez une nouvelle question d'authentification à chaque tentative de connexion. Supprimer la question d'authentification après chaque tentative de connexion, qu'elle ait réussi ou échoué. Vous pouvez également ignorer le défi après un certain temps. N'acceptez jamais plusieurs fois la même question d'authentification dans une réponse.
  2. Assurez-vous que la question est sécurisée sur le plan cryptographique. Il est en principe impossible de deviner un défi. Pour créer une question d'authentification cryptographique côté serveur, il est préférable de s'appuyer sur une bibliothèque côté serveur FIDO de confiance. Si vous créez vos propres défis, utilisez la fonctionnalité cryptographique intégrée disponible dans votre pile technologique ou recherchez des bibliothèques conçues pour des cas d'utilisation cryptographiques. 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 défi, enregistrez-le dans la session de l'utilisateur pour le vérifier ultérieurement.

Créer des options de demande d'identifiants

Créez des options de requête d'identifiants en tant qu'objet publicKeyCredentialRequestOptions.

Pour ce faire, utilisez votre bibliothèque côté serveur FIDO. Elle offre généralement une fonction utilitaire qui crée 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 côté serveur FIDO qui est chargée de créer l'objet publicKeyCredentialRequestOptions.

Certains champs de publicKeyCredentialRequestOptions peuvent être des constantes. D'autres doivent être définies dynamiquement sur le serveur:

  • rpId: ID de tiers assujetti à des restrictions auquel vous souhaitez que l'identifiant soit associé (par exemple, example.com). L'authentification n'aboutit que si l'ID de RP que vous indiquez ici correspond à l'ID de RP associé au justificatif. Pour renseigner l'ID de RP, utilisez la même valeur que celle que vous avez définie dans publicKeyCredentialCreationOptions lors de l'enregistrement des identifiants.
  • challenge: élément de données 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 demande d'authentification. Pour en savoir plus, consultez Créer le défi.
  • allowCredentials: tableau d'identifiants acceptables 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 les articles Récupérer un test sur le serveur RP et Présentation détaillée des identifiants visibles.
  • userVerification: indique si la validation de l'utilisateur à l'aide du verrouillage de l'écran de l'appareil est "obligatoire", "préférée" ou "déconseillée". Consultez la section Récupérer une question d'authentification à partir du serveur de la RP.
  • timeout: temps (en millisecondes) nécessaire à l'utilisateur pour s'authentifier. Il doit être raisonnablement généreux et être plus court que la durée de vie de challenge. La valeur par défaut recommandée est de 5 minutes, mais vous pouvez l'augmenter jusqu'à 10 minutes, ce qui reste dans la plage recommandée. Les longs délais avant expiration sont logiques si vous vous attendez à ce que les utilisateurs suivent 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 le fichier publicKeyCredentialRequestOptions créé, envoyez-le au client.

publicKeyCredentialCreationOptions envoyés par le serveur
Options envoyées par le serveur. Le décodage de challenge s'effectue côté client.

Exemple de code: créer des options de requête d'identifiants

Nous utilisons la bibliothèque SimpleWebAuthn dans nos exemples. Ici, nous transférons la création des 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, un objet PublicKeyCredential est renvoyé.

Objet PublicKeyCredential envoyé par le serveur
navigator.credentials.get renvoie un PublicKeyCredential.

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

Envoyez l'objet PublicKeyCredential au serveur.

Sur le serveur, procédez comme suit:

Schéma de la base de données
Schéma de base de données suggéré Pour en savoir plus sur cette conception, consultez Enregistrement de clés d'accès côté serveur.
  • Rassemblez les informations dont vous aurez besoin pour vérifier l'assertion et authentifier l'utilisateur:
    • Récupérez la question d'authentification attendue que vous avez 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 qui demande l'authentification. Pour le savoir, deux options s'offrent à vous :
      • Option 1: Utilisez response.userHandle dans l'objet PublicKeyCredential. Dans le tableau Utilisateurs, recherchez l'élément 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 identifiants 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 côté serveur FIDO, qui fournit généralement une fonction utilitaire à cet effet. SimpleWebAuthn propose, par exemple, verifyAuthenticationResponse. Découvrez ce qui se passe en arrière-plan dans l'annexe: vérification de la réponse d'authentification.

  • Supprimez la question d'authentification, que la validation soit réussie ou non, pour éviter les attaques par rejeu.

  • Connectez l'utilisateur. Si la validation a abouti, mettez à jour les informations sur la session pour marquer l'utilisateur comme connecté. Vous pouvez également renvoyer un objet user au client afin que l'interface puisse utiliser les informations associées à l'utilisateur nouvellement connecté.

Exemple de code: valider et connecter l'utilisateur

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 validation de la réponse d'authentification comprend les vérifications suivantes:

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