Authentication
Docuccino detects how your routes are protected from their middleware and documents the appropriate
security schemes and requirements. There are three ways an operation ends up with security, and they
compose in a fixed order:
| Source | Layer | When it applies |
|---|---|---|
| A package integration (Sanctum, Passport) | integration | Its package is installed, the route’s middleware matches, and you declared no security.schemes of your own. |
Your security config |
integration / config | You declared schemes — which also stands the integrations down — plus a default requirement for auth-detected routes, or a document-wide one. |
An attribute (#[Security], #[OptionallyAuthenticated], #[Unauthenticated]) |
attribute | Always wins over both, per operation. |
Routes marked #[Unauthenticated] are documented as
public (security: []) whatever the middleware says.
Scheme names
Section titled “Scheme names”Whichever route documents them, schemes are registered in components.securitySchemes under stable
names — the names an operation’s security requirement and your own #[Security] attributes
reference:
| Name | Type | Registered by |
|---|---|---|
sanctumToken |
http / bearer |
Sanctum, on a route with a token guard |
sanctumStateful |
apiKey in cookie |
Sanctum, on a route with the stateful-frontend middleware and an auth guard |
passport |
oauth2 |
Passport, on a Passport-protected route |
| whatever you called it | anything | Your security.schemes config |
Sanctum
Section titled “Sanctum”Activates when Laravel Sanctum is installed. Docuccino documents both of Sanctum’s modes based on the middleware a route uses:
- Token → an HTTP bearer scheme (
sanctumToken). Detected fromauth:sanctum, the baresanctumalias, anabilities:/ability:middleware, or anyauth:<guard>whose configured driver issanctum— so a customauth:mobileguard on the Sanctum driver is recognized, and multi-guard lists (auth:web,sanctum) work. - Stateful SPA → a cookie-based scheme (
sanctumStateful), documented whenEnsureFrontendRequestsAreStatefulis present alongside an auth guard. BecausestatefulApi()prepends that middleware to the wholeapigroup, requiring the guard too means public routes (login, register) are left unsecured rather than falsely cookie-protected. OpenAPI can’t model the CSRF handshake structurally, so the scheme’s description explains theX-XSRF-TOKENflow.
A route that supports both — the common Sanctum reality — gets an OR-list: either credential satisfies it. The middleware alone is enough; you write no security config:
Route::get('/invoices', [InvoiceController::class, 'index']) ->middleware('auth:sanctum');"components": { "securitySchemes": { "sanctumToken": { "type": "http", "scheme": "bearer", "description": "Sanctum API token authentication: send a personal access / API token as a `Bearer` token in the `Authorization` header." } }}// On the operation. With statefulApi() in play the route supports both modes, so this becomes// [ { "sanctumToken": [] }, { "sanctumStateful": [] } ] — either one satisfies it."security": [ { "sanctumToken": [] } ]Tune which schemes are exposed:
// config/docuccino.php → documents.default.integrations'sanctum' => [ 'modes' => ['token', 'stateful'], // which schemes to expose (default: both) 'cookie' => 'myapp_session', // stateful cookie name (default: your session.cookie)],modes is intersected with what each route actually supports, so it narrows rather than invents —
which makes it a clean audience switch across multiple documents:
a public document can list only token while an internal one lists both.
Token abilities
Section titled “Token abilities”Sanctum’s abilities: (all-of) and ability: (any-of) middleware — and the deprecated
CheckScopes / CheckForAnyScope class forms — describe which token abilities a route needs. Since
the sanctumToken scheme is an HTTP bearer token, OpenAPI can’t carry abilities as scopes, so
Docuccino surfaces them as an x-abilities extension member and appends a requirement line to the
operation’s description (“Requires token abilities: invoices:create, invoices:send”). Abilities checked
in the action body ($request->user()->tokenCan(…)) can be declared with
#[Abilities], which is an all-of requirement:
#[Abilities('invoices:create', 'invoices:send')]public function store(StoreInvoiceRequest $request): InvoiceResource { /* … */ }"/api/invoices": { "post": { "x-abilities": [ { "match": "all", "abilities": ["invoices:create", "invoices:send"] } ]} }Passport
Section titled “Passport”Activates when Laravel Passport is installed. A route is
documented with the passport OAuth2 scheme when it carries scope: / scopes: or client-credentials
middleware, or when one of its auth:<guard> middleware resolves to a passport-driver
guard. Detection is driver-based, from config('auth.guards') — so a custom-named guard
(auth:partner on the passport driver) is recognized, a guard on a token/sanctum driver is not
mistaken for Passport, and multi-guard lists (auth:web,api) work.
The authorization-code and client-credentials flows are always emitted; the password and implicit flows
appear only when the app opted into them (Passport::enablePasswordGrant() /
enableImplicitGrant()) — so the docs never advertise a grant the server rejects. The flow endpoints
honor config('passport.path'), and the flow scope map is your real scope catalog from
Passport::tokensCan(). Any scope a route references that the catalog is missing is added (described by
its own id) so the document stays valid; an app that never called tokensCan() gets a single * scope.
Per-operation scopes are read from the middleware and modeled by their Laravel semantics:
scopes: (CheckScopes) requires all listed scopes (one requirement), while scope:
(CheckForAnyScope) requires any one of them, emitted as an OR-list of requirements. The same
applies to machine-to-machine routes: client / CheckClientCredentials (all-of) and
CheckClientCredentialsForAnyScope (any-of) are documented against the client-credentials flow, and
a bare client with no scope is still marked protected.
// config/docuccino.php → documents.default.integrations'passport' => [ 'url' => 'https://auth.example.com', // OAuth2 flow base URL (default: your app URL)],Declaring security yourself
Section titled “Declaring security yourself”Some apps don’t use Sanctum or Passport — a hand-rolled JWT guard, an API key header, an external
identity provider. Declare the schemes in config and Docuccino publishes them verbatim, in full OpenAPI
breadth (http bearer/basic, apiKey in header/query/cookie, oauth2 with any flows,
openIdConnect):
// config/docuccino.php → documents.default'security' => [ // Routes whose middleware matches this wildcard get the `default` requirement below. 'auto_detect_middleware' => 'auth*',
'schemes' => [ 'bearer' => ['type' => 'http', 'scheme' => 'bearer', 'bearerFormat' => 'JWT'], 'apiKey' => ['type' => 'apiKey', 'in' => 'header', 'name' => 'X-API-Key'], ],
// The per-operation requirement applied to routes matched by auto_detect_middleware. 'default' => [['bearer' => []]],],Two things worth knowing:
auto_detect_middlewareon its own does nothing visible — it decides which routes are “protected”, anddefaultdecides what requirement they carry. Set both. The pattern is a wildcard matched against each route’s middleware strings, so the shippedauth*coversauth,auth:web,auth:sanctumand friends.- Declaring any
schemesstands the Sanctum and Passport integrations down entirely — explicit config wins, so you never get a surprise second scheme alongside your own.
Use security.document instead of default for a requirement that applies to the whole document; see
the configuration reference for the full key list.
Declaring security in code
Section titled “Declaring security in code”Middleware detection covers the common cases, but some requirements live where static analysis can’t
see them — a Gate or policy, or a $request->user()?->tokenCan(…) check in the action body. Declare
those with attributes; they apply at the attribute precedence layer, so they win over anything
inferred from middleware.
#[Security(scheme, scopes)]names a requirement against a registered scheme (fromsecurity.schemesconfig or an integration). Several scopes in one attribute are an all-of; the attribute is repeatable, so stacking it models an OR-list where any one alternative satisfies the operation.#[OptionallyAuthenticated]marks an endpoint usable anonymously or authenticated, emittingsecurity: [{}, …]— the empty requirement followed by whatever was declared or inferred.
// Either an OAuth2 token with reports.read, or an API key:#[Security('oauth2', ['reports.read'])]#[Security('apiKey')]public function reports(): JsonResponse { /* … */ }
// Route is behind `auth:sanctum`, so `sanctumToken` is inferred; #[OptionallyAuthenticated]// adds the anonymous alternative — works signed-out, richer response with a token:#[OptionallyAuthenticated]public function feed(): JsonResponse { /* … */ }"/api/reports": { "get": { "security": [ { "oauth2": ["reports.read"] }, { "apiKey": [] }] } }"/api/feed": { "get": { "security": [ {}, { "sanctumToken": [] } ] } }Authorization: roles & permissions
Section titled “Authorization: roles & permissions”Authenticating a request is one thing; authorizing it — the role: / permission: middleware from
spatie/laravel-permission — is documented by a separate, opt-in integration, because permission
names expose your internal authorization taxonomy. See
Spatie Laravel Permission.
Configuration
Section titled “Configuration”| Integration | Option | Default | Effect |
|---|---|---|---|
| any | enabled |
true |
Turn the integration on/off for this document. |
sanctum |
modes |
both | Which Sanctum schemes to expose, intersected with each route’s real support. |
sanctum |
cookie |
your session.cookie |
Stateful cookie name. |
passport |
url |
your app.url |
OAuth2 flow base URL. |
Each integration is a no-op when its package isn’t installed, or when a route has no matching middleware. Sanctum and Passport stay on by default — token schemes and OAuth scopes are the public contract, so they leak nothing internal.
Implicit 401
Section titled “Implicit 401”When a route sits behind authentication middleware (matching security.auto_detect_middleware),
Docuccino also documents the 401 response an unauthenticated request receives — no annotation
needed. Mark a route #[Unauthenticated] to opt out. See
implicit responses.