| Piece | Role |
|---|---|
AuthenticatorInterface | Turns a raw token into an identity (or null). You choose / implement this. |
AbstractAuthIdentity | What a token decodes to: scopes, permissions, plus any fields you need (sub, email, …). |
GuestAuthIdentity | Default identity when no token is provided. isAuthenticated() === false. |
RouteAuthorization | Per-route policy: security scheme name + required scopes + required permissions. |
AuthenticationMiddleware / SecurityMiddleware | Runtime pipeline. First attaches identity to the request, second enforces RouteAuthorization. |
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.
- Declare a security scheme in
ComponentsObject. Thenameyou give it (e.g.BearerAuth,ApiKeyAuth) becomes the link between your routes and the OpenAPI spec. - Attach
RouteAuthorizationto each protected route.
Bootstrap
Route-level declaration
'BearerAuth' must match the name of a SecuritySchemeObject in your ComponentsObject — see OpenAPI Generator / Security Schemes.
What SecurityMiddleware does with this
For every required scope / permission:- If the identity has it → keep going.
- If the identity is authenticated but missing the requirement →
403 Forbidden. - If the identity is a guest →
401 Unauthorized.
401 Unauthorized. Passing no requirements (just the scheme name) means “any authenticated user”.
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:
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.