Weave Code
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

ad3n/jwt-refresh-token-bundle

Symfony bundle to manage JWT refresh tokens alongside LexikJWTAuthenticationBundle. Supports Doctrine ORM or MongoDB ODM, adds refresh token generation/rotation and storage, plus endpoints and security integration for renewing access tokens securely.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation:

    composer require gesdinet/jwt-refresh-token-bundle
    

    Add to config/bundles.php:

    return [
        // ...
        Gesdinet\JWTRefreshTokenBundle\GesdinetJWTRefreshTokenBundle::class => ['all' => true],
    ];
    
  2. Configuration: Update config/packages/gesdinet_jwt_refresh_token.yaml:

    gesdinet_jwt_refresh_token:
        ttl: 3600  # Refresh token TTL in seconds (default: 3600)
        blacklist_ttl: 300  # Blacklist TTL (default: 300)
        storage: doctrine  # Options: doctrine, redis, or custom
        doctrine:
            entity: App\Entity\RefreshToken  # Custom entity (if using Doctrine)
    
  3. Entity Setup (Doctrine): Create a RefreshToken entity (or use the default):

    php bin/console make:entity RefreshToken
    

    Add fields:

    // src/Entity/RefreshToken.php
    use Doctrine\ORM\Mapping as ORM;
    
    #[ORM\Entity]
    class RefreshToken {
        #[ORM\Id, ORM\GeneratedValue, ORM\Column]
        private ?int $id = null;
    
        #[ORM\Column(length: 255)]
        private string $token;
    
        #[ORM\Column]
        private \DateTimeImmutable $createdAt;
    
        #[ORM\Column(nullable: true)]
        private ?\DateTimeImmutable $revokedAt = null;
    
        // Getters/setters...
    }
    
  4. First Use Case:

    • Generate Refresh Token: Extend your authentication logic to issue a refresh token alongside the JWT:

      use Gesdinet\JWTRefreshTokenBundle\Services\RefreshTokenManager;
      
      // In your auth controller/service
      $refreshTokenManager = $this->container->get(RefreshTokenManager::class);
      $refreshToken = $refreshTokenManager->createRefreshToken($user);
      
    • Validate Refresh Token: Use the RefreshTokenManager to validate and revoke tokens:

      $isValid = $refreshTokenManager->validateRefreshToken($refreshToken);
      if ($isValid) {
          $newJwt = $this->jwtManager->create($user); // LexikJWT
      }
      

Implementation Patterns

Workflows

  1. Token Issuance Flow:

    • Login: Issue both JWT (short-lived) and refresh token (long-lived).
      // Example in a login controller
      $user = $authenticator->authenticate($credentials);
      $jwt = $this->jwtManager->create($user);
      $refreshToken = $this->refreshTokenManager->createRefreshToken($user);
      
      return new JsonResponse([
          'access_token' => $jwt,
          'refresh_token' => $refreshToken->getToken(),
      ]);
      
  2. Refresh Token Flow:

    • Client Request: Send refresh token to /refresh endpoint.
    • Server-Side:
      // src/Controller/RefreshTokenController.php
      use Gesdinet\JWTRefreshTokenBundle\Services\RefreshTokenManager;
      use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
      
      #[Route('/refresh', name: 'refresh_token', methods: ['POST'])]
      public function refresh(
          Request $request,
          RefreshTokenManager $refreshTokenManager,
          JWTTokenManagerInterface $jwtManager
      ): JsonResponse {
          $refreshToken = $request->request->get('refresh_token');
          $user = $refreshTokenManager->getUserByRefreshToken($refreshToken);
      
          if (!$user) {
              throw new \RuntimeException('Invalid refresh token');
          }
      
          $newJwt = $jwtManager->create($user);
          return new JsonResponse(['access_token' => $newJwt]);
      }
      
  3. Token Revocation:

    • Logout: Revoke both JWT (via LexikJWT) and refresh token.
      $refreshTokenManager->revokeRefreshToken($refreshToken);
      $this->jwtManager->invalidateToken($jwt); // LexikJWT
      

