composer require firebase/php-jwt:7.2.1 <?phprequire __DIR__ . '/vendor/autoload.php'; use Firebase\JWT\JWT;use Firebase\JWT\Key; JWT::$leeway = 30; // seconds of clock skew allowed for exp, nbf and iat (default 0) // The Key binds the PEM to ES256: a token whose header says anything else is rejected$key = new Key(file_get_contents('pub.pem'), 'ES256');$claims = JWT::decode(trim(file_get_contents('token.txt')), $key); // signature, exp, nbf, iat // php-jwt does not check iss or aud: that is the caller's jobif (($claims->iss ?? null) !== 'https://ecdsa.com') { throw new UnexpectedValueException('unexpected issuer');}echo "valid ES256 token for {$claims->sub}, expires ", gmdate('c', $claims->exp), "\n"; How it works
new Key($pem, 'ES256')binds the public key to one algorithm. A token whose header names another algorithm fails withUnexpectedValueException: Incorrect key for this algorithmbefore the signature is checked.JWT::decodesplits the token, verifies the signature over theheader.payloadtext (re-encoding the 64-byter‖sas DER foropenssl_verify) and returns the payload as astdClass.- It throws
SignatureInvalidExceptionfor a bad signature,ExpiredExceptionafterexp, andBeforeValidExceptionbeforenbf(or a futureiat); the run triggers the first three cases. JWT::$leewayis a static setting for clock skew, 0 by default; set it once at bootstrap rather than per call.
Gotchas
- php-jwt never looks at
issoraud. Compare them yourself afterdecode, as the snippet does for the issuer — otherwise a valid token from another issuer that shares the key, or meant for another API, is accepted. - A token without
exppasses: the library checks expiry only when the claim exists. Requireexpexplicitly if your tokens must expire. - Code written for php-jwt 5 passes the PEM as a plain string. Version 7 then fails with the misleading
"kid" empty, unable to lookup correct key— the fix is not akidbut wrapping the PEM innew Key($pem, 'ES256'), which is also what prevents algorithm confusion. - For keys published as a JWKS, use
JWK::parseKeySet(orCachedKeySet) and let the token'skidselect the key; a mismatch raises thekiderror rather than a signature error.