Configuration SMTP et 2FA par code PIN e-mail
Paramètres du site : - Nouvelle section "Serveur SMTP" avec host, port, chiffrement, identifiant, mot de passe, adresse/nom d'expéditeur - Bouton "Envoyer un e-mail de test" (AJAX via Symfony EsmtpTransport) : tente la connexion + envoie un message réel à l'admin - Badge "Configuré — 2FA actif" quand SMTP est en place - Suppression de la configuration possible Authentification 2FA : - Si SMTP configuré : après validation identifiant/mot de passe, l'utilisateur est déconnecté, un PIN à 6 chiffres est généré, haché (bcrypt) et stocké en session, envoyé par e-mail (10 min) - Page /2fa : saisie du PIN, bouton "Renvoyer le code", retour login - Si l'envoi e-mail échoue : fallback sans 2FA (logue l'erreur) - Si SMTP non configuré : login standard inchangé Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,42 +4,68 @@ namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Auth\LoginRequest;
|
||||
use App\Mail\TwoFactorPinMail;
|
||||
use App\Services\SiteSettingsService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AuthenticatedSessionController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the login view.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming authentication request.
|
||||
*/
|
||||
public function store(LoginRequest $request): RedirectResponse
|
||||
{
|
||||
$request->authenticate();
|
||||
|
||||
// Si SMTP configuré → 2FA par code PIN
|
||||
if (SiteSettingsService::smtpConfigured()) {
|
||||
$user = Auth::user();
|
||||
$intended = $request->session()->pull('url.intended', route('dashboard'));
|
||||
|
||||
$pin = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
|
||||
// Déconnecter et stocker les données 2FA dans la session
|
||||
Auth::logout();
|
||||
$request->session()->regenerate();
|
||||
$request->session()->put([
|
||||
'2fa.user_id' => $user->id,
|
||||
'2fa.pin_hash' => Hash::make($pin),
|
||||
'2fa.expires_at' => now()->addMinutes(10)->timestamp,
|
||||
'2fa.intended' => $intended,
|
||||
]);
|
||||
|
||||
try {
|
||||
Mail::to($user->email)->send(new TwoFactorPinMail($pin, $user->name));
|
||||
} catch (\Exception $e) {
|
||||
// Si l'envoi échoue, on logue l'erreur et on laisse passer sans 2FA
|
||||
Log::error('2FA: envoi du PIN impossible.', ['error' => $e->getMessage()]);
|
||||
$request->session()->forget('2fa');
|
||||
Auth::login($user);
|
||||
$request->session()->regenerate();
|
||||
return redirect($intended);
|
||||
}
|
||||
|
||||
return redirect()->route('2fa.challenge');
|
||||
}
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy an authenticated session.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Mail\TwoFactorPinMail;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class TwoFactorController extends Controller
|
||||
{
|
||||
public function challenge(Request $request): View|RedirectResponse
|
||||
{
|
||||
if (! $request->session()->has('2fa.user_id')) {
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
$user = User::find($request->session()->get('2fa.user_id'));
|
||||
if (! $user) {
|
||||
$request->session()->forget('2fa');
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
return view('auth.two-factor', [
|
||||
'maskedEmail' => $this->maskEmail($user->email),
|
||||
]);
|
||||
}
|
||||
|
||||
public function verify(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate(['pin' => ['required', 'string', 'digits:6']]);
|
||||
|
||||
$userId = $request->session()->get('2fa.user_id');
|
||||
$pinHash = $request->session()->get('2fa.pin_hash');
|
||||
$expiresAt = $request->session()->get('2fa.expires_at');
|
||||
|
||||
if (! $userId || ! $pinHash) {
|
||||
return redirect()->route('login')
|
||||
->withErrors(['email' => 'Session expirée. Veuillez vous reconnecter.']);
|
||||
}
|
||||
|
||||
if (now()->timestamp > (int) $expiresAt) {
|
||||
$request->session()->forget('2fa');
|
||||
return redirect()->route('login')
|
||||
->withErrors(['email' => 'Le code PIN a expiré. Veuillez vous reconnecter.']);
|
||||
}
|
||||
|
||||
if (! Hash::check($request->input('pin'), $pinHash)) {
|
||||
return back()->withErrors(['pin' => 'Code incorrect. Vérifiez votre e-mail et réessayez.']);
|
||||
}
|
||||
|
||||
// PIN valide — authentifier l'utilisateur
|
||||
$user = User::findOrFail($userId);
|
||||
$intended = $request->session()->pull('2fa.intended', route('dashboard'));
|
||||
|
||||
$request->session()->forget('2fa');
|
||||
Auth::login($user);
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect($intended);
|
||||
}
|
||||
|
||||
public function resend(Request $request): RedirectResponse
|
||||
{
|
||||
$userId = $request->session()->get('2fa.user_id');
|
||||
if (! $userId) {
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
$user = User::find($userId);
|
||||
if (! $user) {
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
$pin = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
|
||||
$request->session()->put([
|
||||
'2fa.pin_hash' => Hash::make($pin),
|
||||
'2fa.expires_at' => now()->addMinutes(10)->timestamp,
|
||||
]);
|
||||
|
||||
try {
|
||||
Mail::to($user->email)->send(new TwoFactorPinMail($pin, $user->name));
|
||||
} catch (\Exception $e) {
|
||||
return back()->withErrors(['pin' => "Impossible d'envoyer le code : " . $e->getMessage()]);
|
||||
}
|
||||
|
||||
return back()->with('resent', true);
|
||||
}
|
||||
|
||||
private function maskEmail(string $email): string
|
||||
{
|
||||
[$local, $domain] = explode('@', $email, 2);
|
||||
$visible = min(2, strlen($local));
|
||||
$masked = substr($local, 0, $visible) . str_repeat('*', max(strlen($local) - $visible, 3));
|
||||
return $masked . '@' . $domain;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user