Laravel: Free 2FA for all your users

“I’m Going To Build My Own Theme Park With Blackjack and H**kers”

Italo Baeza Cabrera
Feb 10 · 6 min read

There are not toooooooooo many Two Factor Authentication packages for PHP out there. The ones I found mostly vary between two offerings:

I’m not against these two kind of packages. Not in the slightest. I think Spomky Labs’ OTP is quite complete, and Antonio Oribeiro’s 2FA-QRCode package has a lot of tools, but I only need a bicycle, not a tank.

I you think about it, you’re paying with time by implementing the whole thing yourself and coupling it tightly to your application code, or with money for an easy implementation, covering all the “what if” scenarios plus support.

Being a poor ass in my country, where the minimum wage is approximately USD $2, paying for just logins of your users in the long term is not on my tables. Shoving the whole implementation of OTP to just use two methods is neither, since I only want TOTP, but there are a lot of cool tools going around nevertheless.

After asking in Reddit if someone knew a drop-in package for Laravel, that turned out without satisfactory responses, I decided to open up my editor and start coding. And walá, Laraguard was born.

You just want the solution? Just grab the package and you’re done.

How did I get there? What is this place?

Let’s start from the basics so both are on the same page.

When a another person knows someone’s password, it’s easy to just log in and do whatever they want. To avoid this, the 2FA, also called “Two Factor Authentication”, was born.

The concept of 2FA is simple: the user must confirm he is the issuer of its credentials using information contained in another “place”, like a phone. If you take the password and the device from the equation, no authentication can proceed.

One of the many mechanisms for 2FA is called TOTP, as “Time-based One-Time Password”. As you can read, a secondary password is generated by a device that only works for a given period of time, and cannot be used more than once.

A TOTP mechanism is comprised of two key pieces:

  • The storage that holds the Shared Secret.
  • The logic that verifies codes.

The problem on implementing 2FA in Laravel is that the authentication was created as one-step only. In other words, there is no way to intercede in the Authentication mechanism gracefully once the credentials are sent: if these are indeed correct, the User will be authenticated, otherwise it won’t. Period.

Where to another step without having to rewire everything?

Also, the attempt() method of the Guard, responsible to persist the user into the application, only returns true on complete success, or false on failure.

There are ways to add a “intermediate” step, though.

The most this-does-not-belong-here™ way to add a Two Factor Authentication is just shove in a middleware along all the routes you want to protect, and save into the session or cache if the user used 2FA to log in, or ask him for the correct code. To me, is like having the nightclub bouncer asking for your ID every time you go to the bathroom. No thanks.

The other way, and probably more appropriate, is to tackle on the Login attempt itself. The first is to is to add a macro or new method to the Guard, and edit your Login Controller to ask the Guard to check for 2FA before attempting to login the User. Sounds like an easy way, but that means editing the Login Controller with a method call to the Session Guard (or any ward) outside the Contract.

It would be cool if the Contract had a callback to fire just before the user is validated. The best I could do without breaking the Authentication was to add the Validated event to the framework.

Now, imagine the shenanigans we are have to do just to have a macro that checks the user for 2FA. Yes, since this happens before any attempt, we will need to retrieve the user two times in total: one to check for 2FA, and the other to properly validate the rest of credentials.

Kill it with fire.

The other solution is to just extend the attempt() method of the Guard to not only validate the User but also check for 2FA. This allows to only retrieve the user once from the User Provider, but the problem is that we won’t know why the Login attempt failed, since we only receive true or false.

Sounds sane. Hint: its not.

If you want to know why the attempt failed, you will have to create another public method in the Guard (outside any Contract) to check why the last attempt failed: if it was because the 2FA was required and no code was received, it received a Code and was wrong, or the rest of the credentials were incorrect.

And then, I figured out the Session Guard uses Events. Guess what.

I used events.

Events to the Rescue!

What my package does is relatively simple: registers a listener that hooks up to the Validated event, which has been introduced in Laravel 6.15, and the Attempting event.

In a nutshell, once the User is retrieved using the event data, we will ensure to ask him a code before proceeding with the log in procedure. We can use the listener to stop the authentication in its tracks by forcefully throwing a response asking for this 2FA Code.

Next, we will check if it also issued his 2FA Code and if it’s correct to proceed, otherwise we will kick him out with a view asking him for the correct code.

That’s it. Of course, there is four lines of code you have to add to this to work as intended, but that’s literally waaaaaaay better than rewiring the whole Login Controller or editing the Guard itself.

Is that it? No, there is more:

  • Works with any Guard that fires up Events.
  • No Middleware. No Controllers. No Routes.
  • Comes with Recovery Codes.
  • Can “remember” a device to not ask 2FA codes every damn time.
  • OTPAuth URIs and QR Codes out of the box.
  • No obnoxious OTP logic that nobody will use ever.

Give it a go and tell me what you think.

Italo Baeza Cabrera

Written by

Graphic Designer graduate. Full Stack Web Developer. Retired Tech & Gaming Editor.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade