twitter rss github stackoverflow
Joseph Silber's Avatar

Joseph Silber

Improvements to authentication in Laravel 5.3

Authentication has gotten some nice improvements in 5.3, so let's examine it piece by piece. We'll start with the current state of affairs, then take it from there.

Authentication in Laravel 5.2

In Laravel 5.2, you generally use one of the following methods to make sure a user is authenticated:

  1. The auth middleware. Using this middleware on a route, you can restrict access to logged-in users. It will redirect unauthenticated requests (or, for an AJAX request, respond with a 401).

  2. The guard's check method. Using auth()->check() in your code, you can check if the user is logged in, and branch your logic from there.

This is all nice and dandy, but it can potentially lead to a lot of duplicated code.

If you need some custom logic for an unauthenticated user, using auth()->check() works perfectly. However, most of the time you simply want to redirect an unauthenticated user (or return with a 401 if it's an AJAX request). The logic for this currently lives in the auth middleware, so there's no way to reuse it. Utilizing auth()->check() in your code means you have to replicate this logic all over again:

public function view(Request $request)
{
    if (! auth()->check()) {
        if (($request->ajax() && ! $request->pjax()) || $request->wantsJson()) {
            return response()->json(['error' => 'Unauthenticated.'], 401);
        } else {
            return redirect()->guest('login');
        }
    }

    // The user is logged in. Proceed.
}

Clearly this is a lot of extra code that we shouldn't need here. Ideally, there should be a single global location for handling unauthenticated responses. Lucky for us, Laravel 5.3 makes this a breeze!

Introducing the authenticate method

In Laravel 5.3, there's now a better way: the authenticate method. Calling authenticate will throw an exception if the user is not logged in, so that you don't even need a conditional in your code:

public function view()
{
    $user = auth()->authenticate();

    // The user is logged in. Proceed.
}

If the user isn't logged in, it'll throw an AuthenticationException. There's no need for you to catch it - Laravel gracefully handles this for you in a single global location.

The exception handler's unauthenticated method

The global exception handler catches all authentication exceptions and calls its unauthenticated method. It'll redirect the request to the login page, or, for an AJAX request, return with a 401 error status code.

You can customize this method to your liking, giving you full flexibility on how to handle unauthenticated requests in a single centralized location.

The Authenticate middleware

In Laravel 5.2, the Authenticate middleware ships in the app skeleton. This middleware is responsible for creating a valid response for an unauthenticated request. To customize its functionality, you edit the handle method directly in the middleware class.

In Laravel 5.3, the auth middleware has been pulled into the core, allowing us to properly cover it in our tests. Since the middleware does not ship with the app skeleton anymore, you cannot customize it directly. Instead, the auth middleware now throws an AuthenticationException when the user is not logged in, allowing the global exception handler to catch it. As mentioned before, you can customize the response there.

Authenticating against multiple guards

Another neat addition to the auth middleware is authentication against multiple guards.

Imagine you're using both the web & api guards throughout your app. You may have some routes that you want to be accessible if the user is logged in with either one of those guards. The updated middleware makes this a breeze:

Route::get('users', [
    'uses' => 'UsersController@index',
    'middleware' => 'auth:web,api',
]);

The auth middleware will check each guard. If the user is logged in to any of the guards, the request will be authenticated. Additionally, that guard will be set as the default, so that any calls to auth()->user() throughout your app will properly return the user.

Route model binding and global scopes

The final enhancement to authentication we'll cover here is with regards to route model binding. Route model binding is one of my favorite features in Laravel, removing boilerplate all over your controllers. For the uninitiated, here's a short primer from the docs:

Route model binding provides a convenient way to inject model instances into your routes. For example, instead of injecting a user's ID, you can inject the entire User model instance that matches the given ID.

As people started using route model binding more and more (especially since 5.2 introduced implicit model binding), they started running into the following issue: if you have a global scope on your model that tries to access the current user, well... you can't. That's because route model binding runs before authentication, so when the model resolves the current user has not yet been authenticated.

Here's some code to explain this issue with a little more detail:

// routes.php
Route::get('invoices/{invoice}', [
    'uses' => 'InvoicesController@show',
    'middleware' => 'auth',
]);

// InvoicesController.php
public function show(Invoice $invoice)
{
    // show the invoice
}

// Invoice.php
class Invoice extends Model
{
    protected static function boot()
    {
        static::addGlobalScope(function ($query) {
            $query->where('user_id', auth()->id());
        });
    }
}

We're restricting all queries on the invoice model to just the ones that belong to the current user (this is obviously a bit contrived to keep things simple. For a real-world example, check out Teamwork's excellent UsedByTeams trait).

The problem, as mentioned above, is that auth()->id() will return null when the model is being resolved through the route's model binding, since it runs before the auth middleware.

In Laravel 5.2 there was really no way around this issue. The only remedy was to forgo route model binding and fetch the model yourself:

// InvoicesController.php
public function show($id)
{
    $invoice = Invoice::findOrFail($id);

    // show the invoice
}

Not the end of the world, I know, but once you've been spoiled by route model binding there's really no going back. Laravel's neat little features tend to have that effect on you...

If you've ever run into this issue before, you'll be happy to hear that in Laravel 5.3 this problem is no more. The middleware stack has been rewired so that authentication will now always run before route model binding and you can now safely get the authenticated user in your global scopes. Hallelujah!

Note: as part of this change, session data is no longer available directly in a controller constructor. If you ever run into that, here are some workarounds.

Bonus: the request's expectsJson method

While not directly tied to authentication, I thought it'd be nice to include this little tidbit in here.

As you can see in the first code snippet in this post, determining whether the request needs JSON can be quite tedious. If it's an AJAX request it probably wants JSON, unless it's actually a PJAX request. If it's a regular HTTP request, there may be an Accept header specifying that the response should be JSON.

To make all this easier, there's a new expectsJson method on the request. You can use that directly to determine whether to repond with JSON or not:

public function view(Invoice $invoice)
{
    if (request()->expectsJson()) {
        return $invoice;
    } else {
        return view('invoices.show', compact('invoice'));
    }
}

Just a little tip if you ever have a need for this.


Questions? Comments? Complaints? Ping me on Twitter.

Recent posts

  1. Getting the current user (or other session data) in a Laravel controller constructor

    Session data is not available directly in controller constructors. Let's see how we can work around that.

    Read more ➺

  2. Gate and authorization improvements in Laravel 5.3

    With Laravel 5.2 we got a nice authorization system right out of the box. In 5.3 this got a lot of improvements and refinements. Let's take a look.

    Read more ➺

  3. The new Closure::fromCallable() in PHP 7.1

    Let's take a look at a neat little feature coming in PHP 7.1: easily converting any callable into a proper Closure using the new Closure::fromCallable() method.

    Read more ➺