In Depth: Setting up Two-Factor Authentication!
[In Depth #37] It's time to finally fulfil one of the most common requests for an In Depth article: setting up 2FA! 🎉 So let's add some TOTP 2FA to our boring user/pass auth login!

Now that we've reviewed the new Starter Kits (pt1, pt2), and complained about the lack of Multi-Factor Authentication (MFA) a few times, it's time to finally fulfil one of the most common requests for these In Depth articles... today we're adding 2FA (Two-Factor Authentication) to a Laravel app!
We're going to take a Laravel app that uses a standard Blade-based authentication system, and update it to include TOTP (Time-based One-Time Passcode) as a second authentication factor. Along the way, we'll discuss SMS and Email OTPs, Magic Links, the different ways MFA can be enforced, and - if we get time - Passkeys!
If you're ready for this, let's go!
Enforcing 2FA
The first thing we need to decide is how 2FA will be enforced in our app, as it dictates what changes we need to make and where it sits in the flow.
There are three main ways I've come across for how to implement a TOTP challenge as part of 2FA:
- Two-step login process
- One-step login process
- Enforce via Middleware
Let's look at them in detail, and pick the best one for our app:
Two-step Login Process
This is the most obvious 2FA flow you'll encounter, although it is also the most complicated to implement. The first step is to verify the user credentials as per normal, and upon success redirect the user to the 2FA challenge. If the 2FA challenge is successful, complete the login process by starting the authenticated session and letting the user into the app.
Pros
- Authenticated session created only after 2FA successful.
- User feedback on failed credentials before 2FA, gives nicer user experience.
- 2FA challenge can support most method, since it's independent of user credentials.
Cons
- Can't be "dropped in" to existing app, requires custom authentication flow.
- Complexity around verifying user credentials, remembering user, without actually initiating full session.
- The complexities of setting up this flow definitely discourage some developers from adding 2FA to their apps.
One-step Login Process
Rather than verifying users and then hitting the 2FA challenge, this process collects the user's credentials and their 2FA token and verifies them in the same request. Some apps have this all on the one screen, while others will visually present them as 2 screens on the frontend, while making a single request to the server to verify. The app verifies credentials + 2FA, and either throws an error or starts the authenticated session on success.
Pros
- Simpler authentication flow - no need to verify user credentials and persist until 2FA challenge is completed.
- Authenticated session created only after 2FA successful.
Cons
- User feedback on failed credentials (i.e. password) only after 2FA, which gives a bad user experience.
- Potentially requires storing raw password between requests, which risks exposure of password.
- Can't be "dropped in" to existing app, requires custom authentication flow.
- Not really compatible with non-instant 2FA (i.e. SMS or Email token could force refresh, losing entered credentials).
Enforce via Middleware
The simplest option is to introduce a new piece of middleware for all authenticated sessions. After the user is authenticated using credentials and the authenticated session is initiated, the middleware is triggered and checks if the user has verified their 2FA. If the user has not verified 2FA, it redirects to the 2FA challenge route, if they have then it just lets the request pass through.
Pros
- Simple to implement - no need to modify the existing authentication system!
- Supports all form of 2FA.
- Can be easily used to confirm 2FA for sensitive steps, or on a timeout.
- User feedback on failed credentials before 2FA, gives nicer user experience.
Cons
- Attacker gains access to authentication session.
- Authenticated routes missing 2FA middleware are exposed*.
- APIs and third-party tooling (Horizon, Nova, etc) can be exposed if they don't include the 2FA Middleware
- Separates Authentication from 2FA, potentially removing it from logging and monitoring and extra security protections.
/profile
) were not covered by the 2FA middleware. This included the 2FA page... All I had to do was visit /profile/2fa
in my browser, click the Disable 2FA
button, and I had full access to the user's account!Picking An Option
When it comes to implementing 2FA, it may be tempted to reach for the easy option, such as Enforcing via Middleware. To be honest, if you look at the list of Cons for that option, you'll notice they basically all boil down to one: "Attacker gains access to authentication session". But this is a huge issue. If an attacker can gain authenticated access, there is the potential for so much damage to occur. So this option is out.
The One-step Login Process on the other hand, suffers from two rather significant issues: First, you need to store the user's raw password somewhere! This is a terrible idea - the less time you have a raw password being processed in code, the better. There is too much risk of it being included in a log file or traceroute somewhere on the server, or a malicious plugin/app on the user's device finding it. Secondly, the user experience is rubbish. Users will assume their password worked, hit the 2FA, and then be rejected due to invalid password*. You need to tell them about the failed credentials when they enter them.
Which leaves us with the Two-step Login Process! Implementing it as part of the authentication system is the most secure way to include 2FA - the user is properly verified via the first factor (credentials) and then their second (TOTP), before they get an authenticated session.
But unfortunately, it is more work and can't simply be dropped in... 😔
First Steps
Now that we know where in our code the 2FA is going, we need something to power it - but we're definitely not going to build our own!