Files
laravel-nuxt-ui-inertia-tem…/app/Http/Controllers/Auth/TwoFactorSettingsController.php

212 lines
6.9 KiB
PHP

<?php
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;
use Laravel\Fortify\Actions\ConfirmTwoFactorAuthentication;
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
use Laravel\Fortify\Actions\GenerateNewRecoveryCodes;
use Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider;
use Laravel\Fortify\Fortify;
class TwoFactorSettingsController extends Controller
{
/**
* Begin two-factor enrollment.
*/
public function enable(Request $request, EnableTwoFactorAuthentication $enable): RedirectResponse
{
$this->ensureFeatureIsEnabled($request);
/** @var User $user */
$user = $request->user();
if ($user->hasEnabledTwoFactorAuthentication()) {
return $this->enrollmentRedirect($request);
}
$this->validatePasswordForPasswordUser($request, $user);
$enable($user, force: true);
return $this->enrollmentRedirect($request)
->with('success', 'Scan the QR code, then enter a code to finish enabling two-factor authentication.');
}
/**
* Confirm a newly configured authenticator.
*/
public function confirm(
Request $request,
ConfirmTwoFactorAuthentication $confirm,
SecurityEventRecorder $securityEvents
): RedirectResponse {
$this->ensureFeatureIsEnabled($request);
$validated = $request->validate([
'code' => ['required', 'string', 'regex:/^\d{6}$/'],
]);
/** @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);
$redirect = $request->routeIs('two-factor.setup.confirm')
? redirect()->route('two-factor.setup')
: redirect()->route('profile.security');
return $redirect
->with('success', 'Two-factor authentication is now enabled.')
->with('recoveryCodes', $user->fresh()->recoveryCodes());
}
/**
* Disable two-factor authentication.
*/
public function disable(
Request $request,
DisableTwoFactorAuthentication $disable,
SecurityEventRecorder $securityEvents
): RedirectResponse {
$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();
if ($user->hasEnabledTwoFactorAuthentication()) {
$this->validateSensitiveAction($request, $user);
}
$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.security')
->with('success', 'Two-factor authentication has been disabled.');
}
/**
* Reveal or regenerate recovery codes after re-authentication.
*/
public function recoveryCodes(
Request $request,
GenerateNewRecoveryCodes $generate,
SecurityEventRecorder $securityEvents
): RedirectResponse {
$this->ensureFeatureIsEnabled($request);
/** @var User $user */
$user = $request->user();
if (! $user->hasEnabledTwoFactorAuthentication()) {
abort(409, 'Two-factor authentication is not enabled.');
}
$this->validateSensitiveAction($request, $user);
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.security')
->with('success', $request->boolean('regenerate')
? 'New recovery codes have been generated. Previous codes no longer work.'
: 'Recovery codes revealed.')
->with('recoveryCodes', $user->recoveryCodes());
}
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.security');
}
private function validatePasswordForPasswordUser(Request $request, User $user): void
{
if (! $user->hasPassword()) {
return;
}
$request->validate([
'password' => ['required', 'string', 'current_password:web'],
]);
}
private function validateSensitiveAction(Request $request, User $user): void
{
if ($user->hasPassword()) {
$this->validatePasswordForPasswordUser($request, $user);
}
$validated = $request->validate([
'code' => ['required', 'string'],
]);
if (! $this->hasValidTwoFactorCredential($user, $validated['code'])) {
throw ValidationException::withMessages([
'code' => 'The provided authentication or recovery code was invalid.',
]);
}
}
private function hasValidTwoFactorCredential(User $user, string $code): bool
{
$secret = Fortify::currentEncrypter()->decrypt($user->two_factor_secret);
if (app(TwoFactorAuthenticationProvider::class)->verify($secret, $code)) {
return true;
}
$recoveryCode = collect($user->recoveryCodes())
->first(fn (string $recoveryCode) => hash_equals($recoveryCode, $code));
if (! $recoveryCode) {
return false;
}
$user->replaceRecoveryCode($recoveryCode);
return true;
}
}