v2.2.0
Two features for keeping refresh tokens under control, one for documenting them, and a fix that made the recommended configuration work as written.
See UPGRADE-2.2.md before upgrading. One configuration is now rejected; everything else is additive or a correction.
An expired JWT can be exchanged
With jwt and refresh_jwt on the same firewall, the JWT authenticator was reached first and rejected the expired token before the refresh authenticator saw it — so the configuration in the README did not work as written, and the advice given for years, to reorder them on the firewall, could never have helped. Symfony orders authenticators by the priority each factory declares, not by the file. This one now sits above Lexik's.
Nothing to change. Applications that split the refresh endpoint into its own firewall to get around it can collapse it back.
Storing hashes instead of tokens
gesdinet_jwt_refresh_token:
hash_tokens:
enabled: trueA refresh token gets its holder back into an account without a password, so a copy of the table was a copy of everybody's credentials, next to the passwords that are hashed for exactly that reason. What is stored is now sha256$ and the hash, and a leaked table cannot be used.
Off by default, and turning it on signs nobody out: tokens already stored are taken as they are and rewritten hashed the first time they are used. getRefreshToken() then returns the stored hash, which is worth reading about in the upgrade guide first.
A limit on sessions per user
gesdinet_jwt_refresh_token:
max_tokens_per_user: 5Every login stores a token, deliberately, so that a user's devices are separate sessions. Nothing bounded how many accumulated. Signing in beyond the limit revokes the session that has gone longest without being refreshed, expired ones first.
Documented in API Platform
gesdinet_jwt_refresh_token:
api_platform:
enabled: trueLexik documents the login endpoint, but its response schema only carries the JWT, because the refresh token beside it is added by this bundle. The refresh endpoint was documented by nobody, being a firewall authenticator rather than a controller. Both are covered now, from the bundle's own configuration — so with the cookie replacing the body, no refresh_token field is promised that never arrives.
Also
- A DBAL backend (
dbal_connection) storing tokens through a plain connection, with no object manager and no unit of work. refresh_token_managernaming a manager of your own, wiring none of the bundle's storage, so the tokens can live in a PDO repository or anywhere else and Doctrine need not be installed at all.single_use_ttl_update: falseso a token issued in place of a single use one inherits the expiry of the one it replaced, bounding the chain.ListRefreshTokenManagerInterface::findAllForUser()andrevokeAllForUser()for showing a user their sessions and ending them.- One query per refresh instead of two.
- Symfony 7.0 and 7.1 are installable; two components stopped at
^7.2while the rest allowed^7.0, and Composer resolves the intersection. - Fixes to the cookie expiry, logout invalidating somebody else's token, reading the token from a body with no
Content-Type,delete()reporting the rows it actually removed, andcookie.same_sitefrom an environment variable.
The issue tracker is empty as of this release: 64 issues were reviewed and closed, several of them fixed here.