Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
429e5ec
feat: add two-factor authentication
pushpak1300 Aug 26, 2025
ebfcf9e
Merge branch 'main' into feat/add_two_factor_auth
pushpak1300 Aug 26, 2025
a822d3d
feat: update package-lock
pushpak1300 Aug 26, 2025
ee5f641
feat: add tests
pushpak1300 Aug 26, 2025
29c0df1
linting changes
pushpak1300 Aug 26, 2025
6840944
Merge branch 'main' into feat/add_two_factor_auth
pushpak1300 Aug 29, 2025
bf996b7
feat: enhance two-factor authentication implementation and improve co…
pushpak1300 Aug 29, 2025
38766c1
Update two-factor authentication implementation and refactor related …
pushpak1300 Sep 1, 2025
39bd4bd
Update two-factor authentication implementation and refactor related …
pushpak1300 Sep 1, 2025
07d1019
Formatting
pushpak1300 Sep 1, 2025
ee8bcf3
Formatting Recovery Code
pushpak1300 Sep 1, 2025
94f117f
refactor
pushpak1300 Sep 1, 2025
d1a9b23
Fix Test
pushpak1300 Sep 1, 2025
e2972bb
Formatting
pushpak1300 Sep 1, 2025
ab5140c
Simplify Inertia Form
pushpak1300 Sep 3, 2025
b612ae5
Refactor Test
pushpak1300 Sep 5, 2025
5c38d18
Fix Issue
pushpak1300 Sep 5, 2025
5397f80
Remove explicit typing
pushpak1300 Sep 5, 2025
db3dc97
formating
pushpak1300 Sep 8, 2025
791f3d6
cursor pointer for buttons styled as links
joetannenbaum Sep 9, 2025
2114888
use custom clipboard hook
pushpak1300 Sep 9, 2025
a5f231a
change condition signature to positive first
pushpak1300 Sep 9, 2025
8710fe4
fix typo
pushpak1300 Sep 9, 2025
02be899
formatting
pushpak1300 Sep 9, 2025
a3dbdba
Add hasSetupData to avoid enabling 2FA again
pushpak1300 Sep 9, 2025
4aea6c6
Refactor two-factor authentication types to local scope
pushpak1300 Sep 9, 2025
6260c6b
formatting
pushpak1300 Sep 9, 2025
cf5971d
formatting
pushpak1300 Sep 9, 2025
85e3005
formatting
pushpak1300 Sep 10, 2025
f779357
share more common elements, de-dupe
joetannenbaum Sep 10, 2025
e18dbbe
Add an error state in use-two-factor-auth.ts
pushpak1300 Sep 11, 2025
416b731
Refactor error handling in use-two-factor-auth.ts to use an array for…
pushpak1300 Sep 11, 2025
9965517
Merge branch 'main' into feat/add_two_factor_auth
pushpak1300 Sep 15, 2025
bfd7e8c
Improve two-factor authentication error handling and UI updates
pushpak1300 Sep 16, 2025
b93fa47
extract alert error to its own component
joetannenbaum Sep 18, 2025
f93a661
Update use-two-factor-auth.ts
joetannenbaum Sep 18, 2025
b4d4b6d
Merge branch 'main' into feat/add_two_factor_auth
pushpak1300 Sep 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
/storage/*.key
/storage/pail
/vendor
.DS_Store
.env
.env.backup
.env.production
Expand Down
14 changes: 13 additions & 1 deletion app/Http/Controllers/Auth/AuthenticatedSessionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use Inertia\Response;
use Laravel\Fortify\Features;

class AuthenticatedSessionController extends Controller
{
Expand All @@ -29,7 +30,18 @@ public function create(Request $request): Response
*/
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$user = $request->validateCredentials();

if (Features::enabled(Features::twoFactorAuthentication()) && $user->hasEnabledTwoFactorAuthentication()) {
$request->session()->put([
'login.id' => $user->getKey(),
'login.remember' => $request->boolean('remember'),
]);

return redirect()->route('two-factor.login');
}

Auth::login($user, $request->boolean('remember'));

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

Expand Down
41 changes: 0 additions & 41 deletions app/Http/Controllers/Auth/ConfirmablePasswordController.php

This file was deleted.

71 changes: 71 additions & 0 deletions app/Http/Controllers/Concerns/ConfirmsTwoFactorAuthentication.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

namespace App\Http\Controllers\Concerns;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
use Laravel\Fortify\Features;

