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 Bundle Laravel Package

yivoff/jwt-refresh-bundle

Symfony bundle that adds refresh-token support to JWT authentication, letting clients exchange a long-lived refresh token for new access tokens instead of re-authenticating. Token lifetime, storage, and rotation are configurable.

View on GitHub
Deep Wiki
Context7

Getting Started

The bundle slots into an existing Lexik JWT setup: Lexik mints short-lived access tokens, this bundle mints long-lived, rotating refresh tokens that can be exchanged for a new access token without the user re-entering credentials.

  1. Install and register. The package is small and unmaintained-ish (3 stars, 0 dependents), so pin an exact version and verify the composer.json constraints against your Symfony version before going further.

    composer require yivoff/jwt-refresh-bundle
    
    // config/bundles.php
    Yivoff\JwtRefreshBundle\YivoffJwtRefreshBundle::class => ['all' => true],
    
  2. Configure it against the firewall that already has Lexik wired up:

    # config/packages/yivoff_jwt_refresh.yaml
    yivoff_jwt_refresh:
        firewall: api                 # must be a Lexik-enabled firewall
        ttl: 2592000                  # refresh-token lifetime in seconds (30d example)
        parameter_name: refresh_token # request field the client sends
        cookie: false                 # true = also emit an HttpOnly cookie
    
  3. Import the bundle's routes. The endpoints are shipped with the bundle; look in vendor/yivoff/jwt-refresh-bundle/Resources/config/ for the exact resource filename in your installed version, then:

    # config/routes/yivoff_jwt_refresh.yaml
    yivoff_jwt_refresh:
        resource: '@YivoffJwtRefreshBundle/Resources/config/routing.xml'
        prefix: /api
    
  4. Confirm before writing any code:

    bin/console debug:router | grep -i refresh
    bin/console debug:container Yivoff
    

    You should see the refresh (and usually a verify) route, plus the TokenGeneratorInterface, TokenStorageInterface and TokenHasherInterface services from Yivoff\JwtRefreshBundle\Contracts.

First use case — issue a refresh token at login. You control the login response (Lexik returns the access token; you add the refresh token), so call the generator there:

use Yivoff\JwtRefreshBundle\Contracts\TokenGeneratorInterface;

public function login(UserInterface $user, TokenGeneratorInterface $generator): JsonResponse
{
    return $this->json([
        'token'         => $this->jwtManager->create($user), // Lexik
        'refresh_token' => $generator->createForUser($user),
    ]);
}

Then the client exchanges it:

curl -X POST https://app.local/api/token/refresh \
     -d 'refresh_token=<value>'
# → { "token": "...", "refresh_token": "..." }

Implementation Patterns

Standard rotation loop (stateless clients). Treat the refresh response as a full credential swap: the returned refresh_token replaces the old one, which is revoked server-side on exchange. On the client, persist the new value before retrying the original request, and guard the refresh call with a single-flight mutex so concurrent 401s don't race each other.

async function authedFetch(url, init) {
  let res = await fetch(url, withAuth(init));
  if (res.status !== 401) return res;
  const rotated = await singleFlight(() => refresh());
  store.refreshToken = rotated.refresh_token;
  store.accessToken  = rotated.token;
  return fetch(url, withAuth(init));
}

Cookie mode for browser SPAs. Set cookie: true so the bundle sets the refresh token as an HttpOnly cookie. Keep the access token in memory (never localStorage), let the cookie ride along to the refresh endpoint with credentials: 'include', and tighten cookie flags (SameSite=Lax/Strict, Secure) in the bundle config or by wrapping the response in a listener.

Logout / revocation. The storage contract exposes a revoke operation; call it from your logout controller (or a LogoutEvent listener) using the identifier you received at issue time. On cookie mode, clear the cookie in the same response so the client doesn't hold a token the server has dropped.

Custom storage for scale. The bundled default is fine for a single app, but if you run multiple API pods or want TTLs enforced by the store, implement TokenStorageInterface over Redis (key = token identifier, value = hash, TTL = ttl) and alias it:

services:
    Yivoff\JwtRefreshBundle\Contracts\TokenStorageInterface:
        class: App\Security\RedisTokenStorage

Custom hasher. If you want to change how tokens are hashed at rest (the bundle hashes before persisting so a DB leak doesn't expose usable tokens), swap TokenHasherInterface. Note that this is a migration point: changing the hasher invalidates every stored token, so all users must re-login.

Enriching the new access token. The bundle delegates to Lexik's JWTManager, so to add claims to the refreshed token, hook Lexik's JWTCreatedEvent — the same listener that fires on normal login will fire here. This is how you keep parity between "logged in" and "refreshed" tokens.

Testing. Override TokenStorageInterface with an in-memory implementation in config/packages/test/ and inject a fixed ClockInterface if your version supports it, so refresh-expiry tests are deterministic rather than sleeping.

Gotchas and Tips

  • Maturity risk is the headline gotcha. 3 stars, 0 dependents, low opportunity score. Before adopting, diff its composer.json constraints against your Symfony/Lexik versions — expect to need --with-all-dependencies and possibly local patches for Symfony 6/7. If this is greenfield, compare against gesdinet/jwt-refresh-token-bundle, which solves the same problem with far more usage; the trade-off is that this bundle is leaner and its contracts are easier to swap.
  • Firewall mismatch is the #1 setup failure. The firewall key must name a firewall whose user_provider matches the one Lexik uses. If the refresh request 401s with "user not found," it's almost always provider misconfiguration, not a bad token.
  • The refresh endpoint must be public. It has no JWT to authenticate with — the refresh token is the credential. Exclude it from your firewall's authenticators (security: false or a dedicated firewall with only your auth mechanism).
  • Rotation breaks concurrent refreshes. Old token is revoked on first exchange; a second in-flight request gets 401 and the user is logged out. Single-flight on the client is mandatory, not optional.
  • TTL is not the same as Lexik's. Don't confuse ttl (refresh lifetime, days/weeks) with Lexik's lexik_jwt_authentication.token_ttl (minutes). Keep them clearly named in your own config or you'll debug the wrong clock.
  • Plaintext tokens are one-shot. Storage only keeps a hash, so a lost refresh token cannot be recovered or re-printed — the only remedy is re-authentication. Don't log request bodies on the refresh route.
  • Check the response contract. The bundled controller's JSON shape (token vs token/access_token, expiry keys) is version-specific. Wrap the controller with your own thin endpoint (decorating the controller service, or a listener that reshapes the response) if your clients expect a fixed schema.
  • Doctrine storage means migrations. If the default storage is the bundled entity, run bin/console doctrine:migrations:diff and review the generated table before deploying — don't let schema:update create it silently in prod.
  • Debugging order: debug:router (route exists?) → debug:container Yivoff (services wired?) → the 401 body message (expired / invalid / not found are distinct and point at different bugs) → profiler, where the token-storage query shows whether the hash actually matched.
  • Extension points are clean. All three contracts (TokenGeneratorInterface, TokenStorageInterface, TokenHasherInterface) are aliased in the container, so you can replace any one without forking the bundle — prefer that over modifying vendor code, which you'll lose on the next composer update (and there will be one, given the package's age).
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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package