php
63 lines · 10 steps
Two-factor auth challenge flow in Laravel
How a controller and middleware cooperate to gate login behind a TOTP code stored in the session.
Explained by
highlit
1class TwoFactorController extends Controller
2{
3 public function show(Request $request): View|RedirectResponse
4 {
5 if (! $request->session()->has('2fa:user:id')) {
6 return redirect()->route('login');
7 }
8
9 return view('auth.two-factor');
10 }
11
12 public function store(Request $request): RedirectResponse
13 {
14 $data = $request->validate([
15 'code' => ['required', 'string'],
16 ]);
17
18 $userId = $request->session()->get('2fa:user:id');
19
20 abort_unless($userId, 419);
21
22 $user = User::findOrFail($userId);
23
24 if (! $this->google2fa->verifyKey($user->two_factor_secret, $data['code'])) {
25 RateLimiter::hit($this->throttleKey($request));
26
27 throw ValidationException::withMessages([
28 'code' => __('The provided two-factor code is invalid.'),
29 ]);
30 }
31
32 $request->session()->forget('2fa:user:id');
33 $request->session()->put('2fa:passed_at', now());
34 $request->session()->regenerate();
35
36 Auth::loginUsingId($user->id, $request->session()->pull('2fa:remember', false));
37
38 return redirect()->intended(route('dashboard'));
39 }
40
41 protected function throttleKey(Request $request): string
42 {
43 return 'two-factor:'.$request->ip();
44 }
45}
46
47class EnsureTwoFactorIsConfirmed
48{
49 public function handle(Request $request, Closure $next): Response
50 {
51 $user = $request->user();
52
53 if ($user?->two_factor_secret && ! $request->session()->has('2fa:passed_at')) {
54 $request->session()->put('2fa:user:id', $user->id);
55
56 Auth::logout();
57
58 return redirect()->route('two-factor.challenge');
59 }
60
61 return $next($request);
62 }
63}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A pending user id in the session is what links the challenge screen, verification, and the middleware into one flow.
- 2Regenerating the session and only calling loginUsingId after a valid code prevents fixation and premature authentication.
- 3Rate-limiting verification attempts by IP blunts brute-forcing of the short numeric code.
Related explainers
php
<?php class NameParser {
Parsing a full name into components in PHP
string-parsing
arrays
normalization
Intermediate
8 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
php
<?php namespace App\Services;
Building a cached daily leaderboard in Laravel
caching
aggregation
eager-loading
Intermediate
9 steps
python
import secrets from django.contrib.auth import authenticate, login from django.core.cache import cache
Two-factor login with OTP in Django
two-factor-auth
one-time-passwords
caching
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/two-factor-auth-challenge-flow-in-laravel-explained-php-d43e/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.