Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Jwt Refresh Token Bundle Laravel Package

mrellerm/jwt-refresh-token-bundle

A Symfony bundle that adds JWT refresh token support to your authentication flow, letting clients exchange long-lived refresh tokens for new access tokens. Includes token rotation, Doctrine-backed storage, and integration with LexikJWTAuthenticationBundle.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package — composer require into a Laravel app will pull in Symfony HTTP/Doctrine dependencies and register nothing usable. As a Laravel developer you only reach for this when you're maintaining a Symfony 3.4/4.x/5.x service (the repo was last touched Feb 2020). In a Laravel codebase the same pattern is tymon/jwt-auth rotations plus a custom fingerprint claim; the design notes below still translate.

Minimal setup in a Symfony project:

composer require mrellerm/jwt-refresh-token-bundle
  1. Register the bundle in config/bundles.php:
    MrEllerM\JWTRefreshTokenBundle\JWTRefreshTokenBundle::class => ['all' => true],
    
  2. Configure under config/packages/gesdinet_jwt_refresh_token.yaml (the fork keeps the upstream config key):
    gesdinet_jwt_refresh_token:
        firewall: api
        user_provider: app.user_provider
        ttl: 2592000
        doctrine_orm: true
    
  3. Create the RefreshToken entity extending the bundle's mapped superclass (or copy it if the fork requires extra fingerprint columns — check the fork's Entity/RefreshToken.php, that's where the divergence lives).
  4. Run bin/console doctrine:migrations:diff + migrate — the table is refresh_tokens, plus whatever fingerprint column the fork adds.
  5. Expose the route — either import the bundle's routing attributes or point a controller at gesdinet_jwt_refresh_token.refresh_token service.

First use case: POST /api/token/refresh with {"refresh_token": "..."} returns a fresh JWT pair. The fork's twist is that the request must also carry the same fingerprint the token was minted with (header/IP/UA — see the fork's config), so a stolen refresh token alone is not enough.

Where to look first: DependencyInjection/Configuration.php and Entity/RefreshToken.php in the fork. They tell you exactly which fingerprint field and which config knobs exist — the fork's README is thin and the code is ground truth.

Implementation Patterns

Mint fingerprint on login. The fingerprint must be written to the RefreshToken row when the access token pair is first issued. In a Symfony flow that's either a lexik_jwt_authentication.on_authentication_success listener or an event subscriber on the refresh-token creation. The fork exposes the entity setters; you set the fingerprint hash before $refreshTokenManager->save($token).

$token->setFingerprint(hash('sha256', $request->headers->get('X-Device-Fingerprint')));

Validate fingerprint on refresh. The bundle's refresh controller compares the stored fingerprint to the incoming request. Make sure the client sends the same fingerprint value on login and refresh — same header name, same normalization (trim, lowercase).

Rotation with single-use tokens. Keep single_use: true (or revoke_on_refresh) so the old refresh token is deleted on each refresh. Combined with the fingerprint this gives you: access token short-lived, refresh token single-use and bound to a device.

Multiple devices per user. The bundle naturally issues one row per login, so a user on phone + laptop gets two tokens with two fingerprints. Don't try to make the fingerprint per-user — it's per-session/device.

Logout = revoke. Call gesdinet_jwt_refresh_token.refresh_token service's revoke($token) (or the gesdinet:jwt:revoke command if present in the fork) from your logout controller. Do not rely on TTL expiry for logout.

Scheduled cleanup. Run gesdinet:jwt:clear from cron daily to purge expired rows — otherwise the table grows unbounded on a busy API.

Password change / compromise. Iterate refreshTokenManager->revokeAllForUser($user) (or query the repository by username) and force re-login across all devices when credentials change.

Integration with Lexik: the refresh endpoint is outside the firewall's authenticated pattern. Do not add it to security.firewalls.api.pattern or users will be asked for a valid JWT to refresh a JWT — a chicken-and-egg failure that is the single most common setup bug.

Gotchas and Tips

  • Abandoned and single-maintainer. 0 stars, last release 2020, no dependents. Treat this as vendored code: fork it into your own src/, or better, apply the fingerprint patch onto upstream gesdinet/jwt-refresh-token-bundle and pin a commit hash. Do not composer update this casually — the fork is behind upstream security fixes.
  • Symfony version drift. Built for Symfony 3.4/4.x. On Symfony 5.4+ the bundle still usually works, but on Symfony 6 the doctrine_orm service wiring and controller signatures will likely break. Test boot before committing.
  • Fingerprint mismatches behind proxies. If you fingerprint on IP or User-Agent, mobile clients and CDNs will rotate it. Prefer a client-generated X-Device-Fingerprint header (UUID stored in secure storage) over IP. If you must use IP, configure trusted_proxies and read X-Forwarded-For consistently — otherwise 127.0.0.1 is what gets stored.
  • Fingerprint is not encryption. Hash it (sha256) before storing; a DB leak shouldn't hand attackers the value needed to pass the check.
  • The refresh route must be public in security.yaml, but the controller still needs to run $this->get('gesdinet_jwt_refresh_token.refresh_token')->refresh($token). Don't put auth on the route hoping the bundle handles it.
  • TTL arithmetic. The bundle's ttl is the refresh token lifetime in seconds; the JWT itself has a separate lexik_jwt_authentication.token_ttl. Keep JWT short (15 min) and refresh long (30 days). If refresh ttl < JWT ttl users get logged out silently mid-session.
  • Clock skew. Two servers with drifting clocks can cause "token not yet valid" on refresh. Keep NTP healthy, and if you see it, add a few seconds of leeway in the Lexik config, not the refresh TTL.
  • Debugging tip: the Symfony profiler won't show auth on the refresh endpoint by design. Log the RefreshToken id, the fingerprint compare result, and expiry inside a small subscriber — the bundle gives you almost no logging otherwise.
  • Extension point: subscribe to the bundle's events (gesdinet.jwtrefreshtoken.*) if you need to audit refreshes or emit metrics; the fork inherits them from upstream.
  • CORS: the refresh route is hit from browsers/apps with a body, not a bearer header. Include it in your CORS allow-list and allow the fingerprint header in Access-Control-Allow-Headers, or the browser will strip it and every refresh fails the fingerprint check.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin