gesdinet/jwt-refresh-token-bundle
Symfony bundle to manage JWT refresh tokens for LexikJWTAuthenticationBundle. Stores and rotates refresh tokens via Doctrine ORM or MongoDB ODM (or custom object manager), with configurable TTL, security, and endpoints. Requires PHP 8.2+ and Symfony 6.4+.
Install Dependencies
composer require doctrine/orm doctrine/doctrine-bundle gesdinet/jwt-refresh-token-bundle
Configure the Bundle
Create config/packages/gesdinet_jwt_refresh_token.yaml:
gesdinet_jwt_refresh_token:
refresh_token_class: App\Entity\RefreshToken
Generate the RefreshToken Entity
php bin/console make:entity RefreshToken
Use the provided base class:
// src/Entity/RefreshToken.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 {}
Set Up Routes
Add to config/routes.yaml:
api_refresh_token:
path: /api/token/refresh
Configure Security
Update config/packages/security.yaml:
security:
firewalls:
api:
pattern: ^/api
stateless: true
entry_point: jwt
json_login:
check_path: /api/login
jwt: ~
refresh_jwt:
check_path: api_refresh_token
Run Migrations
php bin/console make:migration
php bin/console doctrine:migrations:migrate
Send a POST request to /api/login with user credentials to receive an initial JWT and refresh token. Use the refresh token to call /api/token/refresh to get a new JWT without re-authenticating.
Authentication
/api/login (using json_login).{
"token": "jwt_token_here",
"refresh_token": "refresh_token_here"
}
Token Refresh
/api/token/refresh with the refresh_token in the request body.single_use: true is enabled).Token Invalidation
/api/token/invalidate to invalidate the refresh token (e.g., during logout).Custom Token Storage
Use the cookie configuration to store refresh tokens in HTTP-only cookies for enhanced security:
gesdinet_jwt_refresh_token:
cookie:
enabled: true
http_only: true
secure: true
Single-Use Tokens Enable single-use tokens to improve security:
gesdinet_jwt_refresh_token:
single_use: true
Custom Refresh Token Class
Extend the base RefreshToken class to add custom fields or logic:
// src/Entity/CustomRefreshToken.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Gesdinet\JWTRefreshTokenBundle\Entity\RefreshToken;
#[ORM\Entity]
#[ORM\Table(name: 'custom_refresh_tokens')]
class CustomRefreshToken extends RefreshToken {
#[ORM\Column(type: 'string', nullable: true)]
private ?string $customField = null;
}
Update the bundle config:
gesdinet_jwt_refresh_token:
refresh_token_class: App\Entity\CustomRefreshToken
Token TTL Management Configure token expiration and auto-refresh:
gesdinet_jwt_refresh_token:
ttl: 2592000 # 1 month in seconds
ttl_update: true # Auto-update TTL on refresh
Event Listeners
Listen to RefreshTokenEvent to customize token behavior:
// src/EventListener/RefreshTokenListener.php
namespace App\EventListener;
use Gesdinet\JWTRefreshTokenBundle\Event\RefreshTokenEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener(event: RefreshTokenEvent::class, method: 'onRefreshToken')]
class RefreshTokenListener {
public function onRefreshToken(RefreshTokenEvent $event) {
// Custom logic here
}
}
Missing Doctrine Dependencies
Forgetting to install doctrine/orm or doctrine/mongodb-odm will cause installation errors. Ensure dependencies are included:
composer require doctrine/orm doctrine/doctrine-bundle
Incorrect Route Configuration
The refresh_jwt firewall configuration must match the route name (api_refresh_token). Mismatches will result in 404 errors.
Token Parameter Name Mismatch
The default refresh_token parameter name must match the request payload. Customize it if needed:
gesdinet_jwt_refresh_token:
token_parameter_name: custom_refresh_token
Single-Use Tokens and Auto-Refresh
If single_use: true is enabled, ensure your frontend handles the new refresh token in the response to avoid token reuse issues.
Cookie Configuration Conflicts
When using cookies, ensure secure: true and http_only: true are set for HTTPS environments. Misconfigurations can lead to security vulnerabilities.
Logout Event Listener Not Triggering
The LogoutEventListener requires a logout path to be defined in the firewall. Without it, refresh tokens won’t be invalidated on logout:
security:
firewalls:
api:
logout:
path: api_token_invalidate
Check Token Existence Use Doctrine queries to verify refresh tokens are stored:
$refreshToken = $entityManager->getRepository(RefreshToken::class)->findOneBy(['token' => $token]);
Enable Debugging for Events
Listen to RefreshTokenEvent to debug token refresh logic:
#[AsEventListener(RefreshTokenEvent::class)]
public function onRefreshToken(RefreshTokenEvent $event) {
dump($event->getRefreshToken());
}
Validate Token Payload
Ensure the JWT payload includes the username or user_id claim, as the bundle relies on it to fetch the user:
{
"username": "user@example.com",
"exp": 1234567890
}
Clear Cache After Configuration Changes
After modifying gesdinet_jwt_refresh_token.yaml, clear the Symfony cache:
php bin/console cache:clear
Custom User Provider Override the default user provider for complex user logic:
security:
firewalls:
api:
refresh_jwt:
provider: custom_user_provider
Custom Token Generator
Extend the Gesdinet\JWTRefreshTokenBundle\Services\RefreshTokenGenerator service to generate tokens with custom logic.
Custom Validation Logic
Implement a custom UserCheckerInterface to validate users during token refresh:
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class CustomUserChecker implements UserCheckerInterface {
public function checkPreAuth(UserInterface $user) {
// Custom validation logic
}
public function checkPostAuth(UserInterface $user) {}
}
Register it in security.yaml:
security:
firewalls:
api:
user_checker: custom_user_checker
Custom Serializer for Responses
Override the default JSON response format by extending the Gesdinet\JWTRefreshTokenBundle\Serializer\RefreshTokenSerializer.
Database Schema Customization
Modify the RefreshToken entity to add custom columns or constraints:
#[ORM\Table(name: 'refresh_tokens', options: ['charset' => 'utf8mb4'])]
class RefreshToken extends BaseRefreshToken {
#[ORM\Column(type: 'datetime', nullable: true)]
private ?\DateTimeInterface $createdAt = null;
}
How can I help you explore Laravel packages today?