Comment gérer les utilisateurs et la double-authentication comme un pro
Évitez les hackers avec la double-authentification en POO. Une méthode infaillible, expliquée avec humour et des codes QR.
Si vous lisez ce billet, c'est que vous en avez plein le dos des failles de sécurité à la con et que vous voulez protéger votre site contre ces gentils hackers. On va voir ensemble comment le code dans src/Command/User
comment j'ai géré les utilisateurs et leur authentification à deux facteurs.
Si t'as l'attention d'un poisson rouge cocaïnomane, y'a des titres et des sous-titres pour t'y retrouver.
La base indispensable pour pas se faire trouer
Aujourd'hui, protéger un site avec juste un mot de passe, c'est comme rouler sans freins - ça donne une fausse impression de sécurité avant de planter royalement ton site. C'est là qu'intervient l'authentification à deux facteurs (2FA), cette petite merveille qui demande, en plus de ton mot de passe, un code temporaire généré par une app sur ton téléphone.
J'ai mis en place trois classes et deux commandes :
UserCreateCommand
: Crée un utilisateur en l'insérant dans la base de donnéesUser2FaSetupCommand
: Configure la double-authentification pour un utilisateur existantAbstractUserQrCommand
: Une classe abstraite (oui, abstraite, car on fait de la POO comme des grands) qui factorise le code commun
Dépendances externes : le bordel qu'on importe mais qu'on lit jamais
Avant d'entrer dans ces commandes en détail, on va voir les dépendances dont que j'ai utilisées :
OTPHP
OTPHP c'est une librairie qui permet de gérer ces mots de passe à usage unique basés sur le temps (TOTP). C'est le standard pour la 2FA, utilisé par Google Authenticator, Authy, et toutes ces merdouilles.
use OTPHP\TOTP;
$totp = TOTP::create();
$totp->setLabel('utilisateur@merde.com');
$totp->setIssuer('TonSiteDeMerde');
Voilà, en trois lignes de code, t'as créé un secret TOTP. Bravo champion.
Endroid/QrCode
Cette classe génère des QR codes. Parce que demander à un utilisateur de taper un secret à 32 caractères à la main, c'est comme demander à un âne de résoudre une équation différentielle.
$qrCode = new QrCode(
data: $uri,
encoding: new Encoding('UTF-8'),
errorCorrectionLevel: ErrorCorrectionLevel::Medium,
size: 300,
margin: 10,
roundBlockSizeMode: RoundBlockSizeMode::Margin,
foregroundColor: new Color(0, 0, 0),
backgroundColor: new Color(255, 255, 255),
);
Symfony/Cache
Le système de cache de Symfony, utilisé ici pour stocker les QR codes temporaires. Ouais, on les stocke pas en mémoire comme des porcs, on utilise le système de fichiers.
$cache = new FilesystemAdapter(
namespace: 'user_qr_codes',
defaultLifetime: 3600,
directory: $this->cacheDirectory,
);
Pourquoi 3600 secondes (1 heure) ? Parce que si t'arrives pas à scanner un QR code en une heure, t'as probablement des problèmes plus graves que la sécurité informatique.
Scheb/2fa-bundle
Le bundle de double authentification pour Symfony. C'est ce qui permet à notre User
d'implémenter les interfaces TwoFactorInterface
et TotpConfigurationInterface
. Sans lui, on serait bien dans la merde.
Symfony/Console et ses potes
Tous les potes de Symfony pour faire des commandes Console avec des couleurs et des options. C'est grâce à cette bande qu'on peut avoir des inputs interactifs et des outputs qui ressemblent pas à une bouillie de caractères ASCII.
Architecture globale : comment ce bordel tient debout
Voici l'architecture de mon système, expliquée comme si vous aviez 5 ans mais avec le vocabulaire d'un docker sur les quais :
-
AbstractUserQrCommand : La classe de base qui contient tout le bordel pour générer des QR codes, les stocker dans le cache, et configurer le TOTP. Elle s'appuie sur le service dédié
TotpQrCodeService
. -
UserCreateCommand : Hérite de la classe précédente. Elle te demande toutes les infos nécessaires pour créer un utilisateur, vérifie qu'il existe pas déjà, puis te propose de configurer la 2FA directement.
-
User2FaSetupCommand : Aussi héritée de la classe abstraite. Elle cherche un utilisateur par son nom, puis configure ou reconfigure sa 2FA.
-
TotpQrCodeService : Un service dédié qui s'occupe de créer les TOTP et les QR codes. Je l'a mis dans un service pour pas se répéter comme des abrutis.
-
Entité User : Mon modèle d'utilisateur qui implémente toutes les interfaces requises pour la 2FA, avec des propriétés pour stocker le secret TOTP et son état d'activation.
Franchement, c'est bien foutu cette merde (ouais auto promo). Ça démontre une architecture orientée objet propre, avec séparation des responsabilités.
Classe par classe : analyse profonde de ce bordel
AbstractUserQrCommand
Cette classe abstraite factorise tout le code lié à la génération de codes QR pour la 2FA. Elle s'appuie sur plusieurs constantes :
private const string QR_CODE_CACHE_NAMESPACE = 'user_qr_codes';
private const int QR_CODE_CACHE_LIFETIME = 3600;
private const string QR_CODE_CACHE_PREFIX = 'qrcode_';
private const string TOTP_ISSUER = 'Lecodeestdanslepre';
Le constructeur prend trois dépendances :
EntityManagerInterface
: Pour persister les changementsTotpQrCodeService
: Pour générer les TOTP et QR codescacheDirectory
: Injecté via l'attribut #[Autowire], c'est le répertoire où sont stockés les fichiers de cache
La méthode principale generateTotpQrCode()
:
- Configure le TOTP pour l'utilisateur
- Génère les données du QR code
- Sauvegarde ces données dans le cache
- Met à jour l'utilisateur en DB
- Affiche les infos à l'utilisateur
- Retourne le chemin du fichier QR code (je modifierai ça pour créer une route qui affichera le QR Code)
La méthode cleanQrCodes()
nettoie tous les anciens QR codes d'un utilisateur.
TotpQrCodeService
Ce service encapsule la logique de génération des TOTP et QR codes. Il a deux méthodes publiques :
configureTotpForUser()
: Crée un objet TOTP avec l'email de l'utilisateur comme labelgenerateQrCodeData()
: Génère les données binaires du QR code au format PNG
Et une méthode privée createQrCode()
qui configure l'objet QR code avec tous les paramètres nécessaires.
UserCreateCommand
Cette commande permet de créer un utilisateur de A à Z. Elle demande interactivement le nom d'utilisateur, l'email, éventuellement prénom et nom, le mot de passe et le rôle.
$username = $io->ask(
question: 'Please enter the username',
validator: function ($answer) {
if (empty($answer)) {
throw new \RuntimeException('Le nom d\'utilisateur ne peut pas être vide.');
}
return $answer;
},
);
Notez la validation en ligne qui empêche les valeurs vides, façon badass.
La commande vérifie ensuite l'unicité du nom d'utilisateur et de l'email, puis crée l'utilisateur et le stocke en base. Enfin, elle propose de configurer directement la 2FA.
User2FaSetupCommand
Cette commande prend en paramètre un nom d'utilisateur, récupère l'utilisateur correspondant, puis propose :
- De mettre à jour ses infos (prénom, nom)
- De configurer ou reconfigurer sa 2FA
Les méthodes sont les mêmes que pour la commande de création, mais appliquées à un utilisateur existant.
Entity User
Notre entité User est au cœur du système. En plus des champs classiques (id, username, email, etc.), elle possède :
totpSecret
: Le secret TOTP stocké (peut être null)totpEnabled
: Un booléen qui indique si la 2FA est activée
Elle implémente TwoFactorInterface
via plusieurs méthodes :
isTotpAuthenticationEnabled()
: Retourne si la 2FA est activéegetTotpAuthenticationUsername()
: Retourne l'identifiant pour la 2FAgetTotpAuthenticationConfiguration()
: Retourne une classe anonyme qui implémenteTotpConfigurationInterface
Cette dernière méthode est particulièrement intéressante :
public function getTotpAuthenticationConfiguration(): ?TotpConfigurationInterface
{
// Si l'authentification TOTP n'est pas activée ou pas configurée, retourner null
if (!$this->totpEnabled || null === $this->totpSecret) {
return null;
}
// Créer un objet configuration TOTP
return new class($this->totpSecret) implements TotpConfigurationInterface {
public function __construct(private readonly string $secret)
{
}
public function getSecret(): string
{
return $this->secret;
}
public function getAlgorithm(): string
{
return 'sha1'; // Algorithme standard pour TOTP
}
public function getPeriod(): int
{
return 30; // Période standard de 30 secondes
}
public function getDigits(): int
{
return 6; // Nombre standard de chiffres
}
};
}
Elle utilise une classe anonyme (PHP 7+) pour implémenter l'interface requise, tout en encapsulant les détails d'implémentation.
Le mot de la fin
Cette architecture a plusieurs avantages :
- Séparation des responsabilités : Chaque classe a un rôle bien défini
- Réutilisation du code : L'héritage de
AbstractUserQrCommand
évite la duplication - Service dédié :
TotpQrCodeService
encapsule la logique de génération de QR codes - Cache intelligent : Les QR codes sont stockés temporairement avec nettoyage automatique
Bref, protégez-vous, parce que sur internet, si vous êtes pas paranos, c'est que vous êtes pas assez informés.
Liens utiles
- OTPHP sur GitHub
- RFC 6238 pour les specs TOTP
- Symfony Security
- Guide d'implémentation de la 2FA par OWASP