Installation
composer require ciricihq/jwt-client-bundle
Add to config/bundles.php:
return [
// ...
Cirici\JWTClientBundle\CiriciJWTClientBundle::class => ['all' => true],
];
Configuration
Define your JWT server in config/packages/cirici_jwt_client.yaml:
cirici_jwt_client:
servers:
my_jwt_server:
url: 'https://api.example.com/auth'
public_key: '-----BEGIN PUBLIC KEY-----...'
algorithm: 'RS256'
First Use Case: Validate a Token
use Cirici\JWTClientBundle\Client\JWTClient;
$client = new JWTClient('my_jwt_server');
$isValid = $client->validateToken('Bearer ' . $userToken);
First Use Case: Login via JWT
$client = new JWTClient('my_jwt_server');
$token = $client->login('username', 'password');
use Symfony\Component\HttpFoundation\Request;
use Cirici\JWTClientBundle\Client\JWTClient;
class AuthController extends AbstractController
{
public function protectedRoute(Request $request, JWTClient $jwtClient)
{
$token = $request->headers->get('Authorization');
if (!$jwtClient->validateToken($token)) {
return $this->json(['error' => 'Invalid token'], 401);
}
// Proceed with authenticated logic
}
}
use Cirici\JWTClientBundle\Client\JWTClient;
class ExternalService
{
public function __construct(private JWTClient $jwtClient) {}
public function fetchProtectedData(string $token): array
{
$this->jwtClient->validateToken($token); // Validate before API call
$client = new \GuzzleHttp\Client();
$response = $client->get('https://external-api.com/data', [
'headers' => ['Authorization' => $token],
]);
return json_decode($response->getBody(), true);
}
}
Create a JWT Authenticator:
use Cirici\JWTClientBundle\Client\JWTClient;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class JWTAuthenticator extends AbstractAuthenticator
{
public function __construct(private JWTClient $jwtClient) {}
public function supports(Request $request): ?bool
{
return $request->headers->has('Authorization');
}
public function authenticate(Request $request): Passport
{
$token = $request->headers->get('Authorization');
if (!$this->jwtClient->validateToken($token)) {
throw new \RuntimeException('Invalid JWT token');
}
// Extract user ID from token payload (customize based on your JWT structure)
$userId = $this->jwtClient->getTokenPayload($token)['user_id'];
return new SelfValidatingPassport(new UserBadge($userId));
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null; // Let Symfony handle it
}
public function onAuthenticationFailure(Request $request, \Throwable $exception): ?Response
{
return new Response('Authentication Failed', 401);
}
}
Register in security.yaml:
security:
firewalls:
main:
pattern: ^/api
stateless: true
jwt: ~
use Cirici\JWTClientBundle\Client\JWTClient;
class TokenManager
{
public function __construct(private JWTClient $jwtClient) {}
public function refreshTokenIfNeeded(string $token): string
{
if ($this->jwtClient->isTokenExpired($token)) {
$refreshToken = $this->jwtClient->getTokenPayload($token)['refresh_token'] ?? null;
if ($refreshToken) {
return $this->jwtClient->refreshToken($refreshToken);
}
throw new \RuntimeException('Token expired and no refresh token available');
}
return $token;
}
}
Public Key Management
openssl rsa -pubin -in key.pem -text -noout to verify key validity.Token Validation Edge Cases
leeway in the client:
cirici_jwt_client:
servers:
my_jwt_server:
leeway: 60 # Allow 60 seconds of clock skew
TokenExpiredException gracefully. Example:
try {
$jwtClient->validateToken($token);
} catch (\Cirici\JWTClientBundle\Exception\TokenExpiredException $e) {
return $this->redirectTo('/login?expired=true');
}
Algorithm Mismatch
HS512), ensure it’s supported by the underlying library (firebase/php-jwt).algorithm in config or extend the bundle to support custom algorithms.Rate Limiting
use Symfony\Component\Panther\Client;
$client = new Client($jwtServerUrl, [
'timeout' => 10,
'connect_timeout' => 5,
]);
Enable Verbose Logging
Add to config/packages/monolog.yaml:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["cirici_jwt_client"]
Inspect Raw Token Data
Use getTokenPayload() to debug token contents:
$payload = $jwtClient->getTokenPayload('Bearer ' . $token);
dump($payload); // Inspect claims like 'exp', 'iss', 'sub'
Test with Postman/cURL Manually validate tokens using:
curl -X POST https://api.example.com/auth/validate \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json"
Custom Claims Validation
Extend the JWTClient to add custom claim checks:
use Cirici\JWTClientBundle\Client\JWTClient;
use Cirici\JWTClientBundle\Exception\TokenInvalidException;
class CustomJWTClient extends JWTClient
{
public function validateToken(string $token): bool
{
parent::validateToken($token);
$payload = $this->getTokenPayload($token);
// Custom logic: e.g., check 'scope' claim
if (!in_array('admin', $payload['scope'] ?? [])) {
throw new TokenInvalidException('Insufficient permissions');
}
return true;
}
}
Override HTTP Client Replace the default Guzzle client for custom behavior (e.g., proxies, middleware):
cirici_jwt_client:
servers:
my_jwt_server:
http_client:
base_uri: 'https://api.example.com'
headers:
'X-Custom-Header': 'value'
Event Listeners Listen for token validation events (requires extending the bundle or using Symfony events):
// Example: Log token validation attempts
$jwtClient->addListener(function (string $token, bool $isValid) {
\Monolog\Logger::getInstance('security')->info(
'JWT Validation',
['token' => substr($token, 0, 10) . '...', 'valid' => $isValid]
);
});
cirici_jwt_client:
servers:
server_a:
url
How can I help you explore Laravel packages today?