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.
2. Declare the security scheme in your OpenAPI components
This name is what your routes reference viaRouteAuthorization.
$components when you run the OpenAPI generator — see Generate OpenAPI and docblocks.
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
| Claim | Becomes |
|---|---|
sub | $identity->getSub() |
email | $identity->getEmail() |
username | $identity->getUsername() |
scope or scp (space-separated string or array) | $identity->getScopes() |
permissions, permission, roles, role (string or array) | $identity->getPermissions() |
iss or aud 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
issandaudexactly match what you passed into the constructor (trailing slash onissmatters). 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.