firebase/php-jwt
Encode and decode JSON Web Tokens (JWT) in PHP (RFC 7519). Supports common signing algorithms, header handling, and clock-skew leeway. Simple API with JWT::encode() and JWT::decode() plus Key objects for verification.
Installation:
composer require firebase/php-jwt
For environments without libsodium, add:
composer require paragonie/sodium_compat
First Use Case: Encode a JWT with a symmetric key (e.g., HS256):
use Firebase\JWT\JWT;
$key = 'your-secret-key';
$payload = ['user_id' => 123, 'iat' => time()];
$jwt = JWT::encode($payload, $key, 'HS256');
Decode it in a Laravel middleware or controller:
try {
$decoded = JWT::decode($jwt, new \Firebase\JWT\Key($key, 'HS256'));
$userId = $decoded->user_id;
} catch (\Firebase\JWT\ExpiredException $e) {
// Handle expired token
}
Where to Look First:
Laravel Service Provider:
Bind a JWTGenerator facade to centralize token creation:
// app/Providers/JWTServiceProvider.php
public function register()
{
$this->app->singleton('jwt', function () {
return new class {
public function generate(array $payload, string $key, string $algorithm = 'HS256')
{
return JWT::encode($payload, $key, $algorithm);
}
};
});
}
Usage:
$token = app('jwt')->generate(['user_id' => 1], config('jwt.secret'));
Payload Standardization: Always include:
$payload = [
'iss' => 'your-app-name', // Issuer
'aud' => 'your-api', // Audience
'iat' => time(), // Issued at
'exp' => time() + 3600, // Expires in 1 hour
'data' => [...] // Custom claims
];
Extract Token from Authorization Header:
// app/Http/Middleware/AuthenticateJWT.php
public function handle($request, Closure $next)
{
$token = $request->bearerToken();
if (!$token) {
return response()->json(['error' => 'Unauthorized'], 401);
}
try {
$decoded = JWT::decode($token, new Key(config('jwt.secret'), 'HS256'));
$request->merge(['user' => $decoded]);
return $next($request);
} catch (\Exception $e) {
return response()->json(['error' => 'Invalid token'], 401);
}
}
Register Middleware:
// app/Http/Kernel.php
protected $routeMiddleware = [
'jwt.auth' => \App\Http\Middleware\AuthenticateJWT::class,
];
Store Keys Securely:
Use Laravel's config or environment variables:
JWT_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----\n...
JWT_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----\n...
Load in config/jwt.php:
return [
'private_key' => env('JWT_PRIVATE_KEY'),
'public_key' => env('JWT_PUBLIC_KEY'),
];
Encode/Decode:
$jwt = JWT::encode($payload, config('jwt.private_key'), 'RS256');
$decoded = JWT::decode($jwt, new Key(config('jwt.public_key'), 'RS256'));
Fetch and Cache JWKS:
use Firebase\JWT\CachedKeySet;
use GuzzleHttp\Client;
$keySet = new CachedKeySet(
'https://your-api.com/.well-known/jwks.json',
new Client(),
new \GuzzleHttp\Psr7\HttpFactory(),
new \PhpFastCache\CacheManager('files')
);
$decoded = JWT::decode($jwt, $keySet);
Laravel Cache Integration:
Use Illuminate\Cache\CacheManager as the cache pool:
$cache = app('cache')->store('file');
$keySet = new CachedKeySet($jwksUri, $httpClient, $httpFactory, $cache);
Mock JWT in Tests:
use Firebase\JWT\JWT;
$payload = ['user_id' => 1];
$token = JWT::encode($payload, 'test-key', 'HS256');
$this->withHeader('Authorization', 'Bearer ' . $token)
->get('/api/protected')
->assertOk();
Assertions for Exceptions:
$this->expectException(\Firebase\JWT\ExpiredException::class);
JWT::decode($expiredToken, new Key('key', 'HS256'));
Algorithm Mismatch:
UnexpectedValueException: Algorithm does not match key.JWT::encode() matches the key type (e.g., HS256 for symmetric keys, RS256 for RSA).JWT::$leeway if tokens expire prematurely due to clock skew.Key Format Issues:
DomainException: Invalid key.\n).libsodium.// Correct (double quotes for newlines)
$privateKey = "-----BEGIN RSA PRIVATE KEY-----\n...";
Missing Claims:
ExpiredException or BeforeValidException even with valid timestamps.iat (issued at) and exp (expiration) claims are included in the payload.Libsodium Dependency:
DomainException: libsodium is required but not available.paragonie/sodium_compat or enable libsodium in PHP:
pecl install channel://pecl.php.net/libsodium
Decode Without Verification (Caution!): Manually inspect JWT structure (for debugging only):
list($headerB64, $payloadB64, $signature) = explode('.', $jwt);
$header = json_decode(base64_decode($headerB64));
$payload = json_decode(base64_decode($payloadB64));
Warning: This bypasses security! Use only for debugging.
Log Exceptions:
Wrap JWT::decode() in a try-catch to log errors:
try {
$decoded = JWT::decode($jwt, $key);
} catch (\Exception $e) {
\Log::error("JWT Decode Error: " . $e->getMessage());
throw $e;
}
Validate Token Structure: Use a regex to check JWT format before processing:
if (!preg_match('/^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/', $jwt)) {
throw new \InvalidArgumentException('Invalid JWT format');
}
Cache Key Sets:
For JWKS endpoints, use CachedKeySet to avoid repeated HTTP requests:
$keySet = new CachedKeySet($jwksUri, $httpClient, $httpFactory, $cache, 3600); // Cache for 1 hour
Precompute Keys:
If using multiple keys, precompute the Key objects and store them in a service container:
$this->app->bind('jwt.keys', function () {
return collect(config
How can I help you explore Laravel packages today?