Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

xoctopus's avatar

How to check authorization before loading Route Model Binding

I ask this question after doing various searches without finding any clarifying information that helps me solve the problem that I am presenting.

I have created a controller with the following command:

php artisan make:controller UserController --api -m User -r -R

Which creates the controller and the FormRequest classes. Inside the controller, let's just focus on the 'update' method, since that's the one I'm having some trouble with.

<?php

namespace App\Http\Controllers;

use App\Http\Requests\StoreUserRequest;
use App\Http\Requests\UpdateUserRequest;
use App\Models\User;

class UserController extends Controller
{
    /**
     * Update the specified resource in storage.
     *
     * @param  \App\Http\Requests\UpdateUserRequest  $request
     * @param  \App\Models\User  $user
     * @return \Illuminate\Http\Response
     */
    public function update(UpdateUserRequest $request, User $user)
    {
        //
    }

    ...

As you can see from the method arguments, this method uses what we call Route Model Binding

After this, I define my route in the routes file 'api.php' as follows:

<?php

use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Route;

Route::middleware('auth:sanctum')->group(function () {

    Route::apiResource('user', UserController::class);

});

Which registers all the routes and within them the 'update' route.

In the FormRequest class named 'UpdateUserRequest', which uses the 'update' method in the previously created controller, I define the 'authorize' method to return false on all checks just for testing. The class would look similar to this:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class UpdateUserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return false;
    }
    
    ...

Now, the problem that I am presenting, when I access the route:

http://127.0.0.1:8000/api/user/100?

Using Postman, I make a request to that route with the PUT method and Laravel returns the following error:

{
    "message": "No query results for model [App\Models\User] 100"
}

This is because I don't have a user with 'id' 100, I only have 2 users in my database for testing purposes.

My question is this: Isn't Laravel supposed to return an error on this request? Telling me that the action is not allowed, since in the FormRequest 'UpdateUserRequest' class, in the 'authorize' method, I always return false.

I think Laravel is loading the middleware \Illuminate\Routing\Middleware\SubstituteBindings::class before middleware \Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class.

I know that in the app/Http/Kernel.php file I can modify the middleware priority, overriding the $middlewarePriority property, but I've tried it and I don't get the expected result.

Because from my perspective, it doesn't make much sense, taking the previous example that a user who doesn't have permissions to update the model User tries to access the route and Laravel returns an error saying that the user trying to modify with that id exists or not, without first verifying that the user trying to perform the (update) action has or does not have permissions to perform it.

0 likes
22 replies
Nakov's avatar

You are saying this:

without first verifying that the user trying to perform the (update) action has or does not have permissions to perform it

That does not make sense to me, because the "user" does not exists. In real world your authorize method won't just return false it will perform a check on a User instance, which actually does not exists in your database, so the middleware priority is in natural order IMO.

click's avatar

I agree with Nakov, this is the expected behavior for the way you programmed it. You define your controller to resolve the User model for you. If that model cannot be resolved based on your current route Laravel automatically throws a 404 error. However, you can change the "Model not found" behavior though, see https://laravel.com/docs/9.x/routing#customizing-missing-model-behavior

xoctopus's avatar

@nakov @click Thanks for your quick response Well maybe I didn't explain myself well, let's suppose this environment, I have an application where there are users, roles and permissions:

You are authenticated with a user in my application, I have assigned you the role of User. Said role only allows basic actions in the application, let's say reading articles.

I, on the other hand, have my own user, my role in the application is Administrator. This role allows me to update the properties of other users, as well as the information of different articles.

There are also other users in the applications, most of them have the User role, just like you.

Now suppose that you, for some strange reason, want to know how many users exist in my application, or if a certain user exists with a certain id. Or you try to update a user, without having the necessary permissions to do so, so you, who have user id 1, make the following request to modify the name of the User with id 100.

PUT http://127.0.0.1:8000/api/user/100?name=User1

So Laravel, returns the error saying: 'Hey you, the user with id 100 does not exist'

When really Laravel should answer you: 'Hey you, this action is not allowed for you'

Why? Well, because from my perspective the request should be handled in the following order:

- Does the user have permission to update other users, hmm, let me check it through the 'authorize' method in the FormRequest?
    - No: 
        - Return error, action not allowed
    - Yes: Check if the user with id 100 exists:
       - Does not exist:
             - Return: No query results for model [App\Models\User] 100
       - Exists:
             - Return: OK response code 200