trait ConfirmsTwoFactorAuthentication
{
/**
* Validate the two-factor authentication state for the request.
*/
protected function validateTwoFactorAuthenticationState(Request $request): void
{
if (! Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm')) {
return;
}

$currentTime = time();

// Notate totally disabled state in session...
if ($this->twoFactorAuthenticationDisabled($request)) {
$request->session()->put('two_factor_empty_at', $currentTime);
}

// If was previously totally disabled this session but is now confirming, notate time...
if ($this->hasJustBegunConfirmingTwoFactorAuthentication($request)) {
$request->session()->put('two_factor_confirming_at', $currentTime);
}

// If the profile is reloaded and is not confirmed but was previously in confirming state, disable...
if ($this->neverFinishedConfirmingTwoFactorAuthentication($request, $currentTime)) {
app(DisableTwoFactorAuthentication::class)(Auth::user());

$request->session()->put('two_factor_empty_at', $currentTime);
$request->session()->remove('two_factor_confirming_at');
}
}

/**
* Determine if two-factor authentication is totally disabled.
*/
protected function twoFactorAuthenticationDisabled(Request $request): bool
{
return is_null($request->user()->two_factor_secret) &&
is_null($request->user()->two_factor_confirmed_at);
}

/**
* Determine if two-factor authentication is just now being confirmed within the last request cycle.
*/
protected function hasJustBegunConfirmingTwoFactorAuthentication(Request $request): bool
{
return ! is_null($request->user()->two_factor_secret) &&
is_null($request->user()->two_factor_confirmed_at) &&
$request->session()->has('two_factor_empty_at') &&
is_null($request->session()->get('two_factor_confirming_at'));
}

/**
* Determine if two-factor authentication was never totally confirmed once confirmation started.
*/
protected function neverFinishedConfirmingTwoFactorAuthentication(Request $request, int $currentTime): bool
{
return ! array_key_exists('code', $request->session()->getOldInput()) &&
is_null($request->user()->two_factor_confirmed_at) &&
$request->session()->get('two_factor_confirming_at', 0) != $currentTime;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace App\Http\Controllers\Settings;

use App\Http\Controllers\Concerns\ConfirmsTwoFactorAuthentication;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\Response as HttpResponse;
use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;
use Inertia\Inertia;
use Inertia\Response;
use Laravel\Fortify\Features;

class TwoFactorAuthenticationController extends Controller implements HasMiddleware
{
use ConfirmsTwoFactorAuthentication;

/**
* Get the middleware that should be assigned to the controller.
*/
public static function middleware(): array
{
return Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')
? [new Middleware('password.confirm', only: ['show'])]
: [];
}

/**
* Show the user's two-factor authentication settings page.
*/
public function show(Request $request): Response
{
abort_if(
! Features::enabled(Features::twoFactorAuthentication()),
HttpResponse::HTTP_FORBIDDEN,
'Two factor authentication is disabled.'
);

$this->validateTwoFactorAuthenticationState($request);

return Inertia::render('settings/two-factor', [
'requiresConfirmation' => Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm'),
'twoFactorEnabled' => $request->user()->hasEnabledTwoFactorAuthentication(),
]);
}
}
14 changes: 10 additions & 4 deletions app/Http/Requests/Auth/LoginRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Http\Requests\Auth;

use App\Models\User;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
Expand Down Expand Up @@ -32,15 +33,18 @@ public function rules(): array
}

/**
* Attempt to authenticate the request's credentials.
* Validate the request's credentials and return the user without logging them in.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function authenticate(): void
public function validateCredentials(): User
{
$this->ensureIsNotRateLimited();

if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
/** @var User $user */
$user = Auth::getProvider()->retrieveByCredentials($this->only('email', 'password'));

if (! $user || ! Auth::getProvider()->validateCredentials($user, $this->only('password'))) {
RateLimiter::hit($this->throttleKey());

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

RateLimiter::clear($this->throttleKey());

return $user;
}

/**
Expand All @@ -75,7 +81,7 @@ public function ensureIsNotRateLimited(): void
}

/**
* Get the rate limiting throttle key for the request.
* Get the rate-limiting throttle key for the request.
*/
public function throttleKey(): string
{
Expand Down
3 changes: 2 additions & 1 deletion app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\TwoFactorAuthenticatable;

class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
use HasFactory, Notifiable, TwoFactorAuthenticatable;

/**
* The attributes that are mass assignable.
Expand Down
39 changes: 39 additions & 0 deletions app/Providers/FortifyServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace App\Providers;

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Inertia\Inertia;
use Laravel\Fortify\Fortify;

class FortifyServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}

/**
* Bootstrap any application services.
*/
public function boot(): void
{
Fortify::twoFactorChallengeView(function () {
return Inertia::render('auth/two-factor-challenge');
});

Fortify::confirmPasswordView(function () {
return Inertia::render('auth/confirm-password');
});

RateLimiter::for('two-factor', function (Request $request) {
return Limit::perMinute(5)->by($request->session()->get('login.id'));
});
}
}
1 change: 1 addition & 0 deletions bootstrap/providers.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@

return [
App\Providers\AppServiceProvider::class,
App\Providers\FortifyServiceProvider::class,
];
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"require": {
"php": "^8.2",
"inertiajs/inertia-laravel": "^2.0",
"laravel/fortify": "^1.29",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1",
"laravel/wayfinder": "^0.1.9"
Expand Down
Loading