TP MFA avec TOTP đâïž
đŻ Objectifs pĂ©dagogiques
- Comprendre le fonctionnement dâun second facteur TOTP (Timeâbased OneâTime Password).
- ImplĂ©menter un parcours dâauthentification Web : login/mot de passe â saisie du code TOTP.
- Générer un secret TOTP, un QR Code et vérifier les codes cÎté serveur.
- Appliquer des bonnes pratiques (hash de mot de passe, sessions, contrĂŽle dâaccĂšs).
1. Travail GuidĂ©âïž
1.1 PrĂ©paration du projetâïž
- Créez un dossier
mfa-php/
avec la structure :
mfa-php/
ââ config.php
ââ schema.sql
ââ register.php
ââ login.php
ââ mfa_setup.php
ââ mfa_verify.php
ââ dashboard.php
ââ logout.php
ââ helpers/
ââ totp.php
đ Placez vous dans mfa-php/
et exécutez la commande suivante :
composer require spomky-labs/otphp
- Dans MySQL, créez une base
demo
et créer la table ci-dessous.
1.2 ModĂšle de donnĂ©esâïž
Créez la table users
:
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
mfa_secret VARCHAR(64) DEFAULT NULL,
mfa_enabled TINYINT(1) NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
1.3 SĂ©curitĂ© minimale attendueâïž
- Hash des mots de passe avec
password_hash()
+ vérif viapassword_verify()
. - Sessions sécurisées (
session_start()
en dĂ©but de chaque page, vĂ©rifs dâaccĂšs). - Ne jamais stocker le mot de passe en clair, ni afficher le secret TOTP Ă lâĂ©cran (sauf pour debug en local si besoin).
1.4 Parcours utilisateur Ă implĂ©menterâïž
1.Inscription (register.php
) : email + mot de passe â crĂ©e lâutilisateur.
2.Connexion (étape 1) (login.php
) : vérifie email + mot de passe.
- Si mfa_enabled = 0 â redirection vers mfa_setup.php.
- Sinon â redirection vers mfa_verify.php.
3.Activation MFA (mfa_setup.php
) :
- GénÚre un secret TOTP.
- Affiche un QR Code Ă scanner dans lâapplication mobile (URI `otpauth://`).
- VĂ©rifie un premier code entrĂ© par lâutilisateur.
- Si valide â `mfa_enabled=1`.
4.Vérification MFA (mfa_verify.php
) : demande le code TOTP Ă chaque connexion.
5.Une fois validé, accÚs à dashboard.php
(zone protégée). logout.php
termine la session.
1.5 Liste des fichiersâïž
- register.php â formulaire email + mot de passe (POST) â redirection login.
- login.php : formulaire email + mot de passe (POST). Si OK et
mfa_enabled=1
â redirectionmfa_verify.php
, sinonmfa_setup.php
. - mfa_setup.php : affiche QR + champ "Code TOTP" â si valide, enregistre
mfa_enabled=1
et redirigedashboard.php
. - mfa_verify.php : champ "Code TOTP" â si valide, session authentifiĂ©e complĂšte â
dashboard.php
. - dashboard.php â page protĂ©gĂ©e avec âBienvenue
â.
â
Fonctionnel : inscription, login, setup MFA, vérification MFA, déconnexion.
â
Code : sĂ©paration logique, vĂ©rifs dâerreurs, messages clairs.
2. Tests & Validationâïž
- Créer un compte, puis se connecter.
- Ă la premiĂšre connexion, la page MFA Setup affiche le QR Code : scannezâle avec votre appli.
- Saisissez un code TOTP : lâactivation doit rĂ©ussir et vous envoyer au dashboard.
- DĂ©connectezâvous, reconnectezâvous : aprĂšs le mot de passe, la page MFA Verify est demandĂ©e ; entrez le code.
- VĂ©rifiez que lâaccĂšs direct Ă
dashboard.php
sans TOTP nâest pas possible (contrĂŽles de session).
Bonnes pratiques & Extensions
-
Sécurité :
- Utiliser
password_hash()
/password_verify()
(déjà fait). - Protéger les pages par des contrÎles de session stricts.
- Limiter les tentatives de connexion (ex. compteur + délai).
- Masquer le secret cÎté interface (affiché ici pour pédagogie seulement).
- Utiliser
-
Codes de secours (bonus) : générer 5à codes aléatoires hashés en BDD pour récupération.
- Souvenir dâappareil (bonus) : cookie signĂ© (HMAC) valide X jours, pour Ă©viter le TOTP Ă chaque login.
- DĂ©calage dâhorloge : afficher lâheure serveur et recommander la synchronisation de lâhorloge client.
đ§Ș DĂ©pannage (FAQ rapide)
- Le QR ne sâaffiche pas : vĂ©rifiez lâURL (encodage de lâURI
otpauth://
). - Redirections bizarres : sessions non démarrées ?
session_start()
doit ĂȘtre tout en haut de chaque script (viaconfig.php
).
3. Correctionâïž
Corrigé & Code de référence
<?php
// ===== config.php =====
ini_set('display_errors', 1);
error_reporting(E_ALL);
session_start();
$DB_HOST = '127.0.0.1';
$DB_NAME = 'mfa_demo';
$DB_USER = 'root';
$DB_PASS = '';
try {
$pdo = new PDO("mysql:host=$DB_HOST;dbname=$DB_NAME;charset=utf8mb4", $DB_USER, $DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
} catch (PDOException $e) {
die("Erreur de connexion DB : " . $e->getMessage());
}
function require_login() {
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}
}
function is_fully_authenticated() {
return isset($_SESSION['user_id']) && (!($_SESSION['mfa_required'] ?? false));
}
// ===== mfa_setup.php =====
require __DIR__ . '/config.php';
require __DIR__ . '/vendor/autoload.php';
use OTPHP\TOTP;
require_login();
$stmt = $pdo->prepare("SELECT * FROM users WHERE id=?");
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
if (!$user) { header("Location: login.php"); exit; }
if ($user['mfa_enabled']) {
header("Location: dashboard.php");
exit;
}
if (empty($user['mfa_secret'])) {
$totp = TOTP::create();
$secret = $totp->getSecret();
$pdo->prepare("UPDATE users SET mfa_secret=? WHERE id=?")->execute([$secret, $user['id']]);
$user['mfa_secret'] = $secret;
}
$totp = TOTP::create($user['mfa_secret']);
$totp->setLabel($user['email']);
$totp->setIssuer("DemoMFA");
$uri = $totp->getProvisioningUri();
$qr = "https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=".urlencode($uri);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$code = trim($_POST['code'] ?? '');
if ($totp->verify($code)) {
$pdo->prepare("UPDATE users SET mfa_enabled=1 WHERE id=?")->execute([$user['id']]);
$_SESSION['mfa_required'] = false;
header("Location: dashboard.php");
exit;
} else {
$error = "Code invalide.";
}
}
?>
<!DOCTYPE html>
<html lang="fr">
<head><meta charset="UTF-8"><title>Setup MFA</title></head>
<body>
<h1>Activer MFA</h1>
<p>Scannez ce QR Code avec Google Authenticator :</p>
<img src="<?= $qr ?>" alt="QR Code"><br>
<form method="post">
<label>Entrez le code: <input type="text" name="code" required></label>
<button type="submit">Activer</button>
</form>
<?php if (!empty($error)) echo "<p style='color:red'>$error</p>"; ?>
</body>
</html>
// ===== mfa_verify.php =====
require __DIR__ . '/config.php';
require __DIR__ . '/vendor/autoload.php';
use OTPHP\TOTP;
require_login();
if (!($_SESSION['mfa_required'] ?? false)) {
header("Location: dashboard.php");
exit;
}
$stmt = $pdo->prepare("SELECT * FROM users WHERE id=?");
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
if (!$user || !$user['mfa_enabled']) {
header("Location: login.php");
exit;
}
$totp = TOTP::create($user['mfa_secret']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$code = trim($_POST['code'] ?? '');
if ($totp->verify($code)) {
$_SESSION['mfa_required'] = false;
header("Location: dashboard.php");
exit;
} else {
$error = "Code invalide.";
}
}
?>
<!DOCTYPE html>
<html lang="fr">
<head><meta charset="UTF-8"><title>Vérification MFA</title></head>
<body>
<h1>Vérification du code MFA</h1>
<form method="post">
<input type="text" name="code" required>
<button type="submit">Vérifier</button>
</form>
<?php if (!empty($error)) echo "<p style='color:red'>$error</p>"; ?>
</body>
</html>
// ===== dashboard.php =====
require __DIR__ . '/config.php';
require_login();
if (!is_fully_authenticated()) {
header('Location: mfa_verify.php');
exit;
}
?>
<!DOCTYPE html>
<html lang="fr">
<head><meta charset="UTF-8"><title>Dashboard</title></head>
<body>
<h1>Bienvenue, <?= htmlspecialchars($_SESSION['user_email'] ?? '') ?></h1>
<p>Votre compte est protĂ©gĂ© par la MFA TOTP â
</p>
<p><a href="logout.php">Se déconnecter</a></p>
</body>
</html>