Tutoriel Mettre en place le 2FA sur votre espace utilisateur en PHP

Paradise GTP

Premium
Inscription
30 Juin 2013
Messages
4 265
Réactions
4 409
Points
21 005
RGCoins
0
You must be registered for see images attach

Dans ce tutoriel, je vais vous expliquer comment mettre en place l'authentification à deux facteurs (2FA) sur votre site afin d'offrir une sécurité supplémentaire à vos utilisateurs.

You must be registered for see images attach

Tout d'abord, avant de commencer, même si je pense qu'une grande partie d'entre vous sait ce qu'est cette mesure de sécurité, voici la définition :

L’authentification à 2 facteurs (2FA) est une méthode de sécurité basée sur la gestion des identités et accès qui impose deux formes d’identification pour accéder aux ressources et données. Elle permet aux entreprises de surveiller et protéger leurs informations et réseaux les plus vulnérables.​
Source :

Dans notre cas, nous utiliserons une application d'authentification comme 2FAS Auth que j'utilise personnellement, mais il en existe des dizaines et leur fonctionnement est très souvent similaire.

Nous utiliserons la bibliothèque nommée TwoFactorAuth de RobThree, que vous pouvez trouver sur .
Voici la commande Composer à utiliser pour le téléchargement :
Code:
composer require robthree/twofactorauth

Pour la réalisation de ce tutoriel, je vais créer un espace utilisateur simple sans trop de sécurité. Il vous suffira d'adapter le code à votre propre environnement.

You must be registered for see images attach

Donc, dans ma base de données nommée "2fa" et ma table "users", j'ai ajouté un champ "secret". Pour le reste, c'est du basique. Bien sûr, cela peut ne pas correspondre exactement à ce que vous avez dans vos projets.
You must be registered for see images attach


Nous allons commencer par créer un fichier nommé config.php qui servira à la connexion à la base de données.
PHP:
<?php
session_start();
require_once('./vendor/autoload.php');
 
try {
  $db = new PDO("mysql:host=localhost;dbname=2fa", 'root', '');
  $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $e) {
  echo "Connection failed: " . $e->getMessage();
}

Comme vous pouvez le voir, nous incluons également le fichier d'autoload de la bibliothèque 2FA. Ensuite, dans notre fichier contenant notre page de connexion (dans mon cas, c'est le fichier index.php), en plus des champs "utilisateur" et "mot de passe", nous ajoutons un troisième champ pour le code 2FA.
PHP:
            <form action="login.php" method="POST">
                <h1>Connexion</h1>
                <input name="email" type="email" placeholder="Adresse Mail" required />
                <input name="password" type="password" placeholder="Mot de passe" required />
                <input name="tfa_code" type="text" placeholder="Code 2FA" />
                <button>Connexion</button>
            </form>

Donc, lorsqu'on se connecte, on est redirigé vers la page login.php. Il faut donc la créer.

Dans notre fichier, nous allons commencer par ajouter notre fichier config.php et la bibliothèque.
PHP:
use RobThree\Auth\TwoFactorAuth;
require('config.php');

Ensuite, nous vérifions que des valeurs ont été transmises dans notre formulaire. Normalement, cela devrait être le cas, car les champs email et mot de passe sont requis. Sinon, nous redirigeons l'utilisateur vers la page d'accueil.
PHP:
if (!empty($_POST['email']) && !empty($_POST['password'])) {

} else {
    header('Location: index.php');
    exit();
}

Si tout se passe bien, pour simplifier, nous allons stocker nos valeurs dans des variables.
PHP:
    $email = $_POST['email'];
    $password = $_POST['password'];
    $tfaCode = $_POST['tfa_code'] ?? null;

Ensuite, nous récupérons les informations de l'utilisateur en exécutant une requête SQL qui recherche l'utilisateur par son adresse e-mail :
PHP:
    $q = $db->prepare('SELECT * FROM users WHERE email = :email');
    $q->bindValue('email', $email);
    $q->execute();
    $user = $q->fetch(PDO::FETCH_ASSOC);

