In Laravel Passport 13, how do you declare scopes with Passport::tokensCan and enforce them with CheckToken versus CheckTokenForAnyScope?
answer
- declare in AppServiceProvider::boot
- undeclared scopes are rejected
- CheckToken::using() needs all
- CheckTokenForAnyScope needs one
- MissingScopeException, renamed from CheckScopes
basics
~10 sPassport::tokensCan(['shipments:read' => 'View shipments', ...]) declares the scopes clients may request. CheckToken::using(...) requires every listed scope on the access token, CheckTokenForAnyScope::using(...) requires one; a missing scope throws MissingScopeException, rendered as 403.
solid answer
~40 sScopes are declared once, usually in `AppServiceProvider::boot()`, with `Passport::tokensCan()`: an array of scope names mapped to descriptions shown on the consent screen. Passport's scope repository accepts only declared scopes, and it strips the `'*'` wildcard except for the password, personal access and client credentials grants. `Passport::defaultScopes()` sets what a token gets when a client asks for none. On routes, Passport 13 ships `CheckToken` (all listed scopes, formerly `CheckScopes`) and `CheckTokenForAnyScope` (at least one, formerly `CheckForAnyScope`), attached with `::using('shipments:read', 'shipments:write')` after `auth:api`, or via the `#[AuthorizeToken]` controller attribute. A missing scope throws `MissingScopeException`, an `AuthorizationException`, so 403. In code, `$request->user()->tokenCan('shipments:write')` checks the current token.
code
php · 12 lines<?php
use App\Http\Controllers\ShipmentController;
use Illuminate\Support\Facades\Route;
use Laravel\Passport\Http\Middleware\CheckToken;
use Laravel\Passport\Http\Middleware\CheckTokenForAnyScope;
Route::middleware(['auth:api', CheckTokenForAnyScope::using('shipments:read', 'shipments:write')])
->get('/shipments', [ShipmentController::class, 'index']);
Route::middleware(['auth:api', CheckToken::using('shipments:write', 'pickups:book')])
->post('/shipments/{shipment}/pickup', [ShipmentController::class, 'bookPickup']);go deeper
Remember that scopes are declared with Passport::tokensCan and checked with CheckToken for all or CheckTokenForAnyScope for any.
Explain how undeclared and wildcard scopes are filtered at issue time, and why a missing scope is 403 while a bad token is 401.
Design a scope vocabulary integrators can reason about, and pair scope checks with per-record policies so a scope never widens access.
Decide scope granularity as a published contract: too coarse over-grants, too fine burdens every integrator and consent screen.
## What a scope is in Passport In OAuth2 a **scope** limits what a client may do on a user's behalf. A shipping-marketplace integrator on a logistics platform might be allowed to read a shipper's shipments but not to book pickups. In Passport scopes are plain strings that you **declare** and then **check**. ## Declaring scopes ```php Passport::tokensCan([ 'shipments:read' => 'View your shipments and tracking events', 'shipments:write' => 'Create and cancel shipments', 'pickups:book' => 'Book carrier pickups', ]); Passport::defaultScopes(['shipments:read']); ``` - `tokensCan()` replaces the static list of known scopes; the descriptions are handed to your consent view (Passport 13 is headless, so you render it). - `defaultScopes()` supplies scopes when a client requests none. - When a token is issued, Passport's scope repository **drops any scope that was not declared**, strips `'*'` unless the grant is password, personal access or client credentials, and applies any per-client scope restriction. - `Passport::hasScope()`, `scopeIds()`, `scopes()` and `scopesFor()` inspect the declared list. ## Enforcing scopes on routes | Middleware | Passes when | Missing scope | |---|---|---| | `CheckToken::using('a', 'b')` | the token has **all** of `a`, `b` | `MissingScopeException` for the first one missing | | `CheckTokenForAnyScope::using('a', 'b')` | the token has **at least one** | `MissingScopeException` listing all | `::using()` builds the middleware string (`Class:a,b`) so you need no alias. Both classes extend `ValidateToken`: if the request already has a user with a current token (from `auth:api`), they reuse it; otherwise they validate the Bearer token themselves and throw Passport's `AuthenticationException` (401) when it is invalid. `MissingScopeException` extends `AuthorizationException`, which Laravel renders as **403**. On controllers, the `Laravel\Passport\Attributes\AuthorizeToken` attribute wraps the same middleware: `#[AuthorizeToken('shipments:read')]` requires all listed scopes, `anyScope: true` switches to any-of, and `only` / `except` target methods. ## Checking scopes in code `$request->user()->tokenCan('pickups:book')` returns true when the current access token carries that scope or `'*'`; a token with no scopes can do nothing. Use it when the needed scope depends on the input, for example requiring `pickups:book` only when a shipment request also asks for a pickup. ## Passport 13 renames | Before 13 | Passport 13 | |---|---| | `CheckScopes` | `CheckToken` | | `CheckForAnyScope` | `CheckTokenForAnyScope` | | `CheckClientCredentials` | `CheckToken` | | `CheckClientCredentialsForAnyScope` | `CheckTokenForAnyScope` | Old aliases such as `scopes:` and `scope:` pointing at the removed classes must be updated when upgrading. ## Scopes are not the whole authorization story A scope says the **user allowed this client** to attempt an action. It does not say the user may act on this particular shipment. An integrator holding `shipments:write` for shipper A must still be refused shipper B's shipment, which is a policy or gate decision on the user and the model. ## Consent and remembered approvals On the authorization-code flow, Passport's authorization controller parses the requested scopes and passes them, with their descriptions, to your consent view. If the user already approved the same client for the same scopes, the approval screen can be skipped; a client model can also override `skipsAuthorization()` for trusted first-party clients (it returns `false` by default). That is why scope descriptions should be written for **end users**, not developers: they are the only thing a shipper reads before granting access. ## Naming conventions that age well 1. Use `resource:action` names (`shipments:read`, `pickups:book`); they sort and group naturally. 2. Keep read and write separate so integrators can ask for the least they need. 3. Never repurpose a published scope: add a new one and deprecate the old one in your integrator docs, because existing tokens keep the scopes they were issued with. 4. Avoid one "admin" scope; it turns every leaked token into a full compromise.
- A client requests scope=shipments:export, which was never passed to Passport::tokensCan(). What happens?Passport's scope repository only recognises declared scopes, so the OAuth2 server rejects the request with an invalid-scope error instead of issuing a token. Declaring the scope, with a description for the consent view, is what makes it requestable.
- Why can an integrator's user token not be given the '*' scope through the authorization-code flow?When finalising scopes, Passport strips `'*'` for every grant except password, personal access and client credentials. A third-party app acting for a user must name the scopes it needs, and the user approves exactly those on the consent screen.
saying these in an interview costs you the question
- CheckToken passes when the token has any one of the listed scopes
- A token missing a scope gets a 401 response
- Clients can request any scope string without it being declared
- CheckScopes is still the current Passport 13 class name
- A token with an empty scope list can do everything
- Holding a scope proves the user may act on that specific record