I appreciate any help.

click's avatar

What you want does not work out of the box like that, however you can build however you want it. You can create your own model binding resolve logic in your models. You could think of returning an empty model instead of throwing a 404 error.

Take a look at authorization policies and the route model binding in the documentation.

In general you should not expose your primary key in your url's that is why slugs or hash id's are invented so you get an url like: /users/1LLb3b4ck or with uuid's

martinbean's avatar

@xoctopus How are you intending to authorise access to models before you’ve actually retrieved the model you want to test permissions for? You can’t check if a user has access to say, a post if you haven’t retrieved the post first. Therefore, yes, route-model binding should happen before any authorisation.

I think you’re conflating two problems here. If you don’t want people to hit URLs by passing sequential IDs to find out how many X models you have, then don’t use primary keys in URLs. Use some other column value for routing, like a UUID or something like a Hashid to stop exposing your database primary keys in URLs, and to stop a user from just iterating over them until they get a 404.

You should also be authenticating users if you’re running authorisation logic, and users should be rate-limited so they can’t just programmatically spin through your API or whatever to harvest content or data.

xoctopus's avatar

Hi @martinbean , thanks for ur time. I really appreciate your help.

How are you intending to authorise access to models before you’ve actually retrieved the model you want to test permissions for? You can’t check if a user has access to say, a post if you haven’t retrieved the post first. Therefore, yes, route-model binding should happen before any authorisation.

The example I described above was an abstract example. Let's discard the previous example, suppose now that instead of roles and permissions, I need to check if the source IP from where the request is made has permission to update the user. Suppose I have the validation mechanism in the 'authorize' method. I'm not loading anything from the DB to perform validation, I'm not loading a user model to first check whether or not it can perform such an action. I just need to check the source IP.

Still, the \Illuminate\Routing\Middleware\SubstituteBindings::class middleware will be executed before the \Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class middleware and this is not what I expected.

I think you’re conflating two problems here. If you don’t want people to hit URLs by passing sequential IDs to find out how many X models you have, then don’t use primary keys in URLs. Use some other column value for routing, like a UUID or something like a Hashid to stop leaking your database primary keys, and to stop a user from just iterating over them until they get a 404.

I think that is quite a lot of behavior to change in the application when I just need to change a priority order in the middleware.

You should also be authenticating users if you’re running authorisation logic, and users should be rate-limited so they can’t just programmatically spin through your API or whatever to harvest content or data.

This is a very good mechanism to mitigate brute force attacks. I appreciate your advice.

In any case, it seems my doubt is something that has already been discussed before.

https://stackoverflow.com/questions/65111155/laravel-sanctum-token-authentication-runs-after-model-binding

https://github.com/laravel/framework/issues/6118

click's avatar

@xoctopus fyi, the sanctum link you are referring to is not related to your issue. That issue is related to authentication and route model binding, not Authorization and route model binding. Authentication and authorization are two different things.

xoctopus's avatar

@click I have shared the first link, because from there it was that I found the second link, I only put it as a reference of a thread. Thank you

martinbean's avatar

I need to check if the source IP from where the request is made has permission to update the user. Suppose I have the validation mechanism in the 'authorize' method

@xoctopus Doesn’t matter. You still need to know the user trying to be updated before you can then perform authorisation logic. If say, the administration panel should be accessed from specific IP addresses then do that check in a middleware:

Route::middleware('admin_ip')->prefix('admin')->group(function () {
    // Admin routes here, i.e. Route::resource('users', UserController::class);
});
Snapey's avatar

Same comment as on Stack Overflow;

Your authorisation might depend on the model. For instance, the user might be allowed to update records within their own team, but not within another team (for instance). Your approach of finding the model after checking authorisation would not allow this

1 like
click's avatar

I don't think this is the best approach, but if you implement it consistently this might work?

You get the error you see because that is simply the default behavior of Route Model Binding, if you don't want that behavior you have two options I think:

  1. Do not use route model binding
  2. Do not let route model binding throw an exception for you.

Option 2 can be done like this:

# Your Model Class
    public function resolveRouteBinding($value, $field = null)
    {
        return parent::resolveRouteBinding($value, $field) ?? new static();
    }
# Your Controller
public function update(YourModel $yourModel)
{
    // exists will be false (or id will be null) if the model was not found. 
    dd($yourModel->exists); 
}