Pour finir, nous vérifions que l'utilisateur a saisi le bon mot de passe, ce qui devrait déjà être fait dans votre formulaire de connexion, ainsi que si le 2FA est activé sur le compte utilisateur. Nous vérifions également si le code saisi dans le formulaire est correct en utilisant une instance de la bibliothèque TwoFactorAuth.
PHP:
if ($user && password_verify($password, $user['password'])) {
        $tfa = new TwoFactorAuth();
        $is2faValid = !$user['secret'] || ($tfaCode && $tfa->verifyCode($user['secret'], $tfaCode));
  
        if ($is2faValid) {
            $_SESSION['user_id'] = $user['user_id'];
            header('Location: profil.php');
            exit();
        } else {
            echo "Code 2FA invalide";
        }
    } else {
        echo "Identifiants invalides";
    }

PHP:
<?php
use RobThree\Auth\TwoFactorAuth;
require('config.php');
if (!empty($_POST['email']) && !empty($_POST['password'])) {
    $email = $_POST['email'];
    $password = $_POST['password'];
    $tfaCode = $_POST['tfa_code'] ?? null;

    $q = $db->prepare('SELECT * FROM users WHERE email = :email');
    $q->bindValue('email', $email);
    $q->execute();
    $user = $q->fetch(PDO::FETCH_ASSOC);

    if ($user && password_verify($password, $user['password'])) {
        $tfa = new TwoFactorAuth();
        $is2faValid = !$user['secret'] || ($tfaCode && $tfa->verifyCode($user['secret'], $tfaCode));
  
        if ($is2faValid) {
            $_SESSION['user_id'] = $user['user_id'];
            header('Location: profil.php');
            exit();
        } else {
            echo "Code 2FA invalide";
        }
    } else {
        echo "Identifiants invalides";
    }
} else {
    header('Location: index.php');
    exit();
}

Maintenant que cela est fait, lorsque l'utilisateur se connecte, s'il n'a pas activé le 2FA et que ses informations sont correctes, ou s'il a activé le 2FA et que tout est correct, il est redirigé vers la page profil.php. Sinon, une erreur est affichée

Ensuite, nous allons voir comment activer le 2FA sur le compte de l'utilisateur sur notre page profil.php.

Comme précédemment, nous commençons par ajouter notre script config.php et la bibliothèque.
PHP:
use RobThree\Auth\TwoFactorAuth;
require('config.php');

Ensuite, nous créons notre instance.
PHP:
$tfa = new TwoFactorAuth();

Si l'utilisateur n'a pas activé le 2FA sur son compte, nous allons générer le code secret et le stocker dans une variable.
PHP:
if (empty($_SESSION['tfa_secret'])) {
    $_SESSION['tfa_secret'] = $tfa->createSecret();
    $secret = $_SESSION['tfa_secret'];
}

Comme précédemment, nous récupérons les informations de notre utilisateur. Bien sûr, pour simplifier, vous pouvez créer une fonction dans un script PHP afin de ne pas avoir à répéter le code sur chaque page. Cependant, ce n'est pas l'objectif de ce tutoriel. Nous optons pour la simplicité (enfin, pas vraiment :blush:)

PHP:
$userReq = $db->prepare('SELECT * FROM users WHERE user_id = :id');
$userReq->bindValue('id', $_SESSION['user_id']);
$userReq->execute();
$user = $userReq->fetch(PDO::FETCH_ASSOC);
$email = $user['email'];

Je mets la valeur de l'adresse e-mail pour l'afficher dans mon formulaire, mais ce n'est pas obligatoire.

Maintenant, pour l'activation du 2FA, nous nous rendons sur le formulaire que vous avez prévu à cet effet, que l'utilisateur ait déjà activé le 2FA ou non.
S'il est activé, nous affichons simplement que c'est déjà activé.
PHP:
        <?php if ($user['secret']): ?>
            <p>2FA activée</p>

Si cependant, il n'est pas activé, nous affichons le code à renseigner dans l'application d'authentification à double facteur (que nous avons généré juste avant), ainsi que le code QR pour simplifier le processus.
Vous pouvez également donner le nom de l'application en remplaçant " RG TUTO "
PHP:
        <?php else: ?>
            <p>Code secret : <?= $secret ?></p>
            <p>QR Code :</p>
            <img src="<?= $tfa->getQRCodeImageAsDataUri('RG TUTO', $secret) ?>">
            <form method="POST">
                <input type="text" placeholder="Vérification Code" name="tfa_code">
                <button type="submit">Valider</button>
            </form>
        <?php endif; ?>

