Skip to content
ecdsa.com
PHP

firebase/php-jwt

firebase/php-jwt, the most installed JWT package for PHP: a Key object that pins the algorithm, short HMAC keys refused, but no ES512 or PS384/512, no issuer or audience checks, and ES256 accepted from P-384 keys.
License
BSD-3-Clause
Checked version
7.2.1 ·
Source
github.com/firebase/php-jwt
Documentation
github.com/firebase/php-jwt#readme

Compatibility at a glance

JOSE and JWT

JWS algorithms
HS256HS384HS512RS256RS384RS512ES256ES384ES256KEdDSA

php-jwt encodes and decodes tokens with JWT::encode() and JWT::decode() on top of the openssl extension and libsodium. Since version 6 a Key object — key material plus algorithm — is mandatory, so the algorithm comes from your code, not from the token: an RS256 token and an HS256 token forged with the EC key's PEM text both failed against new Key($pem, 'ES256') with Incorrect key for this algorithm, and an unsigned token fails with Algorithm not supported. The guard holds only while the application builds the Key; new Key($pem, 'HS256') verified the forged token.

Version 7.2.1 handles HS256/384/512, RS256/384/512, ES256, ES384, ES256K and EdDSA. ES512 is not supported (Algorithm not supported), PS256 needs phpseclib/phpseclib installed, and PS384 and PS512 are not available at all — tokens from issuers using those algorithms need web-token or lcobucci/jwt. HMAC keys shorter than the hash are rejected (Provided key is too short).

ES256 is not bound to P-256: a token signed with a P-384 key under an ES256 header was produced and accepted by the library itself, though other libraries refuse it. ES256 signatures are converted to the 64-byte raw r‖s JWS requires; a DER signature fails with Signature verification failed. Check a token's signature in the JWT debugger.

Claims handling is minimal. exp, nbf and iat are checked only when present, JWT::$leeway is a global static defaulting to 0, and there is no issuer or audience validation at all — the sample checks exp and iss itself. For JWKS-based verification, JWK::parseKeySet() turns a key set into Key objects; inspect a provider's set with the JWKS checker.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/firebase-php-jwt/run.sh ran firebase/php-jwt 7.2.1 in the container image php:8.5.11-cli-trixie on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • JWS algorithms

    HS256HS384HS512RS256RS384RS512ES256ES384ES256KEdDSA

    HS256/384/512, RS256/384/512, ES256, ES384, ES256K and EdDSA signed and verified; ES512 and PS384/512 are not supported, and PS256 requires phpseclib.

    Recorded by the check
    fact jws-algorithms = HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES256K, EdDSA
    evidence jws-algorithms: PS256: DomainException: phpseclib/phpseclib is required for PS256 support
    evidence jws-algorithms: PS384: DomainException: Algorithm not supported
    evidence jws-algorithms: PS512: DomainException: Algorithm not supported
    evidence jws-algorithms: ES512: DomainException: Algorithm not supported
    evidence jws-algorithms: Ed25519: no key family
  • ES256 signature encoding

    raw

    ES256 signatures are 64 bytes of raw r‖s.

    Recorded by the check
    fact jws-ecdsa-encoding = raw
  • DER signature inside a JWS

    rejected

    A DER-encoded ES256 signature fails with SignatureInvalidException: Signature verification failed.

    Recorded by the check
    fact jws-der-signature = rejected
    evidence jws-der-signature: Firebase\JWT\SignatureInvalidException: Signature verification failed
  • Algorithm allow-list

    required

    decode() requires a Key object with an algorithm; a bare key string fails, and an RS256 token against new Key($ecPem, 'ES256') fails with Incorrect key for this algorithm.

    Recorded by the check
    fact jwt-algorithm-allowlist = required
    evidence jwt-algorithm-allowlist: RS256 token against new Key(EC PEM, 'ES256'): UnexpectedValueException: Incorrect key for this algorithm
    evidence jwt-algorithm-allowlist: decode() with a bare key string: UnexpectedValueException: "kid" empty, unable to lookup correct key
  • Unsigned tokens (alg none)

    rejected

    An unsigned token fails with Algorithm not supported.

    Recorded by the check
    fact jwt-alg-none = rejected
    evidence jwt-alg-none: UnexpectedValueException: Algorithm not supported
  • Public key used as an HMAC secret

    rejected

    An HS256 token keyed with the PEM text failed against the ES256 Key, but new Key($pem, 'HS256') verified it.

    Recorded by the check
    fact jwt-hmac-public-key = rejected
    evidence jwt-hmac-public-key: Key(PEM, 'ES256'): UnexpectedValueException: Incorrect key for this algorithm
    evidence jwt-hmac-public-key: Key(PEM, 'HS256') built by the application: ok
  • Curve checked against alg

    not-enforced

    A token signed under ES256 with a P-384 key was produced and verified successfully.

    Recorded by the check
    fact jwt-ecdsa-curve-check = not-enforced
    evidence jwt-ecdsa-curve-check: ok
  • Default clock skew

    0s

    JWT::$leeway is 0: a token expired three seconds earlier was rejected.

    Recorded by the check
    fact jwt-clock-skew = 0s
    evidence jwt-clock-skew: JWT::$leeway = 0
  • Expiry claim required

    no

    A token without exp decoded successfully.

    Recorded by the check
    fact jwt-exp-required = no
  • Short HMAC secret

    rejected

    A 5-byte HS256 key fails with Provided key is too short.

    Recorded by the check
    fact jwt-hmac-short-secret = rejected
    evidence jwt-hmac-short-secret: 5-byte HS256 key: DomainException: Provided key is too short

Usage

Issue an ES256 token, decode it with a Key that pins the algorithm and 30 seconds of leeway, and check exp and iss explicitly.
<?php
require 'vendor/autoload.php';

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

$private = openssl_pkey_new(['private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => 'prime256v1']);
$publicPem = openssl_pkey_get_details($private)['key'];

$token = JWT::encode(['sub' => 'user-42', 'iss' => 'https://auth.example.com', 'exp' => time() + 600], $private, 'ES256');

// The Key object pins the algorithm; leeway is a static setting, 0 by default.
JWT::$leeway = 30;
$claims = JWT::decode($token, new Key($publicPem, 'ES256'));

// php-jwt checks exp/nbf/iat only when present and never checks iss or aud: do it here.
if (!isset($claims->exp) || $claims->iss !== 'https://auth.example.com') {
    throw new UnexpectedValueException('unexpected claims');
}
echo 'sub: ', $claims->sub, "\n";

The snippet harness runs every sample verbatim in the container image php:8.5.11-cli-trixie, and the run fails unless the output matches.

Setup
curl -fsSL -o /tmp/composer.phar https://getcomposer.org/download/2.10.3/composer.phar && echo '7a2d379d5b8ffdaa028580ef26494c36d2feef4b178d3dd1473a4dbc5e17c8d6 /tmp/composer.phar' | sha256sum -c - >/dev/null && apt-get -qq update >/dev/null && apt-get -qq install -y unzip >/dev/null 2>&1 && COMPOSER_ALLOW_SUPERUSER=1 php /tmp/composer.phar require --quiet firebase/php-jwt:7.2.1
Command
php usage.php
Output must match (regular expression)
sub: user-42

Libraries