A strictly-typed PHP library for issuing and verifying JSON Web Tokens, with JWKS support
and no dependencies beyond ext-openssl and ext-json.
Tokens it issues are accepted by other implementations, and tokens from other implementations are accepted by it — the test suite verifies the examples printed in RFC 7515 itself, so interoperability is a checked property rather than an intention.
composer require flytachi/jwtRequires PHP 8.1+, ext-openssl and ext-json.
use Flytachi\Jwt\JWT;
use Flytachi\Jwt\Entity\JwtPayload;
use Flytachi\Jwt\Entity\PrivateKey;
use Flytachi\Jwt\Entity\PublicKey;
$token = JWT::encode(
new JwtPayload([
'iss' => 'https://example.com',
'sub' => 'user-12345',
'aud' => 'https://api.example.com',
'iat' => time(),
'exp' => time() + 3600,
]),
new PrivateKey('a-shared-secret', 'HS256'),
);
$payload = JWT::decode($token, [new PublicKey('a-shared-secret', 'HS256')]);
$userId = $payload->getClaim('sub');Everything that can go wrong throws one type, JWTException: a malformed string, an
unsupported algorithm, a key that does not match, a signature that does not verify, an
expiry that has passed. One catch covers the lot.
alg |
Family | Key material | Signature |
|---|---|---|---|
HS256 HS384 HS512 |
HMAC | shared secret string | hash width |
RS256 RS384 RS512 |
RSA | OpenSSL key pair | key width |
ES256 |
ECDSA, P-256 | OpenSSL key pair | 64 bytes |
ES384 |
ECDSA, P-384 | OpenSSL key pair | 96 bytes |
ES512 |
ECDSA, P-521 | OpenSSL key pair | 132 bytes |
ECDSA signatures travel as the raw pair R || S, each component padded to the width of
the curve (RFC 7515 §3.4) — not as the ASN.1 DER structure OpenSSL produces. The library
converts in both directions, so a token that leaves here is readable anywhere.
$token = JWT::encode(
new JwtPayload(['sub' => 'user-12345']),
new PrivateKey(openssl_pkey_get_private($pem), 'RS256', 'key-2024'),
);
$payload = JWT::decode($token, ['key-2024' => new PublicKey($publicKey, 'RS256')]);The algorithm travels with the key, and a token whose header names a different one is
refused. That is what closes the algorithm-confusion attack, where an RS256 token is
re-signed with the public key as an HMAC secret.
kid is optional under RFC 7515 §4.1.4, and one rule covers every algorithm:
| In the header | Passed to decode() |
Result |
|---|---|---|
kid present |
a key under that id | that key is used |
kid present |
no such id | refused |
no kid |
exactly one key | that key is used |
no kid |
several keys | refused |
A named key is used or none is — there is no falling back to another, so a token cannot
influence what verifies it. Several keys with no kid are refused rather than resolved by
array order.
$jwks = json_decode(file_get_contents('https://accounts.google.com/.well-known/jwks.json'), true);
$keys = JWK::parseKeySet($jwks); // ['kid' => PublicKey, …]
$payload = JWT::decode($token, $keys);JWK::parseKey() handles RSA, EC and oct keys and builds the PEM itself, with no
ASN.1 library involved. Each key remembers its own kid, so a key taken out of the set
still knows which one it is.
Keys carrying a kid land under it; keys without one are appended under a numeric index,
since kid is optional in a JWK too (RFC 7517 §4.5). A key the library cannot parse is
skipped rather than fatal — a provider mid-rotation may publish a type not everyone
handles, and refusing the whole set would stop verification for every token.
Cache the fetched set: parseKeySet() does no I/O and no caching of its own.
Verified: the signature; that the key's algorithm matches the token's alg; and the time
claims exp, nbf and iat, each widened by the $leeway seconds you pass.
JWT::decode($token, $keys, leeway: 30); // tolerate 30s of clock skewNot verified: iss, aud, sub, scopes, revocation. Those depend on your application,
and a library that guessed at them would be wrong quietly. Read them from the payload and
check them yourself:
if ($payload->getClaim('iss') !== 'https://example.com') {
throw new RuntimeException('Unexpected issuer.');
}- Quickstart — first token, signed and verified
- Algorithms — the nine, and what each needs
- Asymmetric keys — RSA, ECDSA, key selection
- Verifying with JWKS — provider key sets
- Security model — what is refused, and why
- API reference — every class and argument
composer test # phpunit
composer cs-check # phpcs, PSR-12
composer cs-fixThe suite covers all nine algorithms end to end, checks the bytes an ECDSA signature is made of rather than only the round trip, and verifies the RFC 7515 Appendix A examples — tokens this library did not produce.
MIT — see LICENSE.