Skip to content

Commit ed51c24

Browse files
Add Two Factor Authentication for React Starter Kit (#156)
* feat: add two-factor authentication * feat: update package-lock * feat: add tests * linting changes * feat: enhance two-factor authentication implementation and improve code consistency * Update two-factor authentication implementation and refactor related components * Update two-factor authentication implementation and refactor related components * Formatting * Formatting Recovery Code * refactor * Fix Test * Formatting * Simplify Inertia Form * Refactor Test * Fix Issue * Remove explicit typing * formating * cursor pointer for buttons styled as links * use custom clipboard hook * change condition signature to positive first * fix typo * formatting * Add hasSetupData to avoid enabling 2FA again * Refactor two-factor authentication types to local scope * formatting * formatting * formatting * share more common elements, de-dupe * Add an error state in use-two-factor-auth.ts * Refactor error handling in use-two-factor-auth.ts to use an array for errors instead of an object * Improve two-factor authentication error handling and UI updates * extract alert error to its own component * Update use-two-factor-auth.ts --------- Co-authored-by: Joe Tannenbaum <[email protected]>
1 parent 3382e92 commit ed51c24

32 files changed

+1623
-112
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 to_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: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Settings;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Http\Requests\Settings\TwoFactorAuthenticationRequest;
7+
use Illuminate\Routing\Controllers\HasMiddleware;
8+
use Illuminate\Routing\Controllers\Middleware;
9+
use Inertia\Inertia;
10+
use Inertia\Response;
11+
use Laravel\Fortify\Features;
12+
13+
class TwoFactorAuthenticationController extends Controller implements HasMiddleware
14+
{
15+
/**
16+
* Get the middleware that should be assigned to the controller.
17+
*/
18+
public static function middleware(): array
19+
{
20+
return Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')
21+
? [new Middleware('password.confirm', only: ['show'])]
22+
: [];
23+
}
24+
25+
/**
26+
* Show the user's two-factor authentication settings page.
27+
*/
28+
public function show(TwoFactorAuthenticationRequest $request): Response
29+
{
30+
$request->ensureStateIsValid();
31+
32+
return Inertia::render('settings/two-factor', [
33+
'twoFactorEnabled' => $request->user()->hasEnabledTwoFactorAuthentication(),
34+
'requiresConfirmation' => Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm'),
35+
]);
36+
}
37+
}

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|null $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
{
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace App\Http\Requests\Settings;
4+
5+
use Illuminate\Foundation\Http\FormRequest;
6+
use Laravel\Fortify\Features;
7+
use Laravel\Fortify\InteractsWithTwoFactorState;
8+
9+
class TwoFactorAuthenticationRequest extends FormRequest
10+
{
11+
use InteractsWithTwoFactorState;
12+
13+
/**
14+
* Determine if the user is authorized to make this request.
15+
*/
16+
public function authorize(): bool
17+
{
18+
return Features::enabled(Features::twoFactorAuthentication());
19+
}
20+
21+
/**
22+
* Get the validation rules that apply to the request.
23+
*
24+
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
25+
*/
26+
public function rules(): array
27+
{
28+
return [];
29+
}
30+
}

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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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(fn () => Inertia::render('auth/two-factor-challenge'));
28+
Fortify::confirmPasswordView(fn () => Inertia::render('auth/confirm-password'));
29+
30+
RateLimiter::for('two-factor', function (Request $request) {
31+
return Limit::perMinute(5)->by($request->session()->get('login.id'));
32+
});
33+
}
34+
}

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.30",
1415
"laravel/framework": "^12.0",
1516
"laravel/tinker": "^2.10.1",
1617
"laravel/wayfinder": "^0.1.9"

0 commit comments

Comments
 (0)