andydefer / laravel-mfa
Laravel MFA: A flexible, multi-purpose Multi-Factor Authentication management system for Laravel applications with support for OTP (email/sms), TOTP (Google Authenticator), passwordless login, 2FA, and action confirmation.
Requires
- php: >=8.1
- bacon/bacon-qr-code: ^3.1
- laravel/framework: ^12.0
- paragonie/constant_time_encoding: ^3.1
- spomky-labs/otphp: ^11.4
Requires (Dev)
- barryvdh/laravel-ide-helper: ^3.6
- larastan/larastan: ^3.8
- laravel/pint: ^1.26
- orchestra/testbench: ^10.8
- phpunit/phpunit: ^12.5
- rector/rector: *
- symfony/var-dumper: ^7.0
- vimeo/psalm: ^6.14
Suggests
- ext-gd: Alternative to Imagick for QR code generation
- ext-imagick: Required to generate QR codes as images
README
Un package complet de Multi-Factor Authentication pour Laravel 12, supportant OTP (email/SMS) et TOTP (Google Authenticator).
✨ Fonctionnalités
- 🔑 OTP (One-Time Password) - Envoi et vérification de codes à usage unique par email ou SMS
- 🔐 TOTP (Time-based One-Time Password) - Authentification à deux facteurs compatible Google Authenticator
- 📱 QR Codes - Génération automatique de QR codes pour l'application Google Authenticator
- 🔁 Codes de récupération - Génération de codes de secours pour récupérer l'accès
- ⏱️ Rate Limiting - Protection contre les attaques par force brute
- 🌍 Multilingue - Support français et anglais inclus (extensible)
- 🧹 Auto-cleanup - Nettoyage automatique des OTPs expirés et anciennes configurations 2FA
- 🔄 Polymorphique - Supporte n'importe quel modèle Eloquent (User, Admin, etc.)
📋 Prérequis
- PHP 8.1 ou supérieur
- Laravel 12.x
- Composer
🚀 Installation
1. Installer via Composer
composer require andydefer/laravel-mfa
2. Publier les fichiers de configuration et migrations
php artisan mfa:install
Cette commande va :
- Publier le fichier de configuration
config/mfa.php - Publier les migrations OTP et TOTP
- Exécuter les migrations
Options disponibles :
# Forcer l'installation sans confirmation php artisan mfa:install --force # Installer sans exécuter les migrations php artisan mfa:install --no-migrate # Installer uniquement l'OTP (sans le 2FA/TOTP) php artisan mfa:install --without-totp # Installer uniquement le TOTP/2FA (sans l'OTP) php artisan mfa:install --without-otp
3. Migrer la base de données (si non fait automatiquement)
php artisan migrate
⚙️ Configuration
Variables d'environnement
Créez/modifiez les variables suivantes dans votre fichier .env :
# OTP Settings MFA_OTP_DEFAULT_EXPIRY_MINUTES=10 MFA_OTP_DEFAULT_MAX_ATTEMPTS=3 MFA_OTP_LOCALE=en MFA_OTP_RATE_LIMIT_REQUESTS=3 MFA_OTP_RATE_LIMIT_VERIFICATIONS=5 # TOTP Settings MFA_TOTP_PERIOD=30 MFA_TOTP_DIGITS=6 MFA_TOTP_ALGORITHM=sha1 MFA_TOTP_WINDOW=1 MFA_TOTP_ISSUER=MyApplication # Recovery Codes MFA_RECOVERY_CODES_COUNT=8 MFA_RECOVERY_CODE_LENGTH=10 MFA_RECOVERY_CODE_HASH_ALGO=sha256 # Cleanup MFA_CLEANUP_AUTO_CLEANUP=true MFA_CLEANUP_RETENTION_DAYS=30
Fichier de configuration
Après installation, vous pouvez personnaliser config/mfa.php :
return [ 'otp' => [ 'default_expiry_minutes' => env('MFA_OTP_DEFAULT_EXPIRY_MINUTES', 10), 'default_max_attempts' => env('MFA_OTP_DEFAULT_MAX_ATTEMPTS', 3), 'localization' => [ 'locale' => env('MFA_OTP_LOCALE', 'en'), 'supported_locales' => ['fr', 'en'], 'fallback_locale' => env('MFA_OTP_FALLBACK_LOCALE', 'en'), ], // ... autres configurations ], 'totp' => [ 'period' => env('MFA_TOTP_PERIOD', 30), 'digits' => env('MFA_TOTP_DIGITS', 6), 'algorithm' => env('MFA_TOTP_ALGORITHM', 'sha1'), 'issuer' => env('MFA_TOTP_ISSUER', config('app.name')), 'window' => env('MFA_TOTP_WINDOW', 1), ], // ... ];
🔧 Utilisation
1. Préparer votre modèle User
Ajoutez les traits à votre modèle User :
<?php namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use Kani\Mfa\Otp\Traits\HasOneTimePasswords; use Kani\Mfa\Totp\Traits\HasTwoFactorAuthentication; class User extends Authenticatable { use HasOneTimePasswords; use HasTwoFactorAuthentication; // Vos autres configurations... }
2. Interface pour les canaux de livraison (optionnel)
Si vous souhaitez personnaliser les canaux de livraison OTP (email, SMS, WhatsApp) :
<?php namespace App\Models; use Kani\Mfa\Otp\Contracts\MustOtpChannels; class User extends Authenticatable implements MustOtpChannels { use HasOneTimePasswords; use HasTwoFactorAuthentication; /** * Définit les canaux de livraison OTP pour cet utilisateur. * * @return array<int, string> */ public function getOtpChannels(): array { return ['mail', 'sms']; // Email et SMS } }
📱 OTP (One-Time Password)
Envoyer un OTP
// Envoi simple $response = $user->sendOtp( type: 'email_verification', destination: 'user@example.com' ); if ($response->isSuccess()) { // OTP envoyé avec succès $expiresAt = $response->data['expires_at']; $expiresInMinutes = $response->data['expires_in_minutes']; } // Envoi avec options personnalisées $response = $user->sendOtp( type: 'login', destination: '+33612345678', channels: ['sms'], // Canal de livraison meta: ['ip' => request()->ip()], // Métadonnées expiresInMinutes: 5, // Expire dans 5 minutes maxAttempts: 2 // Maximum 2 tentatives );
Renvoyer un OTP
$response = $user->resendOtp( type: 'email_verification', destination: 'user@example.com' ); if ($response->isSuccess()) { // Nouvel OTP envoyé, l'ancien a été annulé }
Vérifier un OTP
$response = $user->verifyOtp( code: $request->code, type: 'email_verification', destination: $request->email, consume: true // Marquer comme utilisé après vérification ); if ($response->isSuccess()) { // OTP valide ✅ } else { // OTP invalide switch ($response->status->value) { case 'invalid_code': // Code incorrect break; case 'expired_code': // Code expiré break; case 'max_attempts_exceeded': // Trop de tentatives break; case 'rate_limited': // Trop de demandes, attendez break; } }
Annuler des OTPs en attente
$response = $user->cancelOtps('email_verification', 'user@example.com'); // Retourne le nombre d'OTPs annulés
Vérifier l'existence d'un OTP valide
if ($user->hasValidOtp('email_verification', 'user@example.com')) { // Un OTP valide existe } $pendingOtp = $user->getPendingOtp('email_verification', 'user@example.com');
🔐 TOTP / 2FA (Google Authenticator)
Initialiser la configuration 2FA
// Obtenir ou créer un secret TOTP $secret = $user->getTwoFactorSecret(); // Générer l'URI pour le QR code $qrCodeUri = $user->getTwoFactorQrCodeUri(); // Générer un QR code (exemple avec Simple QrCode) use SimpleSoftwareIO\QrCode\Facades\QrCode; $qrCode = QrCode::size(200)->generate($qrCodeUri);
Activer la 2FA
// L'utilisateur scanne le QR code et fournit le code à 6 chiffres $code = $request->input('code'); // Code de l'application Authenticator $enabled = $user->enableTwoFactor($code); if ($enabled) { // 2FA activée avec succès ! ✅ // Générer les codes de récupération à montrer à l'utilisateur $recoveryCodes = $user->generateRecoveryCodes(); // Affichez ces codes à l'utilisateur (une seule fois !) foreach ($recoveryCodes as $code) { echo $code . PHP_EOL; } }
Désactiver la 2FA
$disabled = $user->disableTwoFactor(); if ($disabled) { // 2FA désactivée }
Vérifier le code 2FA lors de la connexion
// Lors de la connexion, après vérification du mot de passe $code = $request->input('2fa_code'); // Code à 6 chiffres if ($user->verifyTwoFactorCode($code)) { // Code valide - Connexion autorisée ✅ Auth::login($user); } else { // Code invalide return back()->withErrors(['2fa_code' => 'Code invalide']); }
Codes de récupération
// Générer de nouveaux codes de récupération $recoveryCodes = $user->generateRecoveryCodes(); // Vérifier un code de récupération (pendant la connexion) if ($user->verifyTwoFactorCode($recoveryCodeFromUser)) { // Code de récupération valide - Connexion autorisée // Le code est automatiquement consommé (ne peut plus être réutilisé) } // Obtenir les codes de récupération stockés (hachés) $storedCodes = $user->getRecoveryCodes();
🗄️ Nettoyage automatique
Commande manuelle
# Nettoyer tous les OTPs expirés et vieilles configurations 2FA php artisan mfa:cleanup # Forcer sans confirmation php artisan mfa:cleanup --force # Simuler (dry run) - voir ce qui serait supprimé php artisan mfa:cleanup --dry-run # Garder les OTPs expirés, ne nettoyer que les utilisés/vérifiés php artisan mfa:cleanup --keep-expired # Nettoyer uniquement les OTPs php artisan mfa:cleanup --otp-only # Nettoyer uniquement les configurations 2FA php artisan mfa:cleanup --totp-only # Filtrer par type d'OTP php artisan mfa:cleanup --type=email_verification # Jours de rétention personnalisés php artisan mfa:cleanup --days=15
Configuration automatique
Dans config/mfa.php :
'cleanup' => [ 'auto_cleanup' => env('MFA_CLEANUP_AUTO_CLEANUP', true), // Nettoyage auto 'frequency' => env('MFA_CLEANUP_FREQUENCY', 60), // Toutes les 60 minutes 'retention_days' => env('MFA_CLEANUP_RETENTION_DAYS', 30), // Rétention 30 jours ],
🌍 Traductions
Configurer la langue
Dans .env :
MFA_OTP_LOCALE=fr # Français # ou MFA_OTP_LOCALE=en # Anglais
Utiliser le helper de traduction
use Kani\Mfa\Core\Helpers\TranslationHelper; // Traduire un message $message = TranslationHelper::trans('messages.send_success'); // Retourne: "Verification code sent successfully." ou "Code de vérification envoyé avec succès." // Avec placeholders $message = TranslationHelper::trans('messages.subject', ['app_name' => 'MyApp']); // Retourne: "Your verification code - MyApp"
Publier les fichiers de langue pour personnalisation
php artisan vendor:publish --tag=mfa-translations
Les fichiers seront copiés dans resources/lang/vendor/mfa/.
🎯 Exemples complets
Exemple 1 : Vérification d'email
// UserController.php public function sendVerification(Request $request) { $user = auth()->user(); $response = $user->sendOtp( type: 'email_verification', destination: $user->email ); if ($response->isSuccess()) { return response()->json([ 'message' => 'Code de vérification envoyé', 'expires_in' => $response->data['expires_in_minutes'] ]); } return response()->json(['error' => $response->message], 429); } public function verifyEmail(Request $request) { $user = auth()->user(); $response = $user->verifyOtp( code: $request->code, type: 'email_verification', destination: $user->email ); if ($response->isSuccess()) { $user->email_verified_at = now(); $user->save(); return response()->json(['message' => 'Email vérifié avec succès']); } return response()->json(['error' => $response->message], 422); }
Exemple 2 : Authentification à deux facteurs complète
// TwoFactorController.php class TwoFactorController extends Controller { public function showSetup() { $user = auth()->user(); // Obtenir ou créer le secret $secret = $user->getTwoFactorSecret(); // Générer l'URI du QR code $qrCodeUri = $user->getTwoFactorQrCodeUri(); // Générer un QR code (utilisez votre bibliothèque préférée) $qrCode = QrCode::size(200)->generate($qrCodeUri); return view('2fa.setup', compact('qrCode', 'secret')); } public function enable(Request $request) { $user = auth()->user(); $request->validate([ 'code' => 'required|string|size:6' ]); if ($user->enableTwoFactor($request->code)) { // Générer et montrer les codes de récupération $recoveryCodes = $user->generateRecoveryCodes(); return view('2fa.recovery-codes', compact('recoveryCodes')); } return back()->withErrors(['code' => 'Code invalide']); } public function disable() { $user = auth()->user(); $user->disableTwoFactor(); return redirect()->route('profile')->with('success', '2FA désactivée'); } public function verify(Request $request) { $user = auth()->user(); if ($user->verifyTwoFactorCode($request->code)) { // Stocker en session que le 2FA est vérifié session(['2fa_verified' => true]); return redirect()->intended('/dashboard'); } return back()->withErrors(['code' => 'Code invalide']); } public function showRecovery() { $user = auth()->user(); if (!$user->isTwoFactorEnabled()) { return redirect()->route('login'); } return view('2fa.recovery'); } public function verifyRecovery(Request $request) { $user = auth()->user(); if ($user->verifyTwoFactorCode($request->code)) { session(['2fa_verified' => true]); // Optionnel : régénérer de nouveaux codes $newCodes = $user->generateRecoveryCodes(); return redirect()->intended('/dashboard'); } return back()->withErrors(['code' => 'Code de récupération invalide']); } }
Exemple 3 : Middleware pour protection 2FA
// app/Http/Middleware/RequireTwoFactor.php namespace App\Http\Middleware; use Closure; class RequireTwoFactor { public function handle($request, Closure $next) { $user = auth()->user(); // Si 2FA est activée et non vérifiée dans cette session if ($user && $user->isTwoFactorEnabled() && !session('2fa_verified')) { return redirect()->route('2fa.verify'); } return $next($request); } } // Dans Kernel.php protected $routeMiddleware = [ // ... '2fa' => \App\Http\Middleware\RequireTwoFactor::class, ]; // Dans web.php Route::middleware(['auth', '2fa'])->group(function () { Route::get('/dashboard', [DashboardController::class, 'index']); // Routes protégées par 2FA... });
🧪 Test du package
Le package inclut une suite complète de tests. Pour les exécuter :
composer test
Ou avec PHPUnit directement :
./vendor/bin/phpunit
📊 API Reference
Trait HasOneTimePasswords
| Méthode | Paramètres | Retour | Description |
|---|---|---|---|
sendOtp() |
string $type, string $destination, ?array $channels, ?array $meta, ?int $expiresInMinutes, ?int $maxAttempts |
OtpResponseData |
Envoie un nouvel OTP |
resendOtp() |
string $type, string $destination, ?array $channels, ?array $meta, ?int $expiresInMinutes, ?int $maxAttempts |
OtpResponseData |
Renvoie un OTP |
verifyOtp() |
string $code, string $type, string $destination, bool $consume = true |
OtpResponseData |
Vérifie un OTP |
cancelOtps() |
string $type, string $destination |
int |
Annule les OTPs en attente |
hasValidOtp() |
string $type, string $destination |
bool |
Vérifie si un OTP valide existe |
getPendingOtp() |
string $type, string $destination |
?OneTimePassword |
Récupère l'OTP en attente |
cleanupExpiredOtps() |
- | int |
Nettoie les OTPs expirés |
Trait HasTwoFactorAuthentication
| Méthode | Paramètres | Retour | Description |
|---|---|---|---|
getTwoFactorSecret() |
- | TwoFactorSecret |
Obtient ou crée un secret TOTP |
isTwoFactorEnabled() |
- | bool |
Vérifie si la 2FA est activée |
enableTwoFactor() |
string $code |
bool |
Active la 2FA avec vérification du code |
disableTwoFactor() |
- | bool |
Désactive la 2FA |
verifyTwoFactorCode() |
string $code |
bool |
Vérifie un code TOTP ou de récupération |
getTwoFactorQrCodeUri() |
- | string |
URI du QR code pour Google Authenticator |
generateRecoveryCodes() |
- | array |
Génère de nouveaux codes de récupération |
getRecoveryCodes() |
- | array |
Récupère les codes de récupération stockés |
Types de réponse OtpResponseData
$response = $user->sendOtp(...); // Méthodes $response->isSuccess(); // bool $response->isFailed(); // bool $response->toArray(); // array // Propriétés $response->status; // OtpStatus enum $response->errorCode; // ErrorCode|null $response->message; // string|null $response->data; // array|null
Enums
OtpStatus: SUCCESS, FAILED, RATE_LIMITED, INVALID_CODE, EXPIRED_CODE, MAX_ATTEMPTS_EXCEEDED, NOT_FOUND, SEND_FAILED, RESEND_FAILED
ErrorCode: RATE_LIMIT_EXCEEDED, OTP_NOT_FOUND, INVALID_OTP, MAX_ATTEMPTS_EXCEEDED, OTP_SEND_FAILED, OTP_RESEND_FAILED, OTP_EXPIRED
🔒 Sécurité
- Les codes OTP sont hachés avec
Hash::make()avant stockage - Les codes de récupération sont hachés avec SHA-256 (ou configurable)
- Rate limiting intégré pour prévenir les attaques par force brute
- Fenêtre de validation TOTP configurable pour tolérer le décalage horaire
- Utilisation de
hash_equals()pour les comparaisons résistantes aux timing attacks - Génération cryptographique sécurisée avec
random_int()
🐛 Dépannage
Problème : Les emails OTP ne sont pas envoyés
Solution : Vérifiez la configuration mail de Laravel et que votre modèle implémente Notifiable.
Problème : QR code non scannable
Solution : Assurez-vous que la configuration issuer dans mfa.totp.issuer ne contient pas d'espaces ou caractères spéciaux.
Problème : Codes 2FA invalides
Solution : Augmentez la fenêtre de validation window dans la configuration TOTP (ex: window=2 pour tolérer ±60 secondes).
Problème : Translations non chargées
Solution : Publiez et personnalisez les fichiers de langue :
php artisan vendor:publish --tag=mfa-translations --force
🤝 Contribution
Les contributions sont les bienvenues ! Veuillez :
- Fork le projet
- Créer une branche (
git checkout -b feature/amazing-feature) - Commiter vos changements (
git commit -m 'feat: add amazing feature') - Pusher (
git push origin feature/amazing-feature) - Ouvrir une Pull Request
📄 License
MIT License - voir le fichier LICENSE pour plus de détails.
🙏 Crédits
- OTPHP - Bibliothèque TOTP
- BaconQrCode - Générateur de QR codes
- ParagonIE Constant Time Encoding - Encodage Base32 sécurisé
📞 Support
- Documentation : https://github.com/andydefer/laravel-mfa
- Issues : https://github.com/andydefer/laravel-mfa/issues
Fait avec ❤️ pour la communauté Laravel