Skip to main content
Apivalk splits authentication (“who are you?”) from authorization (“are you allowed to do this?”). Five moving parts:

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

  1. Pick or implement an authenticator. Apivalk ships JwtAuthenticator (see how to authenticate with JWT). For anything else, implement AuthenticatorInterface — API key example.
  2. Register the two middlewares in the right order. Authentication before Security.
  3. Register a security scheme on the ApivalkConfiguration. The name you give it (e.g. BearerAuth, ApiKeyAuth) becomes the link between your routes, the middleware and the OpenAPI spec.
  4. Attach RouteAuthorization to 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. No routeAuthorization(...) at all. Guest and authenticated callers both pass.
Any-authenticated route. Pass only the scheme name. Guest → 401; anyone with a valid token → allowed.
Scoped route. Add scopes and/or permissions. All of them must be granted.

Reading the identity in a controller

Optional auth (public route, identity-aware response)

Leave RouteAuthorization off the route. The AuthenticationMiddleware still populates the identity if a valid token is present — you just branch in the controller:
This is how you run a public landing endpoint that shows “Log in” to guests and “Hi, Jane” to authenticated users with the same controller.

Scopes vs permissions

They’re two independent lists on RouteAuthorization. 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.