With Laravel Sanctum, how do the abilities and ability middleware differ, and why does a first-party SPA request pass every token ability check?
answer
- all versus any
- CheckAbilities and CheckForAnyAbility
- aliases you register yourself
- TransientToken::can() returns true
- MissingAbilityException becomes 403
basics
~20 sabilities (CheckAbilities) requires every listed ability; ability (CheckForAnyAbility) requires one. A session-authenticated SPA user gets a TransientToken whose can() always returns true, so ability checks pass and real permission must come from a policy or gate.
solid answer
~40 sSanctum ships two middleware you alias yourself in `bootstrap/app.php`: `abilities` → `CheckAbilities`, which throws `MissingAbilityException` on the first listed ability the token lacks, and `ability` → `CheckForAnyAbility`, which passes if the token has any one of them. Both first require a user with a current token, otherwise they throw `AuthenticationException` (401 for a JSON request). `MissingAbilityException` extends `AuthorizationException`, so it renders as 403. When the `sanctum` guard authenticates a first-party SPA through the session, it attaches a `TransientToken` whose `can()` always returns `true`. That is deliberate: code can call `tokenCan()` everywhere. But it means abilities only narrow what *tokens* may do; whether *this user* may touch *this booking* must still be checked by a policy or gate.
code
php · 15 lines<?php
use App\Models\Booking;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::middleware(['auth:sanctum', 'abilities:bookings:read,bookings:accept'])
->post('/bookings/{booking}/accept', function (Request $request, Booking $booking) {
// Token scope passed; the SPA's TransientToken would pass it too.
abort_unless($booking->sitter_id === $request->user()->id, 403);
$booking->update(['status' => 'accepted']);
return $booking;
});go deeper
Remember: abilities needs all listed abilities, ability needs any one, and both sit after auth:sanctum.
Explain the 401 versus 403 split and why a session user carries a TransientToken that passes every ability check.
Show that abilities only narrow tokens, and design endpoints that pair an ability check with a per-resource ownership rule.
Decide which permissions belong on tokens at all, so third-party scripts are capped without duplicating your role model in ability strings.
## Two middleware, two semantics Sanctum provides two classes, neither aliased by Laravel out of the box. The docs register them in `bootstrap/app.php`: ```php $middleware->alias([ 'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class, 'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class, ]); ``` | Alias | Class | Passes when | Failure | |---|---|---|---| | `abilities:a,b` | `CheckAbilities` | the token has **all** of `a` and `b` | `MissingAbilityException` naming the first missing ability | | `ability:a,b` | `CheckForAnyAbility` | the token has **at least one** | `MissingAbilityException` listing all of them | Both run **after** `auth:sanctum` and begin with the same guard clause: if there is no user, or the user has no current access token, they throw `Illuminate\Auth\AuthenticationException`, which a JSON request renders as **401**. `MissingAbilityException` extends `Illuminate\Auth\Access\AuthorizationException`, which the exception handler turns into **403**. ## Why the SPA passes every check The `sanctum` guard tries the session guards first. When one of them returns a user whose class uses `HasApiTokens`, the guard calls `$user->withAccessToken(new TransientToken)`. `TransientToken::can()` returns `true` for any ability and `cant()` returns `false`. Consequences: - `currentAccessToken()` is not null, so the ability middleware's 401 guard clause does not fire; - every `tokenCan()` call returns `true`; - `abilities:payouts:read` on a route lets any logged-in SPA user through. The docs call this intentional: you can always call `tokenCan()` in authorization code without asking whether the request came from your own UI or a third-party token. ## What abilities do and do not mean Abilities answer one question: **"was this token granted permission to attempt this kind of action?"** They say nothing about the user. In a pet-sitting marketplace: 1. A sitter's mobile token with `['bookings:read']` should not accept bookings — `ability`/`abilities` or `tokenCan()` stops it. 2. A pet owner logged into the SPA hits `/api/sitters/7/payouts` — every ability check passes, because the SPA's token is transient. 3. So the payout endpoint still needs an ownership rule: the user must be sitter 7. The docs' own example combines both checks: the resource must belong to the user **and** `tokenCan()` must pass. ## Traps worth naming in an interview - Using `auth:web` (not `auth:sanctum`) on a route with `abilities:` — no `TransientToken` is attached, `currentAccessToken()` is null, and the middleware throws 401 for a logged-in user. - A model without `HasApiTokens` authenticated by session gets no `TransientToken` either. - Swapping `abilities` and `ability` silently widens access: `ability:bookings:read,payouts:read` lets a read-only token see payouts. - Treating abilities as roles: a token can only be granted strings you put in `createToken()`; the user's role must be checked separately. ## Choosing between the two middleware Use `abilities` when an endpoint genuinely needs several capabilities at once, for example `abilities:bookings:read,bookings:accept` on an accept action that also returns the booking. Use `ability` when any one of several capabilities is enough, for example a booking list readable by tokens holding either `bookings:read` or `bookings:manage`. The failure mode is asymmetric: 1. picking `abilities` where `ability` was meant only rejects too many requests — a visible bug; 2. picking `ability` where `abilities` was meant silently lets narrow tokens through — a security bug nobody notices. ## Reading the exception in a handler `MissingAbilityException::abilities()` returns the abilities that were missing, which lets an API return a helpful message such as "this token lacks bookings:accept" instead of a bare 403. For `CheckAbilities` that list holds the first missing ability; for `CheckForAnyAbility` it holds every ability the route listed.
- A route uses auth:web and abilities:bookings:read, and a logged-in SPA user gets 401. Why?Only the `sanctum` guard attaches a `TransientToken` to a session user. Under `auth:web` the user has no current access token, so `CheckAbilities` hits its guard clause and throws `AuthenticationException`. Protect the route with `auth:sanctum` instead.
- When would you call tokenCan() in code instead of using the middleware?When the needed ability depends on the loaded resource or the input, for example requiring `payouts:write` only when the amount is changed. `tokenCan()` also composes with an ownership check inside a policy, which a route-level middleware cannot see.
saying these in an interview costs you the question
- The abilities middleware passes if the token has any listed ability
- Sanctum registers the abilities and ability aliases automatically
- A token missing an ability gets a 401 response
- A session-authenticated SPA user fails every ability check
- Checking tokenCan() is enough to authorize access to a resource