Understanding the order of authorization and model binding

And why you shouldn't just flip the order

Joel Clermont
Joel Clermont
2024-03-22

We really like the laravel-permissions package and use it on most projects.

One nice thing is that it lets you use the existing Laravel can middleware to check if a user has a certain permission.

In a routes file, we can wrap a whole group of routes in a permission-based middleware check like this:

// routes/web.php

Route::middleware('can:manage-articles')->group(function () {
    Route::get('/articles', 'ArticleController@index');
    Route::get('/articles/{article}', 'ArticleController@show');
    // more article routes...
});

Normally, we'd have a Permission enum and use that instead of a string, but I'm keeping the example simple and easier to read.

In this example, if the user doesn't have access to the manage-articles permission, they shouldn't be able to access any of these routes. And we certainly don't need to do any more granular checks about "can they edit this specific Article model". We just deny early.

But something potentially unexpected happens if a user with no permission to this group hits a route like /articles/123, where 123 is a non-existent article. They'll get a 404 error instead of the expected 403 error, because of route model binding.

By default, Laravel assigns the SubstituteBindings middleware a higher priority than the Authorize middleware. So if a model is not found, it will return a 404 before the Authorize middleware even has a chance to return a 403.

Before talking about a solution, why is it ordered this way? Well, consider a more traditional policy-based authorization check. Some of those methods require a bound model in order to run.

For example, a typical edit method in a policy might look like this:

public function edit(User $user, Article $article)
{
    return $user->id === $article->user_id;
}

So if the model isn't bound, the policy method can't run. And if the policy method can't run, the Authorize middleware can't do its job. So it makes sense for SubstituteBindings to run first.

In our particular example, we don't need a bound model, but if we just flip the order of the middleware, we will break other parts of our authorization logic in policies that do rely on a bound model.

So what's the solution? In this case, we can use the package-specific PermissionMiddleware instead, and we can also make sure it has a higher priority than the SubstituteBindings middleware.

Hope this helps,

Joel

P.S. If you like diving deep on a topic and understanding how Laravel really works, check out Mastering Laravel Validation Rules.

Toss a coin in the jar if you found this helpful.
Want a tip like this in your inbox every weekday? Sign up below 👇🏼

Level up your Laravel skills!

Each 2-minute email has real-world advice you can use.