Integration Tips

  • LexikJWT Integration: Ensure LexikJWTAuthenticationBundle is configured to blacklist tokens (via blacklist_ttl in lexik_jwt_authentication.yaml).

    lexik_jwt_authentication:
        blacklist_ttl: 0  # Disable Lexik's blacklist if using this bundle
    
  • Redis Storage: Configure Redis for storage in gesdinet_jwt_refresh_token.yaml:

    gesdinet_jwt_refresh_token:
        storage: redis
        redis:
            dsn: 'redis://localhost:6379'
    
  • Custom Storage: Implement Gesdinet\JWTRefreshTokenBundle\Storage\RefreshTokenStorageInterface for custom backends (e.g., DynamoDB).

  • Event Listeners: Listen to refresh_token.created and refresh_token.revoked events for auditing/logging:

    use Gesdinet\JWTRefreshTokenBundle\Event\RefreshTokenEvent;
    
    #[AsEventListener(event: RefreshTokenEvent::REFRESH_TOKEN_CREATED)]
    public function onRefreshTokenCreated(RefreshTokenEvent $event) {
        // Log or notify
    }
    
  • Testing: Use RefreshTokenManager in tests to mock token validation:

    $this->refreshTokenManager->expects($this->once())
        ->method('validateRefreshToken')
        ->with('test_token')
        ->willReturn(true);
    

Gotchas and Tips

Pitfalls

  1. Token Storage Mismatch:

    • If storage is set to doctrine but the RefreshToken entity is missing or misconfigured, the bundle will throw EntityManager errors.
    • Fix: Verify the entity is mapped and the RefreshTokenManager is autowired correctly.
  2. Blacklist Conflicts:

    • If both LexikJWTAuthenticationBundle and this bundle are configured to blacklist tokens, tokens may be double-blacklisted.
    • Fix: Disable Lexik's blacklist (blacklist_ttl: 0) if using this bundle.
  3. TTL Misconfiguration:

    • Setting ttl too low (e.g., < 60) may cause refresh tokens to expire too quickly, leading to poor UX.
    • Tip: Default 3600 (1 hour) is reasonable; adjust based on security needs.
  4. Race Conditions:

    • Refresh tokens can be revoked between validation and JWT issuance, causing inconsistent states.
    • Mitigation: Use transactions or atomic operations when revoking tokens.
  5. Redis Connection Issues:

    • If Redis is down, the bundle will throw Predis\Connection\ConnectionException.
    • Fix: Implement a fallback storage (e.g., Doctrine) or retry logic.

Debugging

  1. Token Validation Failures:

    • Check if the token exists in storage:
      $refreshToken = $refreshTokenManager->findRefreshToken('token_value');
      var_dump($refreshToken); // null if not found
      
    • Verify revokedAt is null for valid tokens.
  2. Doctrine Queries:

    • Enable Doctrine debug mode to inspect queries:
      doctrine:
          dbal:
              logging: true
      
  3. Event Debugging:

    • Listen to all events in development:
      #[AsEventListener(event: RefreshTokenEvent::class)]
      public function debugEvents(RefreshTokenEvent $event) {
          error_log('RefreshTokenEvent: ' . $event->getName() . ' - ' . $event->getRefreshToken()->getToken());
      }
      

Extension Points

  1. Custom Token Generation:

    • Extend Gesdinet\JWTRefreshTokenBundle\Services\RefreshTokenManager to customize token generation (e.g., add metadata):
      class CustomRefreshTokenManager extends RefreshTokenManager {
          public function createRefreshToken(UserInterface $user): RefreshToken {
              $token = parent::createRefreshToken($user);
              $token->setMetadata(['ip' => $this->getClientIp()]);
              return $token;
          }
      }
      
    • Register as a service:
      services:
          Gesdinet\JWTRefreshTokenBundle\Services\RefreshTokenManager:
              class: App\Services\CustomRefreshTokenManager
      
  2. Custom Storage:

    • Implement RefreshTokenStorageInterface for non-Doctrine/Redis backends:
      class CustomStorage implements RefreshTokenStorageInterface {
          public function save(RefreshToken $refreshToken): void {
              // Custom logic (e.g., DynamoDB)
          }
      
          public function find(string $token): ?RefreshToken {
              // Custom logic
          }
      
          public function revoke(string $token): void {
      
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.
emuniq/filament-browser-notifications
syriable/filament-translator
hungnm28/livewire-form
wenprise/eloquent
crudly/encrypted
fadion/bouncy
cuci/prototurk-sdk
gos/pubsub-router-bundle
cuci/prototurk-sdk-symfony
clementtalleu/easyadmin-markdown-bundle
codeflextech/permission-manager
karnoweb/livewire-datepicker
sayedenam/sayed-dashboard
milito/query-filter
apiboxsym/user-bundle
apiboxsym/health-check-bundle
jayeshmepani/jpl-moshier-ephemeris-php
elnasnato/laraliveui
labrodev/rest-sdk
sampaui/sampaui