This simply returns an empty model in case it can't be found. Laravel will reach your update() method and you have the option to do your own authorization. If you have a consistent implementation and always take into account your route model binding can be an empty model I think this approach could work.

However, I would not be in favor of such an approach as it is a bit unexpected behavior. I would go for a simpler approach, accept the workflow of Laravel and do not use your primary keys in routes.

Nakov's avatar

@xoctopus I was out of the conversation, but from what you initially responded with

Does the user have permission to update other users, hmm, let me check it through the 'authorize' method in the FormRequest? - No:

You should then protect your route behind a middleware that will check what the currently logged in user has access to and allow that route to be hit at all or just deny access.

1 like
xoctopus's avatar

@Nakov Hi, sorry for not replying sooner, I've been away. Could you show me an example of how you would do it? Thank you very much

Nakov's avatar

@xoctopus So with a middleware like this: https://laravel.com/docs/9.x/middleware#assigning-middleware-to-routes

Route::get('/profile', function () {
    //
})->middleware('yourcustommiddleware');

or group them the routes if you need to apply the same middleware to multiple routes.

And the content of the middleware will be:

public function handle($request, Closure $next)
{
	if (! $user = $request->user())
	{
		abort(403);
	}

	// here hasRole is something that you need to add, however you check for the user's role. 
	if (! $user->hasRole('admin')) {
		abort(403);
	}
 
	// the user can update other users, so now hit the controller and the update request.
	return $next($request);
}

hope this makes sense.

xoctopus's avatar

@Nakov I sincerely appreciate your help. I have sent you a request by discord, could you review? I want to show you something related to this question and if I send you a screenshot it would be much more comfortable for me to show you.

xoctopus's avatar

@Nakov Hello, I really apologize for the time that I may be occupying you. I have shared a small video to show you the problem I want to solve. Basically Laravel is ignoring the middleware to check authorizations when I access a route using the Route Model Binding with the id of a nonexistent model. Please, if you would be so kind as to take a look at it... Thank you very much in advance. https://drive.google.com/file/d/15kc8zOdZK-pwwKu6eUUInO54poazprhP/view?usp=sharing

martinbean's avatar

@xoctopus Stop badgering people on Discord. You DM’d me, I told you to post your question in the #help channel but you didn’t.

Stop expecting people to give you free, one-to-one support.

Nakov's avatar
Nakov
Best Answer
Level 73

@xoctopus you didn't even applied any middleware other than sanctum for authentication:

image

so check out my link to the documentation and create a middleware and put the handle logic I applied above.

1 like
xoctopus's avatar

@martinbean First off, I'm sorry you were annoyed by the simple message I sent you on Discord saying, "Hey, do you think you can help me with a problem I'm having?"

Honestly, I didn't think a simple message like that would offend or upset you, however, I apologize if for any reason it upset you, I really didn't mean to.

Unfortunately, I have been searching for information for many hours and have posted my question on different sites (including the general Discord channel) and have not found a solution for the problem I am trying to solve.

So, as a last resort, I tried to ask for help from people with respectable profiles in the community (like yours).

I don't expect to have free VIP support anyway. I have only asked a question in the hope that someone, provided they have their time and allow it, can help me. Selflessly giving help or sharing knowledge is not anyone's obligation.

Again, my sincerest apologies, I didn't mean to bother you with a simple message asking for help.

nachopitt's avatar

Hey,

I'm sorry that I'm arriving very late to the party.

I'm also facing this "behavior" right now and the only thing I can say about it is that it is by desing: authorization, either by the use of gates and/or policies, will always takes place AFTER implicit model binding resolution because of the reasons that have already been explained. When using gates I don't think so, but if policies are used to perform the action authorization then it makes sense to do first the implicit model binding resoluction and then the authorization part because you need to know in advance the model you're going to authorize your actions against with.

As an exercise, I'm pretty sure that if you store previously in the database that user with an ID of 100 the error response will change from "404 - NOT FOUND" to something like "403 - THIS ACTION IS UNAUTHORIZED."

At first I thought it was an issue, but after reading this thread and putting more thinking into it, I realized that this behavior is "by design" because there are cases where authorization needs to know in advance the model or "resource" the application to perform the authorization routines, for example when using Policies.

Please or to participate in this conversation.