Skip to content

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.

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

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 from auth:sanctum, the bare sanctum alias, an abilities: / ability: middleware, or any auth:<guard> whose configured driver is sanctum — so a custom auth:mobile guard on the Sanctum driver is recognized, and multi-guard lists (auth:web,sanctum) work.
  • Stateful SPA → a cookie-based scheme (sanctumStateful), documented when EnsureFrontendRequestsAreStateful is present alongside an auth guard. Because statefulApi() prepends that middleware to the whole api group, 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 the X-XSRF-TOKEN flow.

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:

routes/api.php
Route::get('/invoices', [InvoiceController::class, 'index'])
->middleware('auth:sanctum');

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.

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"] } ]
} }

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)
],

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_middleware on its own does nothing visible — it decides which routes are “protected”, and default decides what requirement they carry. Set both. The pattern is a wildcard matched against each route’s middleware strings, so the shipped auth* covers auth, auth:web, auth:sanctum and friends.
  • Declaring any schemes stands 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.

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 (from security.schemes config 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, emitting security: [{}, …] — 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 { /* … */ }

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.

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.

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.