Skip to content

Commit 429e5ec

Browse files
committed
feat: add two-factor authentication
1 parent 21e50ce commit 429e5ec

25 files changed

+1102
-58
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
/storage/*.key
1111
/storage/pail
1212
/vendor
13+
.DS_Store
1314
.env
1415
.env.backup
1516
.env.production

app/Http/Controllers/Auth/AuthenticatedSessionController.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Illuminate\Support\Facades\Route;
1111
use Inertia\Inertia;
1212
use Inertia\Response;
13+
use Laravel\Fortify\Features;
1314

1415
class AuthenticatedSessionController extends Controller
1516
{
@@ -29,7 +30,18 @@ public function create(Request $request): Response
2930
*/
3031
public function store(LoginRequest $request): RedirectResponse
3132
{
32-
$request->authenticate();
33+
$user = $request->validateCredentials();
34+
35+
if (Features::enabled(Features::twoFactorAuthentication()) && $user->hasEnabledTwoFactorAuthentication()) {
36+
$request->session()->put([
37+
'login.id' => $user->getKey(),
38+
'login.remember' => $request->boolean('remember'),
39+
]);
40+
41+
return redirect()->route('two-factor.login');
42+
}
43+
44+
Auth::login($user, $request->boolean('remember'));
3345

3446
$request->session()->regenerate();
3547

app/Http/Controllers/Auth/ConfirmablePasswordController.php

Lines changed: 0 additions & 41 deletions
This file was deleted.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Concerns;
4+
5+
use Illuminate\Http\Request;
6+
use Illuminate\Support\Facades\Auth;
7+
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
8+
use Laravel\Fortify\Features;
9+
10+
trait ConfirmsTwoFactorAuthentication
11+
{
12+
/**
13+
* Validate the two-factor authentication state for the request.
14+
*/
15+
protected function validateTwoFactorAuthenticationState(Request $request): void
16+
{
17+
if (! Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm')) {
18+
return;
19+
}
20+
21+
$currentTime = time();
22+
23+
// Notate totally disabled state in session...
24+
if ($this->twoFactorAuthenticationDisabled($request)) {
25+
$request->session()->put('two_factor_empty_at', $currentTime);
26+
}
27+
28+
// If was previously totally disabled this session but is now confirming, notate time...
29+
if ($this->hasJustBegunConfirmingTwoFactorAuthentication($request)) {
30+
$request->session()->put('two_factor_confirming_at', $currentTime);
31+
}
32+
33+
// If the profile is reloaded and is not confirmed but was previously in confirming state, disable...
34+
if ($this->neverFinishedConfirmingTwoFactorAuthentication($request, $currentTime)) {
35+
app(DisableTwoFactorAuthentication::class)(Auth::user());
36+
37+
$request->session()->put('two_factor_empty_at', $currentTime);
38+
$request->session()->remove('two_factor_confirming_at');
39+
}
40+
}
41+
42+
/**
43+
* Determine if two-factor authentication is totally disabled.
44+
*/
45+
protected function twoFactorAuthenticationDisabled(Request $request): bool
46+
{
47+
return is_null($request->user()->two_factor_secret) &&
48+
is_null($request->user()->two_factor_confirmed_at);
49+
}
50+
51+
/**
52+
* Determine if two-factor authentication is just now being confirmed within the last request cycle.
53+
*/
54+
protected function hasJustBegunConfirmingTwoFactorAuthentication(Request $request): bool
55+
{
56+
return ! is_null($request->user()->two_factor_secret) &&
57+
is_null($request->user()->two_factor_confirmed_at) &&
58+
$request->session()->has('two_factor_empty_at') &&
59+
is_null($request->session()->get('two_factor_confirming_at'));
60+
}
61+
62+
/**
63+
* Determine if two-factor authentication was never totally confirmed once confirmation started.
64+
*/
65+
protected function neverFinishedConfirmingTwoFactorAuthentication(Request $request, int $currentTime): bool
66+
{
67+
return ! array_key_exists('code', $request->session()->getOldInput()) &&
68+
is_null($request->user()->two_factor_confirmed_at) &&
69+
$request->session()->get('two_factor_confirming_at', 0) != $currentTime;
70+
}
71+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Settings;
4+
5+
use App\Http\Controllers\Concerns\ConfirmsTwoFactorAuthentication;
6+
use App\Http\Controllers\Controller;
7+
use Illuminate\Http\Request;
8+
use Illuminate\Http\Response as HttpResponse;
9+
use Illuminate\Routing\Controllers\HasMiddleware;
10+
use Illuminate\Routing\Controllers\Middleware;
11+
use Inertia\Inertia;
12+
use Inertia\Response;
13+
use Laravel\Fortify\Features;
14+
15+
class TwoFactorAuthenticationController extends Controller implements HasMiddleware
16+
{
17+
use ConfirmsTwoFactorAuthentication;
18+
19+
/**
20+
* Get the middleware that should be assigned to the controller.
21+
*/
22+
public static function middleware(): array
23+
{
24+
return Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')
25+
? [new Middleware('password.confirm', only: ['show'])]
26+
: [];
27+
}
28+
29+
/**
30+
* Show the user's two-factor authentication settings page.
31+
*/
32+
public function show(Request $request): Response
33+
{
34+
abort_if(
35+
! Features::enabled(Features::twoFactorAuthentication()),
36+
HttpResponse::HTTP_FORBIDDEN,
37+
'Two factor authentication is disabled.'
38+
);
39+
40+
$this->validateTwoFactorAuthenticationState($request);
41+
42+
return Inertia::render('settings/two-factor', [
43+
'requiresConfirmation' => Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm'),
44+
'twoFactorEnabled' => $request->user()->hasEnabledTwoFactorAuthentication(),
45+
]);
46+
}
47+
}

app/Http/Requests/Auth/LoginRequest.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Http\Requests\Auth;
44

5+
use App\Models\User;
56
use Illuminate\Auth\Events\Lockout;
67
use Illuminate\Foundation\Http\FormRequest;
78
use Illuminate\Support\Facades\Auth;
@@ -32,15 +33,18 @@ public function rules(): array
3233
}
3334

3435
/**
35-
* Attempt to authenticate the request's credentials.
36+
* Validate the request's credentials and return the user without logging them in.
3637
*
3738
* @throws \Illuminate\Validation\ValidationException
3839
*/
39-
public function authenticate(): void
40+
public function validateCredentials(): User
4041
{
4142
$this->ensureIsNotRateLimited();
4243

43-
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
44+
/** @var User $user */
45+
$user = Auth::getProvider()->retrieveByCredentials($this->only('email', 'password'));
46+
47+
if (! $user || ! Auth::getProvider()->validateCredentials($user, $this->only('password'))) {
4448
RateLimiter::hit($this->throttleKey());
4549

4650
throw ValidationException::withMessages([
@@ -49,6 +53,8 @@ public function authenticate(): void
4953
}
5054

5155
RateLimiter::clear($this->throttleKey());
56+
57+
return $user;
5258
}
5359

5460
/**
@@ -75,7 +81,7 @@ public function ensureIsNotRateLimited(): void
7581
}
7682

7783
/**
78-
* Get the rate limiting throttle key for the request.
84+
* Get the rate-limiting throttle key for the request.
7985
*/
8086
public function throttleKey(): string
8187
{

app/Models/User.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
use Illuminate\Database\Eloquent\Factories\HasFactory;
77
use Illuminate\Foundation\Auth\User as Authenticatable;
88
use Illuminate\Notifications\Notifiable;
9+
use Laravel\Fortify\TwoFactorAuthenticatable;
910

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

1516
/**
1617
* The attributes that are mass assignable.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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 Inertia\Inertia;
10+
use Laravel\Fortify\Fortify;
11+
12+
class FortifyServiceProvider extends ServiceProvider
13+
{
14+
/**
15+
* Register any application services.
16+
*/
17+
public function register(): void
18+
{
19+
//
20+
}
21+
22+
/**
23+
* Bootstrap any application services.
24+
*/
25+
public function boot(): void
26+
{
27+
Fortify::twoFactorChallengeView(function () {
28+
return Inertia::render('auth/two-factor-challenge');
29+
});
30+
31+
Fortify::confirmPasswordView(function () {
32+
return Inertia::render('auth/confirm-password');
33+
});
34+
35+
RateLimiter::for('two-factor', function (Request $request) {
36+
return Limit::perMinute(5)->by($request->session()->get('login.id'));
37+
});
38+
}
39+
}

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
@@ -11,6 +11,7 @@
1111
"require": {
1212
"php": "^8.2",
1313
"inertiajs/inertia-laravel": "^2.0",
14+
"laravel/fortify": "^1.29",
1415
"laravel/framework": "^12.0",
1516
"laravel/tinker": "^2.10.1",
1617
"laravel/wayfinder": "^0.1.9"

0 commit comments

Comments
 (0)