I’m working on a Laravel 11 app, and using the API tokens functionality of Sanctum (no SPA, no session, no CSRF etc, REST API and tokens, routes in api.php
not web.php
). All is working well – I can issue tokens and use them to access routes protected by auth:sanctum
middleware. I’m now adding abilities to the tokens, and using the abilities middleware to control access to some routes.
I’ve added the required middleware aliases in bootstrap/app.php
:
$middleware->alias([
...
'abilities' => LaravelSanctumHttpMiddlewareCheckAbilities::class,
'ability' => LaravelSanctumHttpMiddlewareCheckForAnyAbility::class,
]);
And added an abilities check in a route in routes/api.php
:
Route::middleware(['auth:sanctum', 'abilities:foo'])->group(function () {
Route::get('/foo', [FooController::class, 'foo']);
});
Now I generate a token without any abilities:
$token_without_abilities = $user->createToken('name', [], Carbon::now()->addMinutes(10))->plainTextToken;
When I try to access the /foo
route described above, using that token which does not include the required foo
ability, I expect it to fail, and sure enough an exception is thrown. According to the source, in LaravelSanctumHttpMiddlewareCheckAbilities
, MissingAbilityException
should be thrown. I added some debug logging and verified that indeed is what seems to be happening, I reach the constructor of MissingAbilityException
, and I am able to log the missing ability as $ability
at that point.
I want to catch that exception and return a JSON response with the right error msg, so I added the following in my bootstrap/app.php
:
use LaravelSanctumExceptionsMissingAbilityException;
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (MissingAbilityException $exception, Request $request) {
Log::debug('MissingAbilityException exception: ' . printr_r($exception->abilities(), true);
return response()->json([
'message' => 'Missing ability ' . $exception->getMessage(),
]);
});
});
But the exception is not caught, instead a AccessDeniedHttpException
is thrown, and I end up with:
{
"message": "Invalid ability provided.",
"exception": "Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException",
"file": "/var/www/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php",
"line": 633,
"trace": [
{
"file": "/var/www/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php",
"line": 577,
"function": "prepareException",
"class": "Illuminate\Foundation\Exceptions\Handler",
"type": "->"
},
{
"file": "/var/www/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php",
"line": 51,
"function": "render",
"class": "Illuminate\Foundation\Exceptions\Handler",
"type": "->"
},
...
Edit: linked to wrong stack trace, fixed. Full stack trace.
The only hint I can find about this is this answer to a Github issue asking more or less the same as me. That answer suggests what I’m already doing, AFAICT, though in pre-Laravel 11 syntax.
If I try to catch AccessDeniedHttpException
in bootstrap/app.php
the same way, it does work, but I can’t seem to access the missing ability any more, $exception->abilities();
gives me a Call to undefined method
.
How can I catch a MissingAbilityException
and access the actual missing ability? The goal is to return a JSON response to the client with a message like You don't have the "foo" permission
.
2
Answers
To catch the MissingAbilityException and access the associated missing ability in a Laravel 11 application using Sanctum, I think you’ll need to customize the exception handling in your application correctly. The AccessDeniedHttpException is the one being thrown when the ability check fails, which is why your catch block is not being triggered as expected.
Try Follow this:
Catch the Exception at the Middleware Level: You should catch the MissingAbilityException directly in the middleware where the abilities are being checked. Since you already have the middleware setup, you can extend the middleware to handle the exception and return the desired JSON response.
Create a Custom Middleware: Instead of using the default CheckAbilities middleware, you can create your own middleware to wrap the functionality. Here’s an example:
// app/Http/Middleware/CustomCheckAbilities.php
namespace AppHttpMiddleware;
use Closure;
use IlluminateHttpRequest;
use LaravelSanctumExceptionsMissingAbilityException;
use SymfonyComponentHttpKernelExceptionAccessDeniedHttpException;
Register Your Middleware: Register your custom middleware in
app/Http/Kernel.php:
];
Use Your Custom Middleware in Routes: Now, you can use your custom middleware in your routes:
});
Test Your Implementation: When you try to access the /foo route with a token that doesn’t have the "foo" ability, it should now return a JSON response indicating the missing ability:
{
"message": "You don’t have the ‘foo’ permission."
}
Using a custom middleware that extends the functionality of the existing CheckAbilities middleware, you gain access to the MissingAbilityException and can handle it appropriately, providing a clear response to the client.
the MissingAbilityException is passed to the AccessDeniedHttpException, this one is inherits from some other Exception classes and more importantly from the RuntimeException class. The MissingAbilityException is passed up all the levels until it reaches the RuntimeException and you can (should, as i have not tested myself) access it with the getPrevious() method.
So you would still catch the AccessDeniedHttpException $e, but you can access the MissingAbilityException by doing $e->getPrevious();