Skip to content
ecdsa.com
PHP

web-token (JWT Framework)

Florent Morselli's JWT Framework (web-token), the complete JOSE implementation for PHP and Symfony: an AlgorithmManager that is the allow-list, separate claim checkers, every RFC 7518 signature algorithm plus EdDSA — but no ES256K.
License
MIT
Checked version
4.2.3 ·
Source
github.com/web-token/jwt-framework
Documentation
web-token.spomky-labs.com/

Compatibility at a glance

JOSE and JWT

JWS algorithms
HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512EdDSA

web-token implements JWS, JWE, JWK and JWK sets as separate components; web-token/jwt-library bundles them, and a Symfony bundle wires them into services. It is the PHP choice when you need JWE, JWK set management or the full algorithm list: 4.2.3 signed and verified HS, RS, PS and ES at 256/384/512 and EdDSA, including PS384/512 and ES512 that firebase/php-jwt lacks. There is no ES256K or fully specified Ed25519 algorithm class.

Algorithms are allowed explicitly: the AlgorithmManager passed to JWSVerifier is the allow-list, and a token in any other algorithm fails with The algorithm "RS256" is not supported. none exists as an algorithm class but is only accepted if you put it in the manager. An HS256 token forged with the EC key's PEM text failed even with HS256 allowed, because an EC JWK cannot be used as an HMAC key. ES256 with a P-384 key produced a signature that the library's own verifier rejected.

Verification and claim checking are separate steps: JWSVerifier::verifyWithKey() returned true for a token that had expired an hour earlier. Run a ClaimCheckerManager afterwards, list mandatory claims — exp is not mandatory otherwise — and pass a drift to ExpirationTimeChecker, which since version 4 also needs a PSR-20 clock; the sample shows the whole sequence.

ES256 signatures are 64 bytes of raw r‖s and a DER signature makes verifyWithKey() return false; the JWT debugger shows which one a token carries. Keys are JWK objects throughout; JWKFactory builds them from PEM, certificates or key parameters, and the JWKS builder produces key sets in the same format.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/web-token-jwt-framework/run.sh ran web-token (JWT Framework) 4.2.3 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

    HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512EdDSA

    HS, RS, PS and ES at 256/384/512 and EdDSA signed and verified; there are no ES256K or Ed25519 algorithm classes.

    Recorded by the check
    fact jws-algorithms = HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, EdDSA
    evidence jws-algorithms: ES256K: class Jose\Component\Signature\Algorithm\ES256K exists: false
    evidence jws-algorithms: Ed25519: class Jose\Component\Signature\Algorithm\Ed25519 exists: false
  • 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 makes verifyWithKey() return false.

    Recorded by the check
    fact jws-der-signature = rejected
    evidence jws-der-signature: false
  • Algorithm allow-list

    required

    The AlgorithmManager is the allow-list: an RS256 token against a manager holding only ES256 failed with The algorithm "RS256" is not supported.

    Recorded by the check
    fact jwt-algorithm-allowlist = required
    evidence jwt-algorithm-allowlist: RS256 token with an AlgorithmManager holding only ES256: InvalidArgumentException: The algorithm "RS256" is not supported.
  • Unsigned tokens (alg none)

    opt-in

    An unsigned token fails with The algorithm "none" is not supported. unless the None algorithm class is added to the manager.

    Recorded by the check
    fact jwt-alg-none = opt-in
    evidence jwt-alg-none: with an ES256 AlgorithmManager: InvalidArgumentException: The algorithm "none" is not supported.; a None algorithm class exists: true
  • Public key used as an HMAC secret

    rejected

    With ES256 and HS256 both allowed, an HS256 token keyed with the PEM text failed against the EC JWK.

    Recorded by the check
    fact jwt-hmac-public-key = rejected
    evidence jwt-hmac-public-key: AlgorithmManager [ES256, HS256] with the EC JWK: false
  • Curve checked against alg

    enforced

    Signing ES256 with a P-384 key succeeded, but the library's verifier rejected the result.

    Recorded by the check
    fact jwt-ecdsa-curve-check = enforced
    evidence jwt-ecdsa-curve-check: ES256 with a P-384 key, signing: ok; verifying: false
  • Claims validated while decoding

    no

    JWSVerifier::verifyWithKey() returned true for a token expired an hour earlier; claims are checked by a separate ClaimCheckerManager.

    Recorded by the check
    fact jwt-claims-validated-on-decode = no
    evidence jwt-claims-validated-on-decode: JWSVerifier on a token expired an hour ago: ok
  • Default clock skew

    0s

    ExpirationTimeChecker allows no drift unless given one: a token expired three seconds earlier was rejected.

    Recorded by the check
    fact jwt-clock-skew = 0s
  • Expiry claim required

    no

    Without exp in the mandatory claims, a token lacking it passed; listing it raised MissingMandatoryClaimException.

    Recorded by the check
    fact jwt-exp-required = no
    evidence jwt-exp-required: ExpirationTimeChecker without exp: ok; with exp listed as mandatory: Jose\Component\Checker\MissingMandatoryClaimException: The following claims are mandatory: exp.

Usage

Sign with ES256 through an AlgorithmManager allow-list, verify the signature, then check exp and iss with claim checkers.
<?php
require 'vendor/autoload.php';

use Jose\Component\Checker\{ClaimCheckerManager, ExpirationTimeChecker, IssuerChecker};
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\Algorithm\ES256;
use Jose\Component\Signature\{JWSBuilder, JWSVerifier};
use Jose\Component\Signature\Serializer\CompactSerializer;

$jwk = JWKFactory::createECKey('P-256');
$algorithms = new AlgorithmManager([new ES256()]); // the allow-list
$payload = json_encode(['sub' => 'user-42', 'iss' => 'https://auth.example.com', 'exp' => time() + 600]);
$jws = (new JWSBuilder($algorithms))->create()->withPayload($payload)->addSignature($jwk, ['alg' => 'ES256'])->build();
$token = (new CompactSerializer())->serialize($jws, 0);

// The signature check does not look at claims: run the claim checkers as a second step.
$parsed = (new CompactSerializer())->unserialize($token);
$valid = (new JWSVerifier($algorithms))->verifyWithKey($parsed, $jwk->toPublic(), 0);
$clock = new class implements Psr\Clock\ClockInterface { public function now(): DateTimeImmutable { return new DateTimeImmutable(); } };
$claims = json_decode($parsed->getPayload(), true);
(new ClaimCheckerManager([new ExpirationTimeChecker($clock, 30), new IssuerChecker(['https://auth.example.com'])]))->check($claims, ['exp', 'iss']);
echo 'valid: ', var_export($valid, true), ', 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 web-token/jwt-library:4.2.3
Command
php usage.php
Output must match (regular expression)
valid: true, sub: user-42

Libraries