JWTRefreshTokenBundle and the problem of keeping a JWT login alive in Symfony
Implements a Refresh Token system over Json Web Tokens in Symfony
At a glance
- What is it?
- JWTRefreshTokenBundle adds refresh token storage to a Symfony application that already uses LexikJWTAuthenticationBundle, with a Doctrine entity you extend and a config key pointing at it. It solves token rotation and revocation, and its sharpest edge is a mapping problem that only appears when you turn off auto-mapping.
- Who is it for?
- JWTRefreshTokenBundle suits a Symfony application that already authenticates with LexikJWTAuthenticationBundle and needs short-lived access tokens backed by a stored refresh token, because it supplies the entity, the mapping and the storage side without asking you to design any of it. It does not suit a project that wants a different token strategy, and it does not suit an application on PHP below 8.2 or Symfony below 6.4, since the current major requires both.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 32 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A bundle that assumes Lexik already does the hard part
The purpose of this bundle is to manage refresh tokens with JWT, Json Web Tokens, in an easy way, and it is explicit that it does not stand alone. The bundle uses LexikJWTAuthenticationBundle, and it supports Doctrine ORM or ODM.
That dependency is the whole design. Lexik handles the authentication side, issuing and validating access tokens, and this bundle handles what comes after: the long-lived token that lets a client obtain a new access token without the user typing a password again.
The practical consequence is that you evaluate it as an addition to a Lexik setup, not as a replacement for one. If your application authenticates some other way, this bundle has nothing to attach to. If it already uses Lexik, the question becomes whether you want refresh tokens at all, because an access token with a short expiry and no refresh path means the user is signed out every time it expires.
The project is MIT licensed, written in PHP, and is a Symfony bundle. The package on Packagist is gesdinet/jwt-refresh-token-bundle, and the default branch is master. The last push was on 2026-09-21.
Installation needs Doctrine alongside it, and the guide is blunt about it
The first installation step carries a warning that is easy to skim past. You must also install either the Doctrine ORM or MongoDB ODM, because these packages are not installed automatically with this bundle, and failing to do so may trigger errors on installation.
With the Doctrine ORM, the command is:
composer require doctrine/orm doctrine/doctrine-bundle gesdinet/jwt-refresh-token-bundleWith the MongoDB ODM it is the MongoDB equivalents plus the same bundle:
composer require doctrine/mongodb-odm doctrine/mongodb-odm-bundle gesdinet/jwt-refresh-token-bundleA third option is a custom persistence layer, and it comes with two requirements. You provide an implementation of the Doctrine persistence ObjectManager interface, and you configure the bundle to use your object manager.
The second step is enabling the bundle. In a Symfony Flex application the bundle should be registered automatically, but if not you add it to config/bundles.php:
<?php
return [
//...
Gesdinet\JWTRefreshTokenBundle\GesdinetJWTRefreshTokenBundle::class => ['all' => true],
];That is the whole registration step. The bundle is a normal Symfony bundle with a normal enable line, and Flex handles the recipe in the common case.
You extend a base entity and point one config key at it
The third step is configuration, and it is a single key.
In a Flex application a recipe should have been applied, and if not you create config/packages/gesdinet_jwt_refresh_token.yaml with the refresh token class named:
gesdinet_jwt_refresh_token:
refresh_token_class: App\Entity\RefreshToken # This is the class name of the refresh token, you will need to adjust this to match the class your application will useThen you create the object class, and this is the part that looks strange at first. Your class extends a base class that the bundle ships, and it has no body of its own. With the Doctrine ORM the file goes to src/Entity/RefreshToken.php:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Gesdinet\JWTRefreshTokenBundle\Entity\RefreshToken as BaseRefreshToken;
#[ORM\Entity]
#[ORM\Table(name: 'refresh_tokens')]
class RefreshToken extends BaseRefreshToken
{
}With the MongoDB ODM the equivalent goes to src/Document/RefreshToken.php, and the base class is the document variant:
<?php
namespace App\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Gesdinet\JWTRefreshTokenBundle\Document\RefreshToken as BaseRefreshToken;
#[ODM\Document(collection: 'refresh_tokens')]
class RefreshToken extends BaseRefreshToken
{
}The reason for the indirection is that the bundle owns the mapping. The identifier, the columns and the table name all come from the base class, and your class exists so your application can have its own namespace, and so the storage location is a configuration decision rather than a hardcoded one.
The collection and table name are both refresh_tokens, and in the ODM case you have to update the refresh_token_class configuration to match the class you actually used, or the bundle will look for a class that does not exist.
The auto_mapping trap and the two errors Doctrine throws
There is one failure mode documented in unusual detail, and it is the most useful thing in the guide for anyone with a non-default Doctrine setup.
The class you write inherits its mapping from the base class, whose mapping ships with the bundle. Doctrine registers it for you only while doctrine.orm.auto_mapping is true, which is the default. With it turned off, the base class is never mapped, so the class you wrote inherits nothing, and Doctrine reports it as:
No identifier/primary key specified for Entity "App\Entity\RefreshToken"
sub class of "Gesdinet\JWTRefreshTokenBundle\Entity\RefreshToken".
Every Entity must have an identifier/primary key.Or, depending on how far Doctrine got before it noticed:
Class "App\Entity\RefreshToken" sub class of
"Gesdinet\JWTRefreshTokenBundle\Entity\RefreshToken"
is not a valid entity or mapped super class.The fix is to name the mapping alongside your own:
# config/packages/doctrine.yaml
doctrine:
orm:
auto_mapping: false
mappings:
App:
# ... your own mapping
GesdinetJWTRefreshToken:
type: xml
is_bundle: false
dir: '%kernel.project_dir%/vendor/gesdinet/jwt-refresh-token-bundle/config/doctrine'
prefix: 'Gesdinet\JWTRefreshTokenBundle\Entity'The base class is a mapped superclass, so this adds no table of its own. The same applies with more than one entity manager, for a different reason: Doctrine allows auto_mapping on only one of them, so the bundle's mapping lands there and nowhere else, and you have to name the manager instead.
This is a good example of a bundle that has clearly been used in awkward configurations and has had its sharp edges written down.
Version support, the upgrade path and the tooling around it
The current major has a clear floor. The bundle requires PHP 8.2 or later and Symfony 6.4, 7.2 or 8.0 or newer, and for support with older Symfony versions you should use the 1.x release.
That is a meaningful spread. The 1.x line is not a legacy branch that only receives security fixes, it is the supported option for anything on an older Symfony, and the 3.0 line is where the current work happens.
The upgrade documentation is unusually thorough, and the number of documents is itself a signal about how much changed across the majors. There are separate UPGRADE files for 2.0, 2.1, 2.2 and 3.0, and the changelog carries the full list.
The 3.0 upgrade guide is a different shape from the rest. Coming from an older version, UPGRADE-RECTOR.md walks the whole path from 1.5 one release at a time, with rule sets for the renames Rector can do and a checklist for the parts it cannot.
That matches the tooling in the repository, which is extensive for a bundle of this size. There is a phpstan.neon.dist with the PHPStan level set to 10, a psalm.xml.dist, a rector.php plus a rector/ directory, a php-cs-fixer.php, a styleci.yml, a markdownlint.json, a phpunit.xml.dist and a .markdownlint.json. The tests run through a GitHub Actions workflow, and there is a coverage badge.
The release history shows a concentrated burst around the 3.0 work: v2.2.1 and v2.2.2 on 2026-08-04 and v3.0.0 on 2026-08-05, with the last push on 2026-09-21.
HTTPS is recommended but not enforced
There is a one-line recommendation in the prerequisites section that is worth taking seriously rather than skimming.
The bundle does not force you to use HTTPS, but the guide says it is highly recommended. The reason follows from what a refresh token is. An access token is short-lived and scoped, and a leaked one expires quickly. A refresh token is the long-lived credential that can mint new access tokens, so a refresh token that crosses a network in the clear is the whole authentication system compromised, not one session.
That is the practical reason to treat HTTPS as a requirement in your deployment checklist even though the package does not check it.
The second reason to look at the configuration is the entity itself. The base class that ships with the bundle defines the table, and the storage of refresh tokens is a security-relevant database table: it holds credentials that can be exchanged for access tokens, so it is worth restricting access to it and worth treating the rows as sensitive rather than as session bookkeeping.
A related operational question is what happens when a user signs out. The bundle stores refresh tokens, which means it can remove or invalidate them, and that is the mechanism by which a session can actually be ended rather than merely expiring. Whether the logout path you use does that is a question about your own authentication wiring, and the documentation here does not cover it.
Editorial conclusion
JWTRefreshTokenBundle suits a Symfony application that already authenticates with LexikJWTAuthenticationBundle and needs short-lived access tokens backed by a stored refresh token, because it supplies the entity, the mapping and the storage side without asking you to design any of it. It does not suit a project that wants a different token strategy, and it does not suit an application on PHP below 8.2 or Symfony below 6.4, since the current major requires both. Before you deploy, read UPGRADE-3.0.md and the changelog, because the 3.0 line changed things, and check your doctrine.orm.auto_mapping setting, since with it turned off the entity you extend inherits no mapping and you have to name the bundle's mapping yourself.
Frequently asked questions
What does JWTRefreshTokenBundle do?
It manages refresh tokens with JWT in a Symfony application, on top of LexikJWTAuthenticationBundle which handles the access token side. It supports Doctrine ORM and MongoDB ODM, and stores the tokens through an entity you extend and a configurable class name.
How do I install JWTRefreshTokenBundle?
With Composer, installing doctrine/orm and doctrine/doctrine-bundle alongside gesdinet/jwt-refresh-token-bundle, or the MongoDB ODM equivalents. The Doctrine packages are not pulled in automatically and failing to install them may trigger errors, and a custom persistence layer is also possible if you provide a Doctrine persistence ObjectManager implementation.
Why does Doctrine report no identifier/primary key for my RefreshToken entity?
Because your entity inherits its mapping from the base class the bundle ships, and Doctrine registers that mapping only while doctrine.orm.auto_mapping is true. With auto_mapping turned off you must name the bundle's mapping in your doctrine.yaml under a GesdinetJWTRefreshToken key.
Which PHP and Symfony versions does JWTRefreshTokenBundle require?
The current major requires PHP 8.2 or later and Symfony 6.4, 7.2 or 8.0 or newer. For older Symfony versions the guide says to use the 1.x release instead.
How should I upgrade to version 3 of JWTRefreshTokenBundle?
Read UPGRADE-3.0.md, UPGRADE-2.2.md, UPGRADE-2.1.md and UPGRADE-2.0.md for what changed, and the changelog for the full list. If you are coming from a much older version, UPGRADE-RECTOR.md walks the path from 1.5 one release at a time, with Rector rule sets for the renames it can automate and a checklist for the rest.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/markitosgv-jwtrefreshtokenbundle)Was this useful?