Laravel Authentication: Under The Hood

Mario Vega
11 min readJul 24, 2018

--

As an aspiring web artisan developing on Laravel, the responsibility for knowing how authentication works in my chosen framework is on me. Previously I had thrown up my hands and trusted in Laravel’s occult magic to authenticate my users for me. When I couldn’t implement a custom authentication method, I knew that relying on the dark arts of others would no longer suffice. In order to master the web application craft, I’d need to embrace the authentication process and understand it head-on.

This column is an expanded and edited version of the notes I took for myself on that journey. I’m sure I’m rehashing to some degree what’s already been said before — Laravel’s own documentation on authentication is, per usual, comprehensive and illuminating. However, the documentation is focused on implementation, not how things work under the hood, and I couldn’t find anything else that described the authentication process in a way that I could wrap my head around.

Authentication in Laravel

There are two major areas in the Laravel request lifecycle where authentication comes into play:

  • When a user attempts to log in
  • When a user attempts to access authenticated content

The two application flows (login and authenticated content) share much of the same underlying functionality. Both are powered by Laravel’s mix of guards, drivers and providers, which we’ll cover in detail in a moment. The differences are significant enough, however, to where they warrant separate columns, or else run the risk of this column being significantly too long.

Because I’m researching this topic in order to support an API, my research has focused on the authenticated content flow, as my login process won’t interact with the Laravel default.

Accordingly, I’ll be focusing on the per-request authentication process in this column; Part 2 will cover the login process and consider the similarities between them.

The goal of this two-part series is to understand the authentication (auth) process well enough to write custom drivers and providers with full confidence that we know what’s going on under the hood. I’ll cover some of the parts I had trouble with, in hopes that a future someone can avoid the same stumbling blocks I encountered.

Authentication Within the Laravel Request Lifecycle

In order to understand how authentication works on a per-request basis, let’s discern its role within the lifecycle of a Laravel request.

After your application has been bootstrapped and before a given request hits your controller logic, every Laravel request goes through a middleware pipeline. Though you’ve probably encountered middleware in your applications and perhaps even written a few custom middleware yourself, their role within your application has always been a bit opaque, at least to me.

Probably the best analogy I’ve found in explaining the role of middleware is the experience of going through an airport on the way to your flight. After arriving at the airport and before your plane takes off for parts unknown, there are several checkpoints you’ll go through on the way to your airline seat. Some of these checkpoints have the power to reject you (if TSA doesn’t like the size of your toothpaste container, for instance) and some simply streamline the process of getting to your seat (like boarding by ticket zone).

Substitute checkpoints for middleware, and takeoff for the launch of your application, and you have a good idea of how the Laravel request lifecycle works. Your request goes through various middleware checkpoints, some of which transform the request and some of which can reject the request entirely.

Authentication is one of the latter group — it’s middleware that can, and does, reject requests that don’t match the criterion that you’ve defined for your application. Essentially, think of authentication as the TSA of your web application, but if TSA worked instantly and with near 100% accuracy.

Found footage of hackers attempting to hack a Laravel application.

By default, this middleware is defined within your app\Http\Kernel.php file, which points to the Laravel middleware at Illuminate\Auth\Middleware\Authenticate.

From here, as you might know already, you can assign the authentication middleware to any route in your application. Here’s a simple example:

The call to middleware tells our application to send requests to this route through the Authenticate middleware. The colon between auth and api tells Laravel to pass everything after the colon into the middleware as an argument. In this case, we're telling our application that we want to use the api guard to protect this specific endpoint.

Before we get any further, though, there’s something we need to talk about.

Guards, Providers, and Drivers, Oh My!

By far one of my biggest hurdles in wrapping my head around Laravel authentication was parsing the difference between the different tools at your disposal.

There are three distinct types involved:

  • Guards
  • Providers
  • Drivers

Part of the reason I decided to write this series was to wrap my head around these concepts. Several days of research later, I still can’t say that I fully understand the distinctions as clearly as I’d like — but I believe I understand it well enough to explain here.

In part, my confusion is fueled by the practice within Laravel of sometimes using guards and drivers interchangeably and sometimes treating them as distinct units. If I can find a way to resolve this discrepancy cleanly without introducing other problems, I’ll submit a pull request to do so. For now, I hope that a clear explanation of my understanding will suffice.

Guards are the top-level authentication abstraction available within Laravel. If you’re interacting with authentication within the context of your application, you’ll refer to guards, not providers or drivers.

  • Guards consist of a driver and a provider.

Providers specify how users are defined within your application. The two batteries-included drivers within Laravel are eloquent and database. Eloquent uses Eloquent to fetch a User model, where database pulls your users directly from the database, in case your user tables don't adhere to the default User class.

Drivers are the “logic” behind your auth process. Once your provider retrieves a suitable user, your driver checks that user to see whether they’re allowed to authenticate on your website.

  • The default driver for APIs is token, which checks to see whether the user has an api_token field that matches the api_token provided by the request.

I tried to think of an analogy that could help to define the relationship between these three concepts, and mostly fell flat.

The closest thing I could come up with is Mary Shelley’s Frankenstein. The reason this metaphor works better than the rest is because, just as guards and drivers can be easily confused, most people believe that the name Frankenstein refers to the monster, not the doctor who created it.

Under this analogy, guards are Dr. Frankenstein, thanklessly combining providers (the monster’s body parts) and drivers (the monster’s life force, and a healthy dose of electricity) to create something truly unique — only to be confused for eternity with his own creation.

