Laravel: Free 2FA for all your users

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

Italo Baeza Cabrera
ITNEXT

--

Photo by Monica Sauro on Unsplash

Hi! I came from the future.

You should use Laragear TwoFactor. Don’t worry, works about the same, but it has active support and compatibility with new Laravel versions.

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 with the whole battalion behind.

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 and leveraging the logic to a service, which also covers the “what if” scenarios plus support.

Being a poor ass in my country, where the minimum wage is approximately USD $2, paying for just the logins the users in the long term is not on my table. 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 we 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, Two Factor Authentication, also called “2FA”, was born.

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

One of the many 2FA strategies and implementations is called TOTP, as “Time-based One-Time Password”. As you 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, avoiding eavesdropping and brute force attacks.

A TOTP mechanism is comprised of three key pieces:

  • The storage that holds the Shared Secret.
  • The logic that verifies codes.
  • The storage that remembers the code used to avoid using it again.

Adding a middle-step

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 once the credentials are valid, 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 what the Contract offers. You change the Guard, and the 2FA won’t work anymore.

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 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.

--

--

Graphic Designer graduate. Full Stack Web Developer. Retired Tech & Gaming Editor. https://italobc.com