Présentation
Voici une présentation générale des principales étapes de l'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:
- 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.
- 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 danspublicKeyCredentialCreationOptions
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 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 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 erreurNotAllowedError
est générée.
Une fois le fichier publicKeyCredentialRequestOptions
créé, envoyez-le au 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é.
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 :
response.authenticatorData
etresponse.clientDataJSON
, comme lors de l'étape d'enregistrement de la 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:
- 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'objetPublicKeyCredential
. Dans le tableau Utilisateurs, recherchez l'élémentpasskey_user_id
qui correspond àuserHandle
. - Option 2: Utilisez l'identifiant
id
présent dans l'objetPublicKeyCredential
. Dans le tableau Identifiants de clé publique, recherchez l'identifiantid
qui correspond à l'identifiantid
présent dans l'objetPublicKeyCredential
. Recherchez ensuite l'utilisateur correspondant à l'aide de la clé étrangèrepasskey_user_id
dans votre table Users.
- Option 1: Utilisez
- 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'identifiantid
présent dans l'objetPublicKeyCredential
.
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:
- Vérifiez que l'ID du tiers assujetti à des restrictions correspond à votre site.
- Assurez-vous que l'origine de la requête correspond à celle de la connexion à votre site. Pour les applications Android, consultez Valider l'origine.
- Vérifiez que l'appareil a répondu au test que vous lui avez proposé.
- Vérifiez que lors de l'authentification, l'utilisateur a bien respecté les exigences que vous exigez en tant que tiers assujetti à des restrictions. Si vous exigez la validation de l'utilisateur, assurez-vous que l'indicateur
uv
(validé par l'utilisateur) dansauthenticatorData
esttrue
. Vérifiez que l'indicateurup
(utilisateur présent) dansauthenticatorData
esttrue
, car la présence de l'utilisateur est toujours obligatoire 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 correspond au défi signé:
response.signature
- La clé publique, avec laquelle valider la signature.
- Données signées d'origine. Il s'agit des données dont la signature doit être vérifiée.
- Algorithme cryptographique utilisé pour créer la signature.
- La signature, qui correspond au défi signé:
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.