Symfony 8, PHP 8.4 and LexikJWTAuthenticationBundle 3.
This release needs a schema change before the application will run. Refresh tokens gained family and family_valid columns, and Doctrine reads every mapped field.
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrateRead UPGRADE-3.0.md first, and UPGRADE-RECTOR.md if you are coming from further back than 2.2.
If you are on Symfony 6.4 or 7.x, stay on 2.2. It is maintained, and Symfony 7.4 is supported until November 2028.
What a session is now
Refresh tokens belong to a chain. A token issued in place of another carries the family of the one it replaced, so a login and every refresh descending from it share one value — which is what makes a session addressable. Without it, "end this session" can only mean "delete this one token", and with single_use that token has usually already been replaced.
Everything below is built on that, and all of it is off by default.
reuse_detection— recognises a spent token being presented again and revokes the whole chain. Rotation on its own leaves a stolen token working until the legitimate client happens to refresh, and nobody learns why it broke. It cannot tell theft from a client racing itself, so it revokes either way and dispatches an event for the judgement it cannot make.max_session_lifetime— a ceiling on how long a chain may go on for, whateverttlsays.block_jwts_on_revocation— refuses the JWTs already issued to a user whenrevokeAllForUser()takes their refresh tokens away. Lexik's blocklist is keyed byjtiand cannot do this; what is recorded instead is when the revocation happened.SessionLister— the "where you are signed in" screen and the button next to each row, grouped by chain rather than by token.rate_limiter— consumed before the token is looked at, so a refusal costs no query and its timing says nothing about whether the token exists.cache_pool— stores the tokens in a PSR-6 pool, where expiry is the pool's job. It implements only what a pool can honour and says so about the rest.- Per-firewall configuration —
ttl,single_useand the rest on therefresh_jwtauthenticator, which is the longest-standing thing this bundle could not do.
Breaking changes
- PHP 8.4, Symfony 8.0, LexikJWTAuthenticationBundle 3, doctrine/dbal 4
check_pathis required onrefresh_jwtRefreshEventtakes the request, and$firewallNameloses its defaultdbal_columns, when configured, has to name theidcolumn- Nine classes are
final; the token models and repositories are deliberately not
The full list is in the changelog.