212 lines
6.9 KiB
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.show');
|
|
|
|
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.show')
|
|
->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.show')
|
|
->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.show');
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|