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
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 viaRouteAuthorization, at runtime and in the generated document. Register it before you construct Apivalk, otherwise the constructor rejects the routes pointing at it.
3. Protect routes
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).JwtAuthenticatorsplits on whitespace for string claims and trims each entry. - Permissions. Providers vary: Auth0 emits
permissionsas an array, Keycloak emitsrealm_access.roles(you’d have to flatten that in a custom authenticator), Okta emitsgroups.JwtAuthenticatorcheckspermissions,permission,roles,rolein that order and merges string + array forms. - Hierarchies.
AbstractAuthIdentity::isScopeGranted()is a strictin_array. If you needadminto imply everything, subclassJwtAuthIdentityand overrideisScopeGranted()/isPermissionGranted().
Troubleshooting
- Always guest, even with a valid token. Confirm the token carries
issandaud, thatissexactly matches what you passed into the constructor (trailing slash matters) and thataudcontains 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 theFilesystemCachedirectory and retry. - Signature failures after key rotation.
JwtAuthenticatorcaches keys for an hour; worst case you wait that hour. To force a refresh on demand, clear the same cache entry.