Skip to content

Adding Two Factor Auth Feature #84

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
aed4c2d
Adding files for 2FA
tnylea Apr 4, 2025
3265500
Adding some finishing touches to 2fa
tnylea Apr 16, 2025
ebcd24a
removing middleware file to rename
tnylea Apr 17, 2025
f541ff6
Adding correct file
tnylea Apr 17, 2025
dc4bdae
Adding updates to support dark mode in custom designs
tnylea Apr 17, 2025
74c255f
Adding a few more updates to recovery code section
tnylea Apr 17, 2025
43397be
Re-organization
tnylea Apr 17, 2025
36f5121
updating user model with user model from config/auth
tnylea Apr 17, 2025
e346f42
fixing the login and auth middleware
tnylea Apr 17, 2025
07ab1be
cleaning up some of the tests
tnylea Apr 17, 2025
00eafe4
refactoring the challenge auth page with new Actions
tnylea Apr 17, 2025
fd98bc2
clean and refactor
tnylea Apr 17, 2025
3623e9e
Remove decryption of 2FA secret when verifying authentication code
tnylea Apr 17, 2025
329f6f7
Adding a few more updates
tnylea Apr 17, 2025
6347ecc
rendering the QR code using SVG as opposed to png with Imagick
tnylea Apr 17, 2025
33ae2b1
reverting the style back to simple auth layout
tnylea Apr 17, 2025
5f5645a
updating syntax in the migration
tnylea Apr 18, 2025
0f4958e
fixing the authentication to first check if user has entered a correc…
tnylea Apr 18, 2025
069b0cc
updating the auth functionality to check credentials before 2fa
tnylea Apr 18, 2025
05cefc1
re-ordering
tnylea Apr 18, 2025
3273598
Update app/Actions/TwoFactorAuth/VerifyTwoFactorCode.php
tnylea Apr 22, 2025
2d11b0b
Update app/Actions/TwoFactorAuth/ProcessRecoveryCode.php
tnylea Apr 22, 2025
0b97f86
updating security fixes and passing remember me functionality
tnylea Apr 22, 2025
345ab3b
Merge branch 'feature/2fa' of https://github.com/laravel/livewire-sta…
tnylea Apr 22, 2025
f5b138c
fixing the OTP input to only allow a single digit
tnylea Apr 22, 2025
99143ec
Adding update
tnylea Apr 22, 2025
039933f
Adding security fixes to 2fa functionality
tnylea Apr 22, 2025
87f4219
Adding throttling and security fixes to the 2fa challenge
tnylea Apr 22, 2025
d693b5b
keeping things consistent with React version
tnylea Apr 22, 2025
43506e1
wip
joetannenbaum Apr 24, 2025
f0b8a5a
Added better security practices, cleaned code and fixed a few bugs
tnylea Apr 30, 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 @@ -6,6 +6,7 @@
/storage/*.key
/storage/pail
/vendor
.DS_Store
.env
.env.backup
.env.production
Expand Down
27 changes: 27 additions & 0 deletions app/Actions/TwoFactorAuth/CompleteTwoFactorAuthentication.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace App\Actions\TwoFactorAuth;

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;

class CompleteTwoFactorAuthentication
{
/**
* Complete the two-factor authentication process.
*
* @param mixed $user The user to authenticate
* @return void
*/
public function __invoke($user): void
{
// Get the remember preference from the session (default to false if not set)
$remember = Session::pull('login.remember', false);

// Log the user in with the remember preference
Auth::login($user, $remember);

// Clear the session variables used for the 2FA challenge
Session::forget(['login.id']);
}
}
21 changes: 21 additions & 0 deletions app/Actions/TwoFactorAuth/ConfirmTwoFactorAuthentication.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace App\Actions\TwoFactorAuth;

class ConfirmTwoFactorAuthentication
{
/**
* Confirm two factor authentication for the user.
*
* @param mixed $user
* @return bool
*/
public function __invoke($user)
{
$user->forceFill([
'two_factor_confirmed_at' => now(),
])->save();

return true;
}
}
26 changes: 26 additions & 0 deletions app/Actions/TwoFactorAuth/DisableTwoFactorAuthentication.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace App\Actions\TwoFactorAuth;

class DisableTwoFactorAuthentication
{
/**
* Disable two factor authentication for the user.
*
* @return void
*/
public function __invoke($user)
{
if (
! is_null($user->two_factor_secret) ||
! is_null($user->two_factor_recovery_codes) ||
! is_null($user->two_factor_confirmed_at)
) {
$user->forceFill([
'two_factor_secret' => null,
'two_factor_recovery_codes' => null,
'two_factor_confirmed_at' => null,
])->save();
}
}
}
26 changes: 26 additions & 0 deletions app/Actions/TwoFactorAuth/GenerateNewRecoveryCodes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace App\Actions\TwoFactorAuth;

