Skip to content
ecdsa.com
PHP

lcobucci/jwt

Luís Cobucci's lcobucci/jwt, the JWT library behind Laravel Passport and many PHP frameworks: every check is an explicit constraint, assert() refuses to run without one, and key lengths are enforced — but there is no RSA-PSS.
License
BSD-3-Clause
Checked version
5.6.1 ·
Source
github.com/lcobucci/jwt
Documentation
lcobucci-jwt.readthedocs.io/en/stable/

Compatibility at a glance

JOSE and JWT

JWS algorithms
HS256HS384HS512RS256RS384RS512ES256ES384ES512EdDSA

lcobucci/jwt separates parsing, signing and validation: a Configuration holds the signer and keys, parser()->parse() reads a token without checking anything, and validator()->assert() applies the constraints you pass. Calling assert() with no constraints throws NoConstraintsGiven, which makes a validation that checks nothing hard to write by accident.

SignedWith pins the signer and key: an RS256 token and an HS256 token forged with the EC key's PEM text both failed with Token signer mismatch. Unsigned tokens cannot even be parsed (The JWT string is missing the Signature part). Key sizes are enforced at signing time — ES256 refused a P-384 key (The length of the provided key is different than 256 bits) and HS256 refused a 40-bit key — which keeps malformed tokens from being produced at all.

Time checks are constraints too, so they only run when listed: a token that had expired an hour earlier passed SignedWith alone. LooseValidAt checks exp, nbf and iat when present and accepted a token without exp; StrictValidAt requires all three — a token without iat failed with "Issued At" claim missing, and one with iat and exp but no nbf with "Not Before" claim missing. Both take an optional leeway interval, zero by default. The sample sets all three claims and asserts signature, issuer and strict validity in one call.

5.6.1 supports HS, RS and ES at 256/384/512 and EdDSA; there are no PS* signers and no ES256K, so tokens from issuers using RSA-PSS need web-token. ES256 signatures are 64 bytes of raw r‖s, and a DER signature fails with Token signature mismatch; the JWT debugger shows which encoding a token carries.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/lcobucci-jwt/run.sh ran lcobucci/jwt 5.6.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

    HS256HS384HS512RS256RS384RS512ES256ES384ES512EdDSA

    HS, RS and ES at 256/384/512 and EdDSA signed and verified; there are no RSA-PSS or ES256K signer classes.

    Recorded by the check
    fact jws-algorithms = HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512, EdDSA
    evidence jws-algorithms: PS256 signer class Lcobucci\JWT\Signer\Rsa\Pss\Sha256 exists: false
    evidence jws-algorithms: ES256K signer class Lcobucci\JWT\Signer\Ecdsa\Sha256K 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 fails with Token signature mismatch.

    Recorded by the check
    fact jws-der-signature = rejected
    evidence jws-der-signature: Lcobucci\JWT\Validation\RequiredConstraintsViolated: The token violates some mandatory constraints, details: - Token signature mismatch
  • Algorithm allow-list

    required

    SignedWith pins the signer: an RS256 token against an ES256 signer failed with Token signer mismatch, and assert() without constraints throws NoConstraintsGiven.

    Recorded by the check
    fact jwt-algorithm-allowlist = required
    evidence jwt-algorithm-allowlist: RS256 token with SignedWith(ES256 signer, EC key): Lcobucci\JWT\Validation\RequiredConstraintsViolated: The token violates some mandatory constraints, details: - Token signer mismatch
    evidence jwt-algorithm-allowlist: assert() with no constraints: Lcobucci\JWT\Validation\NoConstraintsGiven: No constraint given.
  • Unsigned tokens (alg none)

    rejected

    An unsigned token cannot be parsed: The JWT string is missing the Signature part.

    Recorded by the check
    fact jwt-alg-none = rejected
    evidence jwt-alg-none: parsing an unsigned token: Lcobucci\JWT\Token\InvalidTokenStructure: The JWT string is missing the Signature part
  • Public key used as an HMAC secret

    rejected

    An HS256 token keyed with the PEM text failed SignedWith for the ES256 signer with Token signer mismatch.

    Recorded by the check
    fact jwt-hmac-public-key = rejected
    evidence jwt-hmac-public-key: SignedWith(ES256 signer, EC key): Lcobucci\JWT\Validation\RequiredConstraintsViolated: The token violates some mandatory constraints, details: - Token signer mismatch
  • Curve checked against alg

    enforced

    The ES256 signer refused a P-384 key: The length of the provided key is different than 256 bits, 384 bits provided.

    Recorded by the check
    fact jwt-ecdsa-curve-check = enforced
    evidence jwt-ecdsa-curve-check: ES256 signer with a P-384 key: Lcobucci\JWT\Signer\InvalidKeyProvided: The length of the provided key is different than 256 bits, 384 bits provided
  • Claims validated while decoding

    no

    Parsing and SignedWith do not check time: a token expired an hour earlier passed.

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

    0s

    LooseValidAt and StrictValidAt allow no leeway unless given an interval.

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

    no

    LooseValidAt accepted a token without exp; StrictValidAt requires iat, nbf and exp.

    Recorded by the check
    fact jwt-exp-required = no
    evidence jwt-exp-required: LooseValidAt without exp: ok; StrictValidAt: Lcobucci\JWT\Validation\RequiredConstraintsViolated: The token violates some mandatory constraints, details: - "Issued At" claim missing
    evidence jwt-exp-required: StrictValidAt with iat and exp but no nbf: Lcobucci\JWT\Validation\RequiredConstraintsViolated: The token violates some mandatory constraints, details: - "Not Before" claim missing
  • Short HMAC secret

    rejected

    HS256 refused a 5-byte key: Key provided is shorter than 256 bits, only 40 bits provided.

    Recorded by the check
    fact jwt-hmac-short-secret = rejected
    evidence jwt-hmac-short-secret: 5-byte HS256 key: Lcobucci\JWT\Signer\InvalidKeyProvided: Key provided is shorter than 256 bits, only 40 bits provided

Usage

Issue an ES256 token and assert signature, issuer and strict validity with 30 seconds of leeway.
<?php
require 'vendor/autoload.php';

use Lcobucci\Clock\SystemClock;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Ecdsa\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Validation\Constraint\{IssuedBy, SignedWith, StrictValidAt};

$key = openssl_pkey_new(['private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => 'prime256v1']);
openssl_pkey_export($key, $privatePem);
$private = InMemory::plainText($privatePem);
$public = InMemory::plainText(openssl_pkey_get_details($key)['key']);

$config = Configuration::forAsymmetricSigner(new Sha256(), $private, $public);
$now = new DateTimeImmutable();
$token = $config->builder()->issuedBy('https://auth.example.com')->relatedTo('user-42')
    ->issuedAt($now)->canOnlyBeUsedAfter($now)->expiresAt($now->modify('+10 minutes'))
    ->getToken($config->signer(), $config->signingKey());

// Every check is an explicit constraint; StrictValidAt requires iat, nbf and exp.
$parsed = $config->parser()->parse($token->toString());
$config->validator()->assert($parsed,
    new SignedWith($config->signer(), $config->verificationKey()),
    new IssuedBy('https://auth.example.com'),
    new StrictValidAt(SystemClock::fromUTC(), new DateInterval('PT30S')));
echo 'sub: ', $parsed->claims()->get('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 lcobucci/jwt:5.6.1 lcobucci/clock:3.6.0
Command
php usage.php
Output must match (regular expression)
sub: user-42

Libraries