feat(auth): harden authentication and add configurable two-factor support

This commit is contained in:
2026-07-31 03:12:46 +02:00
parent 235d5f646c
commit e47243f7dc
51 changed files with 2904 additions and 1359 deletions

View File

@@ -4,6 +4,8 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Notifications\TwoFactorSecurityNotification;
use App\Services\Auth\SecurityEventRecorder;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
@@ -21,19 +23,19 @@ class TwoFactorSettingsController extends Controller
*/
public function enable(Request $request, EnableTwoFactorAuthentication $enable): RedirectResponse
{
$this->ensureFeatureIsEnabled();
$this->ensureFeatureIsEnabled($request);
/** @var User $user */
$user = $request->user();
if ($user->hasEnabledTwoFactorAuthentication()) {
return redirect()->route('profile.show');
return $this->enrollmentRedirect($request);
}
$this->validatePasswordForPasswordUser($request, $user);
$enable($user, force: true);
return redirect()->route('profile.show')
return $this->enrollmentRedirect($request)
->with('success', 'Scan the QR code, then enter a code to finish enabling two-factor authentication.');
}
@@ -42,9 +44,10 @@ class TwoFactorSettingsController extends Controller
*/
public function confirm(
Request $request,
ConfirmTwoFactorAuthentication $confirm
ConfirmTwoFactorAuthentication $confirm,
SecurityEventRecorder $securityEvents
): RedirectResponse {
$this->ensureFeatureIsEnabled();
$this->ensureFeatureIsEnabled($request);
$validated = $request->validate([
'code' => ['required', 'string', 'regex:/^\d{6}$/'],
@@ -53,8 +56,18 @@ class TwoFactorSettingsController extends Controller
/** @var User $user */
$user = $request->user();
$confirm($user, $validated['code']);
$user->notify(new TwoFactorSecurityNotification(
'Two-factor authentication was enabled for your account.',
(string) $request->ip(),
now()->toIso8601String()
));
$securityEvents->record('two_factor.enabled', $user, $request);
return redirect()->route('profile.show')
$redirect = $request->routeIs('two-factor.setup.confirm')
? redirect()->route('two-factor.setup')
: redirect()->route('profile.show');
return $redirect
->with('success', 'Two-factor authentication is now enabled.')
->with('recoveryCodes', $user->fresh()->recoveryCodes());
}
@@ -64,9 +77,15 @@ class TwoFactorSettingsController extends Controller
*/
public function disable(
Request $request,
DisableTwoFactorAuthentication $disable
DisableTwoFactorAuthentication $disable,
SecurityEventRecorder $securityEvents
): RedirectResponse {
$this->ensureFeatureIsEnabled();
$this->ensureFeatureIsEnabled($request);
abort_if(
config('auth-ui.features.two_factor_required'),
403,
'Two-factor authentication is required for this application.'
);
/** @var User $user */
$user = $request->user();
@@ -76,6 +95,12 @@ class TwoFactorSettingsController extends Controller
}
$disable($user);
$user->notify(new TwoFactorSecurityNotification(
'Two-factor authentication was disabled for your account.',
(string) $request->ip(),
now()->toIso8601String()
));
$securityEvents->record('two_factor.disabled', $user, $request);
return redirect()->route('profile.show')
->with('success', 'Two-factor authentication has been disabled.');
@@ -86,9 +111,10 @@ class TwoFactorSettingsController extends Controller
*/
public function recoveryCodes(
Request $request,
GenerateNewRecoveryCodes $generate
GenerateNewRecoveryCodes $generate,
SecurityEventRecorder $securityEvents
): RedirectResponse {
$this->ensureFeatureIsEnabled();
$this->ensureFeatureIsEnabled($request);
/** @var User $user */
$user = $request->user();
@@ -102,6 +128,14 @@ class TwoFactorSettingsController extends Controller
if ($request->boolean('regenerate')) {
$generate($user);
$user->refresh();
$user->notify(new TwoFactorSecurityNotification(
'New two-factor authentication recovery codes were generated for your account.',
(string) $request->ip(),
now()->toIso8601String()
));
$securityEvents->record('two_factor.recovery_codes_regenerated', $user, $request);
} else {
$securityEvents->record('two_factor.recovery_codes_revealed', $user, $request);
}
return redirect()->route('profile.show')
@@ -111,9 +145,20 @@ class TwoFactorSettingsController extends Controller
->with('recoveryCodes', $user->recoveryCodes());
}
private function ensureFeatureIsEnabled(): void
private function ensureFeatureIsEnabled(Request $request): void
{
abort_unless(config('auth-ui.features.two_factor'), 404);
if ($request->routeIs('two-factor.setup.*')) {
abort_unless(config('auth-ui.features.two_factor_required'), 404);
}
}
private function enrollmentRedirect(Request $request): RedirectResponse
{
return $request->routeIs('two-factor.setup.*')
? redirect()->route('two-factor.setup')
: redirect()->route('profile.show');
}
private function validatePasswordForPasswordUser(Request $request, User $user): void
@@ -131,8 +176,6 @@ class TwoFactorSettingsController extends Controller
{
if ($user->hasPassword()) {
$this->validatePasswordForPasswordUser($request, $user);
return;
}
$validated = $request->validate([