Comme vous pouvez le voir, nous avons un formulaire qui s'exécute sur cette page. Nous allons donc rajouter le code pour vérifier si le code saisi pour activer le 2FA est correct, et stocker le code secret dans la base de données pour ainsi activer le 2FA sur le compte utilisateur.
PHP:
if (!empty($_POST['tfa_code'])) {
    if ($tfa->verifyCode($secret, $_POST['tfa_code'])) {
        $q = $db->prepare('UPDATE users SET secret = :secret WHERE user_id = :id');
        $q->bindValue('secret', $secret);
        $q->bindValue('id', $_SESSION['user_id']);
        $q->execute();
    } else {
        echo "Code invalide";
    }
}

Voilà, normalement le 2FA est possible sur votre site. Nous allons procéder à un test : je vais créer un compte sur mon environnement de test. Par défaut, le 2FA n'est donc pas activé (le champ "secret" de ma base de données est vide).
You must be registered for see images attach


Je peux donc me connecter sans code secret, et je suis redirigé(e) vers ma page utilisateur qui me propose d'activer le 2FA.
You must be registered for see images attach


Maintenant, rendez-vous sur mon téléphone dans mon application d'authentification, où je vais scanner mon code QR (je pourrais aussi saisir le code secret)
You must be registered for see images attach


L'application me donne donc mon code à saisir dans le formulaire. Une fois saisi, mon profil indique que le 2FA est activé, et dans ma base de données, je retrouve donc mon code secret.
You must be registered for see images attach

You must be registered for see images attach


Si j'essaie de me connecter sans saisir le code 2FA ou en saisissant un faux code, j'obtiens donc l'erreur : "Code 2FA invalide". Mais si je saisis le bon code, je me retrouve bien sûr sur mon profil utilisateur

You must be registered for see images attach
You must be registered for see images attach
You must be registered for see images attach



Utilisation de pour reformuler les phrases et corriger les fautes.
You must be registered for see images attach

Comme ce n'est pas forcément très clair car cela doit être adapté à votre environnement et que je n'ai pas fourni le code de toutes les pages (inscription, etc.), voici le code source de l'intégralité des pages de mon projet tutoriel.
PHP:
<?php
session_start();
require_once('./vendor/autoload.php');
 
try {
  $db = new PDO("mysql:host=localhost;dbname=2fa", 'root', '');
  $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $e) {
  echo "Connection failed: " . $e->getMessage();
}

PHP:
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Page de Connexion / Inscription</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="container">
        <div class="form-container sign-in-container">
            <form action="login.php" method="POST">
                <h1>Connexion</h1>
                <input name="email" type="email" placeholder="Adresse Mail" required />
                <input name="password" type="password" placeholder="Mot de passe" required />
                <input name="tfa_code" type="text" placeholder="Code 2FA" />
                <button>Connexion</button>
            </form>
        </div>
        <div class="form-container sign-up-container">
            <form action="register.php" method="POST">
                <h1>Inscription</h1>
                <input name="email" type="email" placeholder="Adresse Mail" required />
                <input name="password" type="password" placeholder="Mot de passe" required />
                <button>Inscription</button>
            </form>
        </div>
        <div class="overlay-container">
            <div class="overlay">
                <div class="overlay-panel overlay-left">
                    <h1>Bienvenue de retour!</h1>
                    <p>Pour rester connecté, veuillez vous connecter avec vos informations personnelles</p>
                    <button class="ghost" id="signIn">Connexion</button>
                </div>
                <div class="overlay-panel overlay-right">
                    <h1>Bonjour, Ami!</h1>
                    <p>Entrez vos informations personnelles pour commencer votre voyage avec nous</p>
                    <button class="ghost" id="signUp">Inscription</button>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

<style>
    * {
    box-sizing: border-box;
}

body {
    font-family: 'Arial', sans-serif;
    background: #f6f5f7;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    margin: 0;
}

