-
Notifications
You must be signed in to change notification settings - Fork 119
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
base: main
Are you sure you want to change the base?
Conversation
I love this implementation, I did something similar in my project, and while I was thinking about security, I think there are two things to consider:
I'm not an expert in security, this is just something that crossed my mind. |
@tnylea great work, would love to see it getting in there. Btw (might be offtopic a little) but does anyone has any suggestion on what best way to implement this in existing project (based on laravel livewire starter kit) is? |
public string $companyName; | ||
|
||
/** | ||
* Generate new recovery codes for the user. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* Generate new recovery codes for the user. | |
* Generate a QR code image and secret key for the user. |
The best way is probably waiting for the PR to merge, so everything is reviewed and ready to use, and then look at the changed files and copy/merge into your project by hand. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't reviewed the inner workings of the code just yet, but have some nits before I dive in (see comments).
First, love the screen visuals, design part looks great.
Some other notes (all notes refer to the images below them):
This looks like a button, is there another way to style this to make it look more like a status or indicator?
This looks great, couple of things:
- The lock icon should be closed? Unless open in standard in this case?
- Would love a “copy” and/or “download” button
- We’re mixing zinc (body bg color) with stone and the grays feel off, maybe we lock in on zinc? Not a designer but it’s triggering my spidey sense.
Punched in some random numbers on the 2FA screen after login to see what the error looks like and got this (also happens on correct code entry afterwards).
public function __invoke($user): void | ||
{ | ||
// Get the remember preference from the session (default to false if not set) | ||
$remember = Session::get('login.remember', false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason to not use Session::pull
since you're doing it below anyway?
* @param mixed $user | ||
* @return void | ||
*/ | ||
public function __invoke($user): Collection |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're not using this argument, remove?
|
||
namespace App\Actions\TwoFactorAuth; | ||
|
||
use App\Models\User; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import
use BaconQrCode\Renderer\ImageRenderer; | ||
use BaconQrCode\Renderer\RendererStyle\RendererStyle; | ||
use BaconQrCode\Writer; | ||
use App\Models\User; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import
} | ||
} | ||
} | ||
let that = this; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you use an arrow function for the setTimeout
, don't you avoid having to set this variable?
let focusLastInput = (paste.length <= this.total_digits) ? paste.length : this.total_digits; | ||
this.$refs['input' + focusLastInput].focus(); | ||
if (paste.length >= this.total_digits) { | ||
let that = this; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See arrow function comment above
@@ -29,20 +32,56 @@ public function login(): void | |||
$this->ensureIsNotRateLimited(); | |||
if (! Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) { | |||
// Get the user model from auth config in a single line | |||
$userAttemptingLogin = User::where('email', $this->email)->first(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if the user isn't coming from User
model? Can we tap into the auth user resolver?
RateLimiter::clear($this->throttleKey($user)); | ||
// Redirect to the intended page | ||
return $this->redirectIntended(default: route('dashboard', absolute: false), navigate: true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're returning here, no need for an else
statement below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, the ProcessRecoveryCode::class
will return false if the entered recovery code does not match. If it returns false, we need to display a message to the user, which occurs in the else
condition.
However, feel free to elaborate on your thinking; perhaps there's something I'm missing.
Thanks!
use App\Actions\TwoFactorAuth\GenerateQrCodeAndSecretKey; | ||
use App\Actions\TwoFactorAuth\GenerateNewRecoveryCodes; | ||
use Illuminate\Support\Facades\Session; | ||
use Illuminate\Support\Facades\Hash; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import
My reply to the discussion about not using I'm not saying you need to use During a successful login Also, I'm pretty sure the new logic will also fail to trigger My suggestion is to either replicate those events and rehashing within the starter kit logic, or create a methods on the Guard that will fire the events and perform rehashing, but also support the 2-step process needed here. I can see it being useful for others, so something in the framework could be useful. Also would avoid duplicating across the different starter kits. As for Fortify not using |
*/ | ||
protected function throttleKey(User $user): string | ||
{ | ||
return Str::transliterate($user->id . '|2fa|' . request()->ip()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest dropping the IP and limiting purely based on User ID. Otherwise, it would be possible for an attacker to rotate their IP to bypass the limit.
There's a critical error when attempting a two factor verification, on rate limitation code. It's the "ensureIsNotRateLimited" function at \resources\views\livewire\auth\two-factor-challenge.blade.php Fix would be this:
Fixes #84 (review) |
I think it would be wise to confirm the 2FA code once again before allowing to disable the 2FA. At the moment, just a button click disables it off. |
Thanks for the review, @joetannenbaum; I've implemented all the recommendations, as well as the security recommendations from @valorin (I appreciate it!). I've also made a few additional security updates, pointed out in this comment: laravel/vue-starter-kit#120 (comment), from the Vue PR. @iamsandakelum, thank you for pointing out the issue above. I've gone ahead and fixed it. |
This PR adds Two Factor Authentication to this starter kit.
On the user settings page, a new tab called Two-Factor Auth will be added (we can rename this tab if desired).
When the user clicks that tab, they will see the following page which notifies them that 2FA is currently Disabled.
Clicking the Enable button will open a modal with a QR code and an alpha-numeric code. The user can enter this into their 2FA application (Google Authenticator).
Next, after clicking Continue, they’ll be prompted to enter the authentication code from their authenticator app.
If the user enters the correct code the modal will close and they will see the following screen.
This shows the user that 2FA is now Enabled and provides them with Recovery codes. Now, when the user logs in to their account, they will see an additional authentication page where they are prompted for their 2FA code.
The user may also click the Use Recovery Codes button, and they will be presented with an input to enter one of their recovery codes. A recovery code can only be used once. After the user enters the recovery code, it will no longer be valid. If the user adds all the codes to the input, the logic will strip out the first code and verify it.
The 2FA challenge page utilizes the
auth
layout, so it will look good on any layout the developer chooses.Other Starter Kit 2FA Feature PR's
React: github.com/laravel/react-starter-kit/pull/101
Vue: github.com/laravel/vue-starter-kit/pull/120