Skip to main content
JwtAuthenticator validates bearer tokens using public keys fetched from a JWKS URL. It checks the standard claims (iss, aud, exp), extracts scopes and permissions, and returns a JwtAuthIdentity.

1. Register the authenticator

The cache is optional but strongly recommended in production — without it every request re-downloads the JWKS. Any CacheInterface works; FilesystemCache is shipped. A configured iss or aud is mandatory: a token that omits the claim is rejected just like one that carries the wrong value. Pass an empty string to skip either check. Per RFC 7519 the aud claim is either a single string or a list of strings, and the token passes as soon as one entry equals the configured audience.

2. Register the security scheme

This name is what your routes reference via RouteAuthorization, at runtime and in the generated document. Register it before you construct Apivalk, otherwise the constructor rejects the routes pointing at it.
The OpenAPI generator reads the schemes from the same configuration — see Generate OpenAPI and docblocks. Add the audiences the scheme accepts as the last argument when tokens of a foreign audience must be rejected:

3. Protect routes

Clients send Authorization: Bearer <jwt>. Anything else reaches your controller as a guest (and gets rejected by SecurityMiddleware if the route requires auth).

What the authenticator reads from the token

A token that carries none of the permission claims gets an empty permission list, a token without aud an empty audience list. A security scheme can require one of those audiences on top of scopes and permissions, see Security Middleware. If iss or aud are missing or don’t match the values you passed into the constructor, or the signature is invalid, or the token is expired, authenticate() returns null and the middleware falls back to GuestAuthIdentity. It never throws.

Reading identity in a controller

SecurityMiddleware already enforced that the token is valid and has the required scopes, so at this point the downcast is safe.

Scope / permission mapping tips

  • Scopes. In Auth0 / Okta / Keycloak, scopes come from the OAuth2 authorization request (scope=openid profile animal). JwtAuthenticator splits on whitespace for string claims and trims each entry.
  • Permissions. Providers vary: Auth0 emits permissions as an array, Keycloak emits realm_access.roles (you’d have to flatten that in a custom authenticator), Okta emits groups. JwtAuthenticator checks permissions, permission, roles, role in that order and merges string + array forms.
  • Hierarchies. AbstractAuthIdentity::isScopeGranted() is a strict in_array. If you need admin to imply everything, subclass JwtAuthIdentity and override isScopeGranted() / isPermissionGranted().

Troubleshooting

  • Always guest, even with a valid token. Confirm the token carries iss and aud, that iss exactly matches what you passed into the constructor (trailing slash matters) and that aud contains the configured audience. Confirm the JWKS URL is reachable from your PHP container — JwtAuthenticator::httpGet() times out after 5 seconds.
  • “Invalid JWKS response”. Your provider returned HTML or an error. Open the URL in a browser; the response must be {"keys": [...]}.
  • Cache poisoning suspicions. JWKS is cached for 3600 seconds per URL. Delete the cached entry (key pattern: jwks_<md5(url)>) or clear the FilesystemCache directory and retry.
  • Signature failures after key rotation. JwtAuthenticator caches keys for an hour; worst case you wait that hour. To force a refresh on demand, clear the same cache entry.

Multiple providers

Run multiple JWKS-backed providers by composing them behind your own authenticator: