Double authentification (2FA) : principe et exemples

Article rédigé par

Marco_Avatar
Marc Schiavone

Web Developer

Principe

En bref, TOTP (Time based One Time Password) est un algorithme qui calcule un mot de passe qui aura une validité pour une durée précise (potentiellement personnalisé).

1 – De quoi a-t-on besoin pour la double authentification (2FA) ?

Nous devons créer un algorithme (ou utiliser une bibliothèque existante) qui, dans un premier temps, va vérifier si le mot de passe généré à un instant précis correspond bien à celui saisi par l’utilisateur, et ensuite communiquer le résultat à l’application cliente.

Pour cela, nous devons d’abord enregistrer cet utilisateur dans une base de données avec un secret personnel qui a été généré. Ce secret va être la clé pour extraire les mots de passe de l’algorithme et les vérifier.

Réalisation d'un POC avec Node.js et React.js

Prérequis :

  • Node.js et React.js
  • NPM or YARN
  • Postgres

Serveur Node

Pour ce POC, j’ai utilisé les librairies suivantes :

  • sequelize comme ORM pour manipuler la base de données (mais on ne se concentrera pas sur cette partie).
  • otplib, une librairie qui va gérer le calcul OTP.
  • qrcode, une autre librairie qui génère des qrcodes utilisables tant pour le serveur que le client. 🔥

Les exemples ci-dessous présentent des routes simples avec très peu de logique.

2 - Exemple n°1 : génération de la clé d'authentification

Après une vérification préalable de User dans DB, cette première route va générer une clé aléatoire authenticationator.generateSecret(); qui va être unique pour l’utilisateur et uniquement connu de la base de données (ce hachage peut être renforcé en le combinant avec d’autres paramètres).

Nous avons maintenant besoin de générer une url OTP de base qui est reconnue par la plupart des générateurs TOTP (comme Authenticator de Google) : const totp_url = `otpauth://totp/${uid}?secret=${secret}`; 

L’ uid ici peut être n’importe quoi et sera l’étiquette/label de votre générateur de mot de passe.

Pour finir, nous transformons cette URL en une URL de données qui sera lue par notre client toDataURL (totp_url) et renvoyée en réponse.

userRouter.post("/register/:neoid", async (req, res) => {
    try {
        const neoid = parseInt(req.params.neoid as string);
        const { uid } = req.body;
        // Create user in the database
        let user = await User.findOne({
            where: {
                neoId: neoid,
            },
        });
        if (user) {
            return res.status(400).json("User already exist");
        }
        // Create temporary secret until it it verified
        const secret = authenticator.generateSecret();
        const totp_url = `otpauth://totp/${uid}?secret=${secret}`;
        const code = await toDataURL(totp_url);
        user = await User.create({ neoId: neoid, directoryUid: uid, secret, totp_url });
        // otpauth://totp/test?secret=secret
        // Send user id and base32 key to user
        res.status(200).json({ user, code });
    } catch (e) {
        console.log(e);
        res.status(500).json({ message: "Nah, won't do it" });
    }
});

Le code du front-end ci-dessus n’est pas vraiment utile car il ne fait que recevoir et afficher la chaîne d’URL de données du back-end. Attention si vous le scannez, vous aurez accès à mon TOTP personnel 😱😱😱. Soyez gentil de ne pas jouer avec mes données personnelles. 😼

3 - Exemple n°2 : vérification des codes correspondants

Cet exemple, assez simple, présente la vérification des codes correspondants.

La requête contient :

  • l’identifiant de l’utilisateur afin de le trouver dans la base de données.
  • le TOTP const { token } = req.body; (correspondant au code à 6 chiffres généré à partir de l’application mobile une fois l’utilisateur enregistré avec le qrcode).

Les données de l’utilisateur sont récupérées afin d’extraire son secret const secret = user.secret; pour le transmettre à l’algorithme OTP const code = authenticationator.generate(secret);.

Enfin, nous comparons ce code généré avec le token pour répondre positivement ou négativement.

userRouter.post("/verify/:neoid", async (req, res) => {
    try {
        const { token } = req.body;
        const { neoid } = req.params;
        // Retrieve user from database
        const userFound = await User.findOne({
            where: {
                neoId: parseInt(neoid),
            },
        });
        if (!userFound) {
            throw Error();
        }
        const secret = userFound.secret;
        const code = authenticator.generate(secret);
        if (code === token) {
            res.status(200).json("verified");
        } else {
            res.status(200).json("Not allowed");
        }
    } catch (error) {
        console.error(error);
        res.status(500).json({ message: error.message });
    }
});

Merci d’avoir lu cet article !

4 - Ressources

Articles qui pourraient vous plaire

Suivez-nous sur les réseaux sociaux

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *