kelvinmo/simplejwt
SimpleJWT is a PHP library for JSON Web Tokens, signatures, and encryption. It supports JWK and COSE keys, HMAC/RSA/ECDSA/EdDSA signatures, key management, and AES encryption. Requires PHP 8+ with gmp, hash, openssl, and sodium for EdDSA/X25519.
Installation:
composer require kelvinmo/simplejwt
Ensure your composer.json includes PHP 8.0+ and extensions: gmp, hash, openssl, and sodium (for EdDSA/X25519).
Pin to
^1.1.3or later. v1.1.3 adds validation of the decompressed length for tokens usingzip: DEF(JWE compression), closing a decompression-bomb / memory-exhaustion vector. Treat anything below 1.1.3 as unsafe for any endpoint that accepts JWEs from untrusted clients.
First Use Case: Generate a JWT for API authentication:
use SimpleJWT\Keys\KeySet;
use SimpleJWT\JWT;
$keySet = KeySet::createFromSecret('your-secret-key');
$headers = ['alg' => 'HS256', 'typ' => 'JWT'];
$claims = ['sub' => 'user123', 'exp' => time() + 3600];
$jwt = new JWT($headers, $claims);
$token = $jwt->encode($keySet);
Where to Look First:
HMAC Secrets:
$keySet = KeySet::createFromSecret('secret-key');
Use for stateless APIs (e.g., API gateways).
Asymmetric Keys (RSA/ECDSA):
$privateKey = new \SimpleJWT\Keys\RSAKey(file_get_contents('private.pem'), 'pem');
$publicKey = new \SimpleJWT\Keys\RSAKey(file_get_contents('public.pem'), 'pem');
$keySet->add($privateKey, true); // Auto-generate kid
$keySet->add($publicKey);
Ideal for server-to-server or user authentication with public/private pairs.
Key Rotation:
$keySet->add($newPrivateKey, true); // Add new key with kid
$keySet->remove($oldPrivateKey); // Remove old key
Store keys in environment variables or a secrets manager (e.g., AWS Secrets Manager).
JWT Creation:
$jwt = new JWT(['alg' => 'RS256'], ['sub' => 'user123', 'iat' => time()]);
$token = $jwt->encode($keySet);
HS256 for simplicity, RS256/ES256 for security.kid/iat with $jwt->encode($keySet, false).JWT Validation:
try {
$decoded = JWT::decode($token, $keySet, 'RS256');
$claims = $decoded->getClaims();
} catch (\SimpleJWT\InvalidTokenException $e) {
// Handle invalid token (expired, tampered, etc.)
}
alg against a whitelist (e.g., ['HS256', 'RS256']).JWT::deserialise() for debugging (no validation).JWE (Encrypted Tokens):
$jwe = new \SimpleJWT\JWE(['alg' => 'PBES2-HS256+A128KW', 'enc' => 'A128CBC-HS256'], 'secret-payload');
$encrypted = $jwe->encrypt($keySet);
$decrypted = \SimpleJWT\JWE::decrypt($encrypted, $keySet, 'PBES2-HS256+A128KW');
Use for confidential claims (e.g., PII in tokens).
Compressed JWEs (zip: DEF):
$jwe = new \SimpleJWT\JWE(
['alg' => 'A256KW', 'enc' => 'A256GCM', 'zip' => 'DEF'],
json_encode($largeClaims)
);
$encrypted = $jwe->encrypt($keySet);
kelvinmo/simplejwt 1.1.3, decompression validates the inflated length before handing data back, so a hostile token can no longer expand into gigabytes of memory.zip on endpoints where you control (or trust) the issuer — and keep the library patched, since the protection lives in the library, not your code.Middleware for JWT Validation:
use SimpleJWT\JWT;
class AuthenticateJWT
{
public function handle($request, Closure $next)
{
$token = $request->bearerToken();
if (!$token) abort(401);
try {
$decoded = JWT::decode($token, $this->keySet, 'HS256');
$request->merge(['user' => $decoded->getClaims()]);
} catch (\Exception $e) {
abort(401);
}
return $next($request);
}
}
Service Provider for Key Management:
class JWTServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton('jwt.keySet', function () {
return KeySet::createFromSecret(config('jwt.secret'));
});
}
}
API Resource Responses:
return response()->json([
'data' => $user,
'access_token' => (new JWT(['alg' => 'HS256'], [
'sub' => $user->id,
'exp' => time() + 3600
])->encode($this->keySet)),
]);
Keep the Library Current in CI:
# composer.json
"require": { "kelvinmo/simplejwt": "^1.1.3" }
Add composer audit (or Dependabot/Renovate) to your pipeline — patch releases of this library have carried security fixes.
Algorithm Mismatch:
InvalidTokenException with SIGNATURE_VERIFICATION_ERROR.alg in the token matches the one passed to decode().
Example: JWT::decode($token, $keySet, 'HS256') must match the token’s alg.Key ID (kid) Handling:
kid is missing or mismatched.kid in keys or use KeySet::add($key, true) to auto-generate it.SimpleJWT\Keys\Key::getKeyId() to verify kid values.Clock Skew:
nbf (Not Before) claims and adjust server time or add a leeway buffer:
$claims['exp'] = time() + 3600;
$claims['nbf'] = time() - 60; // Allow 1-minute leeway
PEM vs. JWK:
RSAKey fails to load from PEM files.$key = new \SimpleJWT\Keys\RSAKey(file_get_contents('private.pem'), 'pem');
PHP Extensions:
SodiumException or RuntimeException during EdDSA/X25519 operations.sodium extension in php.ini or Dockerfile:
extension=sodium
Compressed Tokens (zip: DEF) — Decompression Bombs:
zip header set to DEF can be a few hundred bytes on the wire but inflate to hundreds of MB, exhausting memory_limit and taking the worker down. Before v1.1.3, the library did not bound the decompressed length.kelvinmo/simplejwt ^1.1.3, which validates the length of the decompressed data. Verify with:
composer show kelvinmo/simplejwt | grep versions
zip headers on inbound tokens if your issuer never sets them:
$parsed = \SimpleJWT\JWE::deserialise($token); // inspect header only
if (($parsed['protected']['zip'] ?? null) !== null) {
abort(400);
}
post_max_size, middleware max: validation rule) so oversized ciphertexts never reach the library.Deserialize Without Validation:
$result = JWT::deserialise($token);
print_r($result['claims']); // Inspect claims
print_r($result['signatures']); // Inspect signatures
Key Validation:
try {
$keySet->validateKey($token, 'HS256');
} catch (\SimpleJWT\KeyException $e) {
// Key is invalid or unsupported
}
Algorithm Support:
SimpleJWT\Algorithms.PBES2-HS256+A128KW requires openssl and gmp.Compression Failures:
zip: DEF payloads surface as decode/decrypt exceptions — wrap decrypt in try/catch (\Throwable $e) and log the exception class, since a decompression-limit rejection looks like any other invalid-token error to the caller.alg, enc, zip) for triage.KeySet Caching:
$keySet = Cache::remember('jwt.keys', 3600, function () {
return KeySet::createFromSecret(config('jwt.secret'));
});
Avoid Re-encoding:
JWT objects for multiple encodes if claims/headers are static.JWE Optimization:
A256GCM) for performance-critical paths.zip: DEF costs CPU on both ends; use it only for genuinely large, repetitive payloads — and only on ^1.1.3.Custom Claims Validation:
$decoded = JWT::decode($token, $keySet, 'HS256');
if ($decoded->getClaim('role') !== 'admin') {
abort(403);
}
Multi-Recipient JWE:
$jwe = new \SimpleJWT\JWE(['alg' => 'ECDH-ES+A128KW', 'enc' => 'A128GCM'], 'secret');
$encrypted = $jwe->encrypt($keySet);
Add one recipient key per party; each recipient decrypts with their own private key. Keep recipient keys in a rotating key set so a compromised recipient can be dropped without reissuing every token.
Pre-flight Header Inspection:
// Gate on header values (alg, enc, zip) before doing any crypto work.
$header = \SimpleJWT\JWE::deserialise($token)['protected'];
abort_unless($header['alg'] === 'A256KW' && $header['enc'] === 'A256GCM', 400);
Rejecting unsupported or unexpected alg/enc/zip values up front is the cheapest defence you can add — it also lets you enforce the v1.1.3 protections on your own terms.
How can I help you explore Laravel packages today?