h1 {
    font-weight: bold;
    margin: 0;
}

p {
    font-size: 14px;
    font-weight: 100;
    line-height: 20px;
    letter-spacing: 0.5px;
    margin: 20px 0 30px;
}

button {
    border-radius: 20px;
    border: 1px solid #ff4b2b;
    background-color: #ff4b2b;
    color: #ffffff;
    font-size: 12px;
    font-weight: bold;
    padding: 12px 45px;
    letter-spacing: 1px;
    text-transform: uppercase;
    transition: transform 80ms ease-in;
}

button:active {
    transform: scale(0.95);
}

button:focus {
    outline: none;
}

button.ghost {
    background-color: transparent;
    border-color: #ffffff;
}

form {
    background-color: #ffffff;
    display: flex;
    flex-direction: column;
    padding: 0 50px;
    height: 100%;
    justify-content: center;
    align-items: center;
    text-align: center;
}

input {
    background-color: #eee;
    border: none;
    padding: 12px 15px;
    margin: 8px 0;
    width: 100%;
}

.container {
    background-color: #fff;
    border-radius: 10px;
    box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
    position: relative;
    overflow: hidden;
    width: 768px;
    max-width: 100%;
    min-height: 480px;
}

.form-container {
    position: absolute;
    top: 0;
    height: 100%;
    transition: all 0.6s ease-in-out;
}

.sign-in-container {
    left: 0;
    width: 50%;
    z-index: 2;
}

.sign-up-container {
    left: 0;
    width: 50%;
    opacity: 0;
    z-index: 1;
}

.overlay-container {
    position: absolute;
    top: 0;
    left: 50%;
    width: 50%;
    height: 100%;
    overflow: hidden;
    transition: transform 0.6s ease-in-out;
    z-index: 100;
}