use Illuminate\Support\Collection;
use Illuminate\Support\Str;

class GenerateNewRecoveryCodes
{
/**
* Generate new recovery codes for the user.
*
* @return void
*/
public function __invoke(): Collection
{
return Collection::times(8, function () {
return $this->generate();
});
}

public function generate()
{
return Str::random(10).'-'.Str::random(10);
}
}
53 changes: 53 additions & 0 deletions app/Actions/TwoFactorAuth/GenerateQrCodeAndSecretKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace App\Actions\TwoFactorAuth;

use BaconQrCode\Renderer\Image\SvgImageBackEnd;
use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Writer;
use PragmaRX\Google2FA\Google2FA;

class GenerateQrCodeAndSecretKey
{
/**
* The length of the secret key to generate.
*/
private static int $SECRET_KEY_LENGTH = 16;

public string $companyName;

/**
* Generate a QR code image and secret key for the user.
*
* @return array{string, string}
*/
public function __invoke($user): array
{
$google2fa = new Google2FA;
$secret_key = $google2fa->generateSecretKey(self::$SECRET_KEY_LENGTH);

$this->companyName = 'Auth';

if (is_string(config('app.name'))) {
$this->companyName = config('app.name');
}

$g2faUrl = $google2fa->getQRCodeUrl(
$this->companyName,
(string) $user->email,
$secret_key
);

$writer = new Writer(
new ImageRenderer(
new RendererStyle(400),
new SvgImageBackEnd()
)
);

$qrcode_image = base64_encode($writer->writeString($g2faUrl));

return [$qrcode_image, $secret_key];
}
}
27 changes: 27 additions & 0 deletions app/Actions/TwoFactorAuth/GetTwoFactorAuthenticatableUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace App\Actions\TwoFactorAuth;

use Illuminate\Support\Facades\Session;

class GetTwoFactorAuthenticatableUser
{
/**
* Get the user that is in the process of two-factor authentication.
*
* @return mixed|null The user model instance or null if not found
*/
public function __invoke()
{
$userId = Session::get('login.id');

if (!$userId) {
return null;
}

// Get the user model from auth config
$userModel = app(config('auth.providers.users.model'));

return $userModel::find($userId);
}
}
36 changes: 36 additions & 0 deletions app/Actions/TwoFactorAuth/ProcessRecoveryCode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace App\Actions\TwoFactorAuth;

class ProcessRecoveryCode
{
/**
* Verify a recovery code and remove it from the list if valid.
*
* @param array $recoveryCodes The array of recovery codes
* @param string $submittedCode The code submitted by the user
* @return array|false Returns the updated array of recovery codes if valid, or false if invalid
*/
public function __invoke(
#[\SensitiveParameter] array $recoveryCodes,
#[\SensitiveParameter] string $submittedCode
) {
// Clean the submitted code
$submittedCode = trim($submittedCode);

// If the user has entered multiple codes, only validate the first one
$submittedCode = explode(" ", $submittedCode)[0];

// Check if the code is valid
if (!in_array($submittedCode, $recoveryCodes)) {
return false;
}

// Remove the used recovery code from the list
$updatedCodes = array_values(array_filter($recoveryCodes, function($code) use ($submittedCode) {
return !hash_equals($code, $submittedCode);
}));

return $updatedCodes;
}
}
23 changes: 23 additions & 0 deletions app/Actions/TwoFactorAuth/VerifyTwoFactorCode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace App\Actions\TwoFactorAuth;

use PragmaRX\Google2FA\Google2FA;

class VerifyTwoFactorCode
{
/**
* Verify a two-factor authentication code.
*
* @param string $secret The decrypted secret key
* @param string $code The code to verify
* @return bool
*/
public function __invoke(
#[\SensitiveParameter] string $secret,
#[\SensitiveParameter] string $code
): bool {
$google2fa = new Google2FA();
return $google2fa->verifyKey($secret, $code);
}
}
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
"license": "MIT",
"require": {
"php": "^8.2",
"bacon/bacon-qr-code": "^3.0",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1",
"livewire/flux": "^2.1.1",
"livewire/volt": "^1.7.0"
"livewire/volt": "^1.7.0",
"pragmarx/google2fa": "^8.0"
},
"require-dev": {
"fakerphp/faker": "^1.23",
Expand Down
Loading