Présentation
Voici une présentation générale des principales étapes de l'authentification par clé d'accès :

- Définissez le test d'authentification et les autres options nécessaires pour s'authentifier avec une clé d'accès. Envoyez-les au client pour pouvoir les transmettre à votre appel d'authentification par clé d'accès (
navigator.credentials.getsur 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 décrivent les spécificités de chaque étape.
Créer le défi
En pratique, un challenge est un tableau d'octets aléatoires, représenté sous la forme d'un objet ArrayBuffer.
// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8
Pour que le challenge remplisse son objectif, vous devez :
- Assurez-vous que la même question d'authentification n'est jamais utilisée plus d'une fois. Générez une nouvelle question d'authentification à chaque tentative de connexion. Supprimez la question d'authentification après chaque tentative de connexion, qu'elle ait réussi ou échoué. Supprimez également le challenge après une certaine durée. N'acceptez jamais plus d'une fois la même question d'authentification dans une réponse.
- Assurez-vous que la question d'authentification est sécurisée de manière cryptographique. Il doit être pratiquement impossible de deviner un challenge. Pour créer un test sécurisé par chiffrement côté serveur, il est préférable de s'appuyer sur une bibliothèque FIDO côté serveur de confiance. Si vous créez vos propres challenges, utilisez la fonctionnalité de chiffrement intégrée disponible dans votre pile technologique ou recherchez des bibliothèques conçues pour les cas d'utilisation de chiffrement. Par exemple, iso-crypto dans Node.js ou secrets dans Python. Conformément à la spécification, le challenge doit comporter au moins 16 octets pour être considéré comme sécurisé.
Une fois que vous avez créé un test, enregistrez-le dans la session de l'utilisateur pour le valider ultérieurement.
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. Elle propose généralement une fonction utilitaire qui peut créer ces options pour vous. 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 chargée de créer l'objet publicKeyCredentialRequestOptions.
Certains champs de publicKeyCredentialRequestOptions peuvent être des constantes. D'autres doivent être définis de manière dynamique sur le serveur :
rpId: ID de la partie de confiance auquel vous souhaitez associer l'identifiant, par exempleexample.com. L'authentification ne réussira que si l'ID de RP que vous fournissez ici correspond à l'ID de RP associé aux identifiants. Pour renseigner l'ID de RP, utilisez la même valeur que celle que vous avez définie danspublicKeyCredentialCreationOptionslors de l'enregistrement des identifiants.challenge: élément de données que le fournisseur de clés d'accès signera pour prouver que l'utilisateur détient la clé d'accès au moment de la demande d'authentification. Consultez les détails dans Créer le challenge.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 Récupérer un challenge depuis 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 "required" (obligatoire), "preferred" (recommandée) ou "discouraged" (déconseillée). Consultez Récupérer un test à partir du serveur RP.timeout: temps (en millisecondes) dont dispose l'utilisateur pour s'authentifier. Elle doit être raisonnablement généreuse et plus courte que la durée de vie dechallenge. 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 délais d'expiration longs sont judicieux si vous prévoyez que les utilisateurs utiliseront le workflow hybride, qui prend généralement un peu plus de temps. Si l'opération expire, une erreurNotAllowedErrorest renvoyée.
Une fois que vous avez créé publicKeyCredentialRequestOptions, envoyez-le au client.
challengeLe décodage s'effectue côté client.Exemple de code : créer des options de demande d'identifiants
Dans nos exemples, nous utilisons la bibliothèque SimpleWebAuthn. Ici, nous confions la création des options de demande d'identifiants à sa fonction generateAuthenticationOptions.
import {
generateAuthenticationOptions,
} 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.
navigator.credentials.get renvoie un PublicKeyCredential.response est un AuthenticatorAssertionResponse. Il représente la réponse du fournisseur de clé d'accès à l'instruction du client de créer ce qui est nécessaire pour essayer de s'authentifier avec une clé d'accès sur la partie de confiance. Il comprend :
response.authenticatorDataetresponse.clientDataJSON, comme à l'étape d'enregistrement de la clé d'accès.response.signature, qui contient une signature de ces valeurs.
Envoyez l'objet PublicKeyCredential au serveur.
Sur le serveur, procédez comme suit :
- Rassemblez les informations dont vous aurez besoin pour vérifier l'assertion et authentifier l'utilisateur :
- Récupérez le challenge attendu que vous avez stocké dans la session lorsque vous avez généré les options d'authentification.
- Obtenez l'origine et l'ID de RP attendus.
- Recherchez l'identité de l'utilisateur dans votre base de données. Dans le cas d'identifiants détectables, vous ne savez pas qui est l'utilisateur qui effectue une demande d'authentification. Pour le savoir, deux options s'offrent à vous :
- Option 1 : Utilisez
response.userHandledans l'objetPublicKeyCredential. Dans la table Users, recherchez l'élémentpasskey_user_idqui correspond àuserHandle. - Option 2 : Utilisez l'identifiant
idprésent dans l'objetPublicKeyCredential. Dans le tableau Identifiants de clé publique, recherchez l'identifiantidqui correspond à l'identifiantidprésent dans l'objetPublicKeyCredential. Recherchez ensuite l'utilisateur correspondant à l'aide de la clé étrangèrepasskey_user_iddans votre table Users.
- Option 1 : Utilisez
- Recherchez dans votre base de données les informations d'identification de clé publique qui correspondent à l'assertion d'authentification que vous avez reçue. Pour ce faire, dans le tableau Identifiants de clé publique, recherchez l'identifiant
idqui correspond à l'identifiantidprésent dans l'objetPublicKeyCredential.
Vérifiez l'assertion d'authentification. Confiez cette étape de validation à votre bibliothèque côté serveur FIDO, qui propose généralement une fonction utilitaire à cet effet. SimpleWebAuthn propose par exemple
verifyAuthenticationResponse. Pour en savoir plus sur ce qui se passe en arrière-plan, consultez l'Annexe : Validation de la réponse d'authentification.Supprimez la question d'authentification, que la validation ait réussi ou non, afin d'éviter les attaques par rejeu.
Connectez l'utilisateur. Si la validation a réussi, mettez à jour les informations de session pour marquer l'utilisateur comme connecté. Vous pouvez également renvoyer un objet
userau client, afin que l'interface puisse utiliser les informations associées à l'utilisateur qui vient de se connecter.
Exemple de code : valider et connecter l'utilisateur
Dans nos exemples, nous utilisons la bibliothèque SimpleWebAuthn. Ici, nous confions la validation de la réponse d'authentification à sa fonction verifyAuthenticationResponse.
import {
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.');
}
// Prepare credential information for verification
const credential = {
id: cred.id,
publicKey: isoBase64URL.toBuffer(cred.publicKey),
transports: cred.transports,
};
// Verify the credential
const { verified, authenticationInfo } = await verifyAuthenticationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
credential,
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 : Validation de la réponse d'authentification
La validation de la réponse d'authentification comprend les vérifications suivantes :
- Assurez-vous que l'ID de la partie de confiance correspond à votre site.
- Assurez-vous que l'origine de la requête correspond à l'origine de connexion de votre site. Pour les applications Android, consultez Vérifier l'origine.
- Vérifiez que l'appareil a pu fournir le test que vous lui avez donné.
- Vérifiez que, lors de l'authentification, l'utilisateur a respecté les exigences que vous imposez en tant que RP. Si vous exigez la validation de l'utilisateur, assurez-vous que l'option
uv(utilisateur validé) dansauthenticatorDataest définie surtrue. Vérifiez que l'indicateurup(utilisateur présent) dansauthenticatorDataest défini surtrue, car la présence de l'utilisateur est toujours requise pour les clés d'accès. - Vérifiez la signature. Pour valider la signature, vous avez besoin des éléments suivants :
- La signature, qui est le défi signé :
response.signature - La clé publique, pour valider la signature.
- Données signées d'origine. Il s'agit des données dont la signature doit être validée.
- Algorithme cryptographique utilisé pour créer la signature.
- La signature, qui est le défi signé :
Pour en savoir plus sur ces étapes, consultez le code source de SimpleWebAuthn pour verifyAuthenticationResponse ou parcourez la liste complète des vérifications dans la spécification.