Skip to content

Commit 7cc15cc

Browse files
Merge pull request #120 from laravel/feat/compoenent_two-factor-auth
Add Two Factor Auth For Livewire Starter Kit (Components)
2 parents cff9db3 + 2a846f7 commit 7cc15cc

23 files changed

+1242
-102
lines changed

app/Livewire/Auth/ConfirmPassword.php

Lines changed: 0 additions & 37 deletions
This file was deleted.

app/Livewire/Auth/Login.php

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
namespace App\Livewire\Auth;
44

5+
use App\Models\User;
56
use Illuminate\Auth\Events\Lockout;
67
use Illuminate\Support\Facades\Auth;
78
use Illuminate\Support\Facades\RateLimiter;
89
use Illuminate\Support\Facades\Session;
910
use Illuminate\Support\Str;
1011
use Illuminate\Validation\ValidationException;
12+
use Laravel\Fortify\Features;
1113
use Livewire\Attributes\Layout;
1214
use Livewire\Attributes\Validate;
1315
use Livewire\Component;
@@ -32,20 +34,45 @@ public function login(): void
3234

3335
$this->ensureIsNotRateLimited();
3436

35-
if (! Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) {
36-
RateLimiter::hit($this->throttleKey());
37+
$user = $this->validateCredentials();
3738

38-
throw ValidationException::withMessages([
39-
'email' => __('auth.failed'),
39+
if (Features::canManageTwoFactorAuthentication() && $user->hasEnabledTwoFactorAuthentication()) {
40+
Session::put([
41+
'login.id' => $user->getKey(),
42+
'login.remember' => $this->remember,
4043
]);
44+
45+
$this->redirect(route('two-factor.login'), navigate: true);
46+
47+
return;
4148
}
4249

50+
Auth::login($user, $this->remember);
51+
4352
RateLimiter::clear($this->throttleKey());
4453
Session::regenerate();
4554

4655
$this->redirectIntended(default: route('dashboard', absolute: false), navigate: true);
4756
}
4857

58+
/**
59+
* Validate the user's credentials.
60+
*/
61+
protected function validateCredentials(): User
62+
{
63+
$user = Auth::getProvider()->retrieveByCredentials(['email' => $this->email, 'password' => $this->password]);
64+
65+
if (! $user || ! Auth::getProvider()->validateCredentials($user, ['password' => $this->password])) {
66+
RateLimiter::hit($this->throttleKey());
67+
68+
throw ValidationException::withMessages([
69+
'email' => __('auth.failed'),
70+
]);
71+
}
72+
73+
return $user;
74+
}
75+
4976
/**
5077
* Ensure the authentication request is not rate limited.
5178
*/
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
<?php
2+
3+
namespace App\Livewire\Settings;
4+
5+
use Exception;
6+
use Laravel\Fortify\Actions\ConfirmTwoFactorAuthentication;
7+
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
8+
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
9+
use Laravel\Fortify\Features;
10+
use Laravel\Fortify\Fortify;
11+
use Livewire\Attributes\Locked;
12+
use Livewire\Attributes\Validate;
13+
use Livewire\Component;
14+
use Symfony\Component\HttpFoundation\Response;
15+
16+
class TwoFactor extends Component
17+
{
18+
#[Locked]
19+
public bool $twoFactorEnabled;
20+
21+
#[Locked]
22+
public bool $requiresConfirmation;
23+
24+
#[Locked]
25+
public string $qrCodeSvg = '';
26+
27+
#[Locked]
28+
public string $manualSetupKey = '';
29+
30+
public bool $showModal = false;
31+
32+
public bool $showVerificationStep = false;
33+
34+
#[Validate('required|string|size:6', onUpdate: false)]
35+
public string $code = '';
36+
37+
/**
38+
* Mount the component.
39+
*/
40+
public function mount(DisableTwoFactorAuthentication $disableTwoFactorAuthentication): void
41+
{
42+
abort_unless(Features::enabled(Features::twoFactorAuthentication()), Response::HTTP_FORBIDDEN);
43+
44+
if (Fortify::confirmsTwoFactorAuthentication() && is_null(auth()->user()->two_factor_confirmed_at)) {
45+
$disableTwoFactorAuthentication(auth()->user());
46+
}
47+
48+
$this->twoFactorEnabled = auth()->user()->hasEnabledTwoFactorAuthentication();
49+
$this->requiresConfirmation = Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm');
50+
}
51+
52+
/**
53+
* Enable two-factor authentication for the user.
54+
*/
55+
public function enable(EnableTwoFactorAuthentication $enableTwoFactorAuthentication): void
56+
{
57+
$enableTwoFactorAuthentication(auth()->user());
58+
59+
if (! $this->requiresConfirmation) {
60+
$this->twoFactorEnabled = auth()->user()->hasEnabledTwoFactorAuthentication();
61+
}
62+
63+
$this->loadSetupData();
64+
65+
$this->showModal = true;
66+
}
67+
68+
/**
69+
* Load the two-factor authentication setup data for the user.
70+
*/
71+
private function loadSetupData(): void
72+
{
73+
$user = auth()->user();
74+
75+
try {
76+
$this->qrCodeSvg = $user?->twoFactorQrCodeSvg();
77+
$this->manualSetupKey = decrypt($user->two_factor_secret);
78+
} catch (Exception) {
79+
$this->addError('setupData', 'Failed to fetch setup data.');
80+
81+
$this->reset('qrCodeSvg', 'manualSetupKey');
82+
}
83+
}
84+
85+
/**
86+
* Show the two-factor verification step if necessary.
87+
*/
88+
public function showVerificationIfNecessary(): void
89+
{
90+
if ($this->requiresConfirmation) {
91+
$this->showVerificationStep = true;
92+
93+
$this->resetErrorBag();
94+
95+
return;
96+
}
97+
98+
$this->closeModal();
99+
}
100+
101+
/**
102+
* Confirm two-factor authentication for the user.
103+
*/
104+
public function confirmTwoFactor(ConfirmTwoFactorAuthentication $confirmTwoFactorAuthentication): void
105+
{
106+
$this->validate();
107+
108+
$confirmTwoFactorAuthentication(auth()->user(), $this->code);
109+
110+
$this->closeModal();
111+
112+
$this->twoFactorEnabled = true;
113+
}
114+
115+
/**
116+
* Reset two-factor verification state.
117+
*/
118+
public function resetVerification(): void
119+
{
120+
$this->reset('code', 'showVerificationStep');
121+
122+
$this->resetErrorBag();
123+
}
124+
125+
/**
126+
* Disable two-factor authentication for the user.
127+
*/
128+
public function disable(DisableTwoFactorAuthentication $disableTwoFactorAuthentication): void
129+
{
130+
$disableTwoFactorAuthentication(auth()->user());
131+
132+
$this->twoFactorEnabled = false;
133+
}
134+
135+
/**
136+
* Close the two-factor authentication modal.
137+
*/
138+
public function closeModal(): void
139+
{
140+
$this->reset(
141+
'code',
142+
'manualSetupKey',
143+
'qrCodeSvg',
144+
'showModal',
145+
'showVerificationStep',
146+
);
147+
148+
$this->resetErrorBag();
149+
150+
if (! $this->requiresConfirmation) {
151+
$this->twoFactorEnabled = auth()->user()->hasEnabledTwoFactorAuthentication();
152+
}
153+
}
154+
155+
/**
156+
* Get the current modal configuration state.
157+
*/
158+
public function getModalConfigProperty(): array
159+
{
160+
if ($this->twoFactorEnabled) {
161+
return [
162+
'title' => __('Two-Factor Authentication Enabled'),
163+
'description' => __('Two-factor authentication is now enabled. Scan the QR code or enter the setup key in your authenticator app.'),
164+
'buttonText' => __('Close'),
165+
];
166+
}
167+
168+
if ($this->showVerificationStep) {
169+
return [
170+
'title' => __('Verify Authentication Code'),
171+
'description' => __('Enter the 6-digit code from your authenticator app.'),
172+
'buttonText' => __('Continue'),
173+
];
174+
}
175+
176+
return [
177+
'title' => __('Enable Two-Factor Authentication'),
178+
'description' => __('To finish enabling two-factor authentication, scan the QR code or enter the setup key in your authenticator app.'),
179+
'buttonText' => __('Continue'),
180+
];
181+
}
182+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace App\Livewire\Settings\TwoFactor;
4+
5+
use Exception;
6+
use Laravel\Fortify\Actions\GenerateNewRecoveryCodes;
7+
use Livewire\Attributes\Locked;
8+
use Livewire\Component;
9+
10+
class RecoveryCodes extends Component
11+
{
12+
#[Locked]
13+
public array $recoveryCodes = [];
14+
15+
/**
16+
* Mount the component.
17+
*/
18+
public function mount(): void
19+
{
20+
$this->loadRecoveryCodes();
21+
}
22+
23+
/**
24+
* Generate new recovery codes for the user.
25+
*/
26+
public function regenerateRecoveryCodes(GenerateNewRecoveryCodes $generateNewRecoveryCodes): void
27+
{
28+
$generateNewRecoveryCodes(auth()->user());
29+
30+
$this->loadRecoveryCodes();
31+
}
32+
33+
/**
34+
* Load the recovery codes for the user.
35+
*/
36+
private function loadRecoveryCodes(): void
37+
{
38+
$user = auth()->user();
39+
40+
if ($user->hasEnabledTwoFactorAuthentication() && $user->two_factor_recovery_codes) {
41+
try {
42+
$this->recoveryCodes = json_decode(decrypt($user->two_factor_recovery_codes), true);
43+
} catch (Exception) {
44+
$this->addError('recoveryCodes', 'Failed to load recovery codes');
45+
46+
$this->recoveryCodes = [];
47+
}
48+
}
49+
}
50+
}

app/Models/User.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
use Illuminate\Foundation\Auth\User as Authenticatable;
88
use Illuminate\Notifications\Notifiable;
99
use Illuminate\Support\Str;
10+
use Laravel\Fortify\TwoFactorAuthenticatable;
1011

1112
class User extends Authenticatable
1213
{
1314
/** @use HasFactory<\Database\Factories\UserFactory> */
14-
use HasFactory, Notifiable;
15+
use HasFactory, Notifiable, TwoFactorAuthenticatable;
1516

1617
/**
1718
* The attributes that are mass assignable.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace App\Providers;
4+
5+
use Illuminate\Cache\RateLimiting\Limit;
6+
use Illuminate\Http\Request;
7+
use Illuminate\Support\Facades\RateLimiter;
8+
use Illuminate\Support\ServiceProvider;
9+
use Laravel\Fortify\Fortify;
10+
11+
class FortifyServiceProvider extends ServiceProvider
12+
{
13+
/**
14+
* Register any application services.
15+
*/
16+
public function register(): void
17+
{
18+
//
19+
}
20+
21+
/**
22+
* Bootstrap any application services.
23+
*/
24+
public function boot(): void
25+
{
26+
Fortify::twoFactorChallengeView(fn () => view('livewire.auth.two-factor-challenge'));
27+
Fortify::confirmPasswordView(fn () => view('livewire.auth.confirm-password'));
28+
29+
RateLimiter::for('two-factor', function (Request $request) {
30+
return Limit::perMinute(5)->by($request->session()->get('login.id'));
31+
});
32+
}
33+
}

bootstrap/providers.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22

33
return [
44
App\Providers\AppServiceProvider::class,
5+
App\Providers\FortifyServiceProvider::class,
56
];

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"license": "MIT",
1111
"require": {
1212
"php": "^8.2",
13+
"laravel/fortify": "^1.30",
1314
"laravel/framework": "^12.0",
1415
"laravel/tinker": "^2.10.1",
1516
"livewire/flux": "^2.1.1",

0 commit comments

Comments
 (0)