The pipeline in one picture
AuthenticationMiddleware is passive — it never rejects the request. That means the same pipeline supports public routes (no RouteAuthorization), optional-auth routes (public but identity-aware), and strict routes all at once.
What you have to wire
- Pick or implement an authenticator. Apivalk ships
JwtAuthenticator(see how to authenticate with JWT). For anything else, implementAuthenticatorInterface— API key example. - Register the two middlewares in the right order. Authentication before Security.
- Register a security scheme on the
ApivalkConfiguration. Thenameyou give it (e.g.BearerAuth,ApiKeyAuth) becomes the link between your routes, the middleware and the OpenAPI spec. - Attach
RouteAuthorizationto each protected route.
Bootstrap
Route-level declaration
'BearerAuth' must match the name of a SecuritySchemeObject registered with $configuration->addSecurityScheme(...), otherwise new Apivalk($configuration) throws — see OpenAPI Generator / Security Schemes.
What SecurityMiddleware does with this
- A guest on a route with a
RouteAuthorization→401 Unauthorized. Passing no requirements (just the scheme name) therefore means “any authenticated user”. - The scheme lists audiences and the identity carries none of them →
401 Unauthorized. - An authenticated identity missing a required scope or permission →
403 Forbidden.
The three useful route shapes
Public route. NorouteAuthorization(...) at all. Guest and authenticated callers both pass.
Reading the identity in a controller
Optional auth (public route, identity-aware response)
LeaveRouteAuthorization off the route. The AuthenticationMiddleware still populates the identity if a valid token is present — you just branch in the controller:
Scopes vs permissions
They’re two independent lists onRouteAuthorization. Both must be satisfied. How you split them is a policy choice — a common convention:
- Scopes model who is allowed in this area (
admin,animal,billing). Typically tied to an OAuth2 scope emitted at token-issuance time. - Permissions model fine-grained actions (
animal:read,animal:create). Typically derived from the user’s roles on your side.
AbstractAuthIdentity::isScopeGranted() / isPermissionGranted() is simple in_array — override if you need hierarchy (e.g. admin implies everything).
Custom identity classes
JwtAuthIdentity is just the built-in that ships with JwtAuthenticator. Nothing stops you from defining your own:
AbstractAuthIdentity already implements getAud() on top of its protected $aud property, which defaults to []. Set $this->aud in your constructor when the credential is bound to audiences; leave it alone when the concept doesn’t apply, but note that such an identity is rejected on every route whose scheme lists audiences. Never redeclare $aud as private in the subclass — that is a fatal error.
Return it from your authenticator and downcast with PHPStan-friendly narrowing in the controllers that care.
Related how-tos
- Authenticate with JWT (JWKS)
- Authenticate with a custom API key header
- Write a custom middleware — useful if you need session-based auth, mTLS, or anything that isn’t a bearer token.