.overlay {
    background: #ff416c;
    background: -webkit-linear-gradient(to right, #ff4b2b, #ff416c);
    background: linear-gradient(to right, #ff4b2b, #ff416c);
    background-repeat: no-repeat;
    background-size: cover;
    background-position: 0 0;
    color: #ffffff;
    position: relative;
    left: -100%;
    height: 100%;
    width: 200%;
    transform: translateX(0);
    transition: transform 0.6s ease-in-out;
}

.overlay-panel {
    position: absolute;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    text-align: center;
    top: 0;
    height: 100%;
    width: 50%;
    transform: translateX(0);
    transition: transform 0.6s ease-in-out;
}

.overlay-left {
    transform: translateX(-20%);
}

.overlay-right {
    right: 0;
    transform: translateX(0);
}

.container.right-panel-active .sign-in-container {
    transform: translateX(100%);
}

.container.right-panel-active .sign-up-container {
    transform: translateX(100%);
    opacity: 1;
    z-index: 5;
}

.container.right-panel-active .overlay-container {
    transform: translateX(-100%);
}

.container.right-panel-active .overlay {
    transform: translateX(50%);
}

.container.right-panel-active .overlay-left {
    transform: translateX(0);
}

.container.right-panel-active .overlay-right {
    transform: translateX(20%);
}

</style>

<script>
    const signUpButton = document.getElementById('signUp');
const signInButton = document.getElementById('signIn');
const container = document.querySelector('.container');

signUpButton.addEventListener('click', () => {
    container.classList.add('right-panel-active');
});

signInButton.addEventListener('click', () => {
    container.classList.remove('right-panel-active');
});

</script>

PHP:
<?php
use RobThree\Auth\TwoFactorAuth;
require('config.php');
if (!empty($_POST['email']) && !empty($_POST['password'])) {
    $email = $_POST['email'];
    $password = $_POST['password'];
    $tfaCode = $_POST['tfa_code'] ?? null;

    $q = $db->prepare('SELECT * FROM users WHERE email = :email');
    $q->bindValue('email', $email);
    $q->execute();
    $user = $q->fetch(PDO::FETCH_ASSOC);

    if ($user && password_verify($password, $user['password'])) {
        $tfa = new TwoFactorAuth();
        $is2faValid = !$user['secret'] || ($tfaCode && $tfa->verifyCode($user['secret'], $tfaCode));
      
        if ($is2faValid) {
            $_SESSION['user_id'] = $user['user_id'];
            header('Location: profil.php');
            exit();
        } else {
            echo "Code 2FA invalide";
        }
    } else {
        echo "Identifiants invalides";
    }
} else {
    header('Location: index.php');
    exit();
}

PHP:
<?php
use RobThree\Auth\TwoFactorAuth;
require('config.php');

$tfa = new TwoFactorAuth();
if (empty($_SESSION['tfa_secret'])) {
    $_SESSION['tfa_secret'] = $tfa->createSecret();
}
$secret = $_SESSION['tfa_secret'];
 
if (empty($_SESSION['user_id'])) {
    header('location:index.php');
    exit();
}
 
if (!empty($_POST['tfa_code'])) {
    if ($tfa->verifyCode($secret, $_POST['tfa_code'])) {
        $q = $db->prepare('UPDATE users SET secret = :secret WHERE user_id = :id');
        $q->bindValue('secret', $secret);
        $q->bindValue('id', $_SESSION['user_id']);
        $q->execute();
    } else {
        echo "Code invalide";
    }
}

$userReq = $db->prepare('SELECT * FROM users WHERE user_id = :id');
$userReq->bindValue('id', $_SESSION['user_id']);
$userReq->execute();
$user = $userReq->fetch(PDO::FETCH_ASSOC);

$email = $user['email'];

?>

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Profil Utilisateur</title>
</head>
<body>
    <div class="profile-container">
        <h1>Profil Utilisateur</h1>
        <p><strong>Adresse Email :</strong> <?= htmlspecialchars($email) ?></p>

        <?php if ($user['secret']): ?>
            <p>2FA activée</p>
        <?php else: ?>
            <p>Code secret : <?= $secret ?></p>
            <p>QR Code :</p>
            <img src="<?= $tfa->getQRCodeImageAsDataUri('RG TUTO', $secret) ?>">
            <form method="POST">
                <input type="text" placeholder="Vérification Code" name="tfa_code">
                <button type="submit">Valider</button>
            </form>
        <?php endif; ?>
    </div>
</body>
</html>

<style>
    body {
    font-family: 'Arial', sans-serif;
    background: #f6f5f7;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    margin: 0;
}

.profile-container {
    background-color: #fff;
    border-radius: 10px;
    box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
    padding: 30px;
    text-align: center;
    max-width: 400px;
    width: 100%;
}

h1 {
    font-weight: bold;
    margin-bottom: 20px;
}

p {
    font-size: 16px;
    margin: 10px 0;
}

form {
    display: flex;
    flex-direction: column;
    align-items: center;
}

input[type="text"] {
    background-color: #eee;
    border: none;
    padding: 12px 15px;
    margin: 10px 0;
    width: 100%;
    max-width: 300px;
}

button {
    border-radius: 20px;
    border: 1px solid #ff4b2b;
    background-color: #ff4b2b;
    color: #ffffff;
    font-size: 12px;
    font-weight: bold;
    padding: 12px 45px;
    letter-spacing: 1px;
    text-transform: uppercase;
    transition: transform 80ms ease-in;
    cursor: pointer;
}

button:active {
    transform: scale(0.95);
}

button:focus {
    outline: none;
}
</style>

<script>
    document.addEventListener('DOMContentLoaded', () => {
    const form = document.querySelector('form');
    form.addEventListener('submit', (event) => {
        const tfaCodeInput = form.querySelector('input[name="tfa_code"]');
        if (!tfaCodeInput.value.trim()) {
            event.preventDefault();
            alert('Veuillez entrer le code de vérification.');
        }
    });
});

</script>

PHP:
<?php
require('config.php');
 
if (!empty($_POST['email']) && !empty($_POST['password'])) {
    $email = $_POST['email'];
    $password = password_hash($_POST['password'], PASSWORD_DEFAULT);
 
 
    $q = $db->prepare('INSERT INTO users (email, password) VALUES (:email, :password)');
    $q->bindValue('email', $email);
    $q->bindValue('password', $password);
    $res = $q->execute();
 
    if ($res) {
        header('Location:index.php');
    }
}
 
Haut