Like capitalism, my analogy is the worst one possible, except for all the rest of them. I’ll happily accept pull requests for a better analogy, but until then, Frankenstein helped me, so Frankenstein stays.

IT’S AUTHENTICATED!!!!

A Tour Through the Code: Authentication Under the Hood

Ham-fisted analogies out of the way, let’s get into some code.

Here’s the Authenticate middleware, also available here:

The handle method is the primary function of all middleware, so let's start there. Like all middleware, our handle method accepts the request and the next middleware in the stack. If our authenticate method doesn't throw an exception, the request moves on to the next checkpoint.

You’ll notice that this middleware also accepts a third parameter — an array of guards. These guards are specified when you define your routes, like we did with our api route earlier. Anything after the colon will be converted into an array and passed to the handle method to be used in our auth process.

As it happens, the handle method delegates much of the responsibility for authentication to the authenticate method, which in turn uses the auth property defined in the Auth\Factory contract.

If no guards are passed to authenticate, our application falls back to the default guard. Otherwise, we'll load each provided guard and use it on our request to see if authentication was successful. If one of the guards passes, our application sets that as the guard that should be used and allows the request to continue. Otherwise an AuthenticationException is thrown, terminating the request. Next time, bring a travel size.

As it turns out, the Auth\Factory middleware is bound to the AuthManager class within Illuminate\Auth, which as it turns out is the epicenter for much of Laravel's authentication functionality.

Inside the Auth Factory

The AuthManager class is fairly large, and as such can be intimidating if you attempt to understand it all at once. Instead, let's start at the methods we've already seen in use and work inwards from there.

Assuming we’ve defined a guard in our routes, the entry point from the Authenticate middleware to the AuthManager class is the guard method:

Again, a fairly straightforward method (I love how many of those there are in Laravel).

First we find out which guard we need to load. If we’re provided the name of a guard we use that, otherwise the default is pulled in from your application’s config/auth.php file.

Next, we see whether this guard has been loaded into memory already. If it has, we use it; if not, we load it using the resolve method and the name of the guard we want.

The first thing that the resolve method does is load the necessary configuration from your auth.php file. To do this, it goes through each defined guard and returns the name of the driver and provider for the guard you've specified. If it can't find the guard, an exception is thrown.

Next, it checks to see whether you’re using a custom driver, and if so loads your guard using the custom driver you’ve created. If you’re writing your own driver, your own code will take over from here, but let’s inspect the Laravel defaults for now.

The next step is to find out which first-party driver should be used to create your guard. To find out, it defines a method name by using the driver name you provided in auth.php and checks if the AuthManager contains such a method. If it does, that method is called and returned as the correct guard. Otherwise, an exception is thrown, and your request's journey ends here.

You might notice the first sign of interchangeability between guards and drivers. By the method’s own documentation, the returned value is a resolved instance of a Guard. However, the methods it's calling and returning under the hood are all variations of createTokenDriver, createSessionDriver, etc. Could these functions be called createTokenGuard and createSessionGuard without breaking Laravel in some forbidden way? Let's find out.

We create a new provider based on what’s defined in our auth.php, then use that provider to create a TokenGuard. After refreshing our request to use the new guard we've just created, we return the guard.

It would definitely appear as if this function could be renamed to createTokenGuard with a minimum of confusion. As the TokenGuard contains within itself all the driver logic that it will use to authenticate our user, the two concepts appear in this case to be interchangeable.

At this point, the guard is returned to our Authenticatable middleware, and the check method is called on each guard returned. In order to understand what the check method does, we've got to dig deeper into the Guard class to understand how it interacts with the provider and driver.

Inside the TokenGuard

Before getting into implementation details, sometimes I like to look at interfaces for a basic understanding of what a given class is supposed to do.

We see a lot of the functionality that we’d expect out of a top-level authentication object.

There are methods to extract basic information about a user, methods to check a user’s status within your application, and methods to validate and load users into your guard.

We see lots of methods that we’d expect our drivers and providers to implement under the hood, supporting our thesis that the guard is a top-level wrapper around these functionalities.

As it turns out, most of these methods are implemented in the GuardHelpers trait, which is shared between the various first-party guards.

Here’s the check method, as implemented by the GuardHelpers trait:

Very simple — it calls the user method and determines whether there's something there.

What this user method does depends on the guard you're using, although it should always return a user if it can find one. Let's check out the user method within the TokenGuard class:

After checking for and potentially returning a preloaded user, the method defines $user as null. Unless this variable is populated by subsequent functions, it will remain null and the check method will fail, causing the guard to fail in turn.

Next, the user method attempts to pull a token from the request using the getTokenForRequest method. If the method returns a token (ie. if $token is not empty), a user is then fetched from the provider using the token from the request. Ideally, we want to find a user whose api_token is identical to the token we find in the request.

If this description is a bit abstract, let’s run through an example. If I want to authenticate over a given API, I could request a token from the API service. When I get the token, I can include it in my API calls using the api_token field, either as an input in a POST request or as a query string in a GET request.

Then, during the authentication process, the TokenGuard will fetch the api_token from the request while calling the user method we're looking at now. If that token matches the token retrieved by the user provider, we're good to use the API — or, at least, to continue onto the next middleware check. If not, an exception is thrown, and we're left to navigate a cold, bleak, API-less world.

Conclusion

We’ll look more into the specific drivers and providers that comprise the Laravel authentication experience in Part 2 of this series, in which we’ll also cover the login process. With a better understanding of the way in which drivers, providers and guards interact within the auth process, the next column will include more code and less shark-jumping metaphors.

--

--