Verify JWT in PHP (firebase/php-jwt & Laravel)
Verify JWT signatures in PHP with firebase/php-jwt and Laravel Sanctum. Sign and verify HS256 and RS256 tokens, validate iss/aud/exp, and verify JWTs against a JWKS endpoint with full PHP code examples.
PHP’s standard JWT library is firebase/php-jwt. Laravel users often pair it with tymon/jwt-auth for a batteries-included JWT guard, or use Laravel Sanctum (which issues opaque tokens, not JWTs — covered briefly for context).
Install
composer require firebase/php-jwt
# Optional Laravel JWT guard
composer require tymon/jwt-auth
Sign and verify an HS256 token (firebase/php-jwt)
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$secret = getenv('JWT_SECRET'); // 32+ bytes
// Sign
$payload = [
'sub' => 'user_123',
'iss' => 'https://auth.example.com',
'aud' => 'https://api.example.com',
'iat' => time(),
'exp' => time() + 15 * 60,
];
$token = JWT::encode($payload, $secret, 'HS256');
// Verify — Key object with algorithm is mandatory since 6.0
JWT::$leeway = 60; // 60s clock skew for exp/nbf
try {
$decoded = JWT::decode($token, new Key($secret, 'HS256'));
// firebase/php-jwt does not validate iss/aud — check them yourself
if ($decoded->iss !== 'https://auth.example.com') {
throw new Exception('wrong iss');
}
$aud = $decoded->aud;
$audMatch = is_array($aud) ? in_array('https://api.example.com', $aud, true) : $aud === 'https://api.example.com';
if (!$audMatch) {
throw new Exception('wrong aud');
}
} catch (Firebase\JWT\ExpiredException $e) {
http_response_code(401);
} catch (Firebase\JWT\SignatureInvalidException $e) {
http_response_code(401);
}
Sign and verify an RS256 token (firebase/php-jwt)
$privateKey = openssl_pkey_get_private('file:///path/to/rsa-private.pem');
$publicKey = openssl_pkey_get_public('file:///path/to/rsa-public.pem');
$token = JWT::encode($payload, $privateKey, 'RS256', 'rsa-key-v1');
$decoded = JWT::decode($token, new Key($publicKey, 'RS256'));
Verify a JWT against a JWKS endpoint (firebase/php-jwt)
use Firebase\JWT\JWK;
// Cache this for 1 hour in APCu — do not fetch on every request
$jwks = json_decode(file_get_contents('https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json'), true);
$keyRing = JWK::parseKeySet($jwks, 'RS256'); // array of Key objects indexed by kid
// Find the kid from the token header
$header = json_decode(base64_url_decode(explode('.', $token)[0]), true);
$key = $keyRing[$header['kid']] ?? null;
if (!$key) {
throw new Exception('unknown kid');
}
$decoded = JWT::decode($token, $key); // Key already carries the algorithm
JWK::parseKeySet converts the JWKS into an array of Key objects indexed by kid. Cache the result with a TTL. The fetch is the only network call — the token itself never leaves your server.
tymon/jwt-auth (Laravel)
config/jwt.php (published via php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"):
return [
'secret' => env('JWT_SECRET'),
'algo' => 'HS256',
'ttl' => 15, // access token minutes
'refresh_ttl' => 20160, // refresh token minutes (14 days)
'required_claims' => ['iss', 'iat', 'exp', 'sub', 'jti'],
'lock_subject' => true,
];
Protect routes with the jwt.auth middleware, issue tokens with JWTAuth::fromUser($user), and verify with JWTAuth::parseToken()->authenticate(). tymon/jwt-auth handles iss, iat, exp, sub, jti validation per the required_claims config.
Continue reading
- JWT decoder → - paste a token to decode and verify it in your browser
- JWT Signing Algorithms → - HS256, RS256, ES256, EdDSA with key sizes and code
- Registered Claims Reference → - iss, sub, aud, exp, jti, and every RFC 7519 claim
- JWT Security Vulnerabilities → - alg:none, algorithm confusion, kid injection, and every fix
- Verify JWT in Node.js → - jsonwebtoken and jose code
- Verify JWT in Java → - Spring Security and jjwt, including the aud mismatch fix