Saltar al contenido
ecdsa.com
PHP

lcobucci/jwt

lcobucci/jwt, de Luís Cobucci, la biblioteca JWT detrás de Laravel Passport y muchos frameworks de PHP: cada comprobación es una restricción explícita, assert() se niega a ejecutarse sin ninguna y se validan las longitudes de clave, pero no hay RSA-PSS.
Licencia
BSD-3-Clause
Versión comprobada
5.6.1 ·
Código fuente
github.com/lcobucci/jwt
Documentación
lcobucci-jwt.readthedocs.io/en/stable/

Compatibilidad de un vistazo

JOSE y JWT

Algoritmos JWS
HS256HS384HS512RS256RS384RS512ES256ES384ES512EdDSA

lcobucci/jwt separa el análisis, la firma y la validación: una Configuration contiene el firmante y las claves, parser()->parse() lee un token sin comprobar nada y validator()->assert() aplica las restricciones que le pasas. Llamar a assert() sin restricciones lanza NoConstraintsGiven, lo que hace difícil escribir por accidente una validación que no compruebe nada.

SignedWith fija el firmante y la clave: un token RS256 y un token HS256 falsificado con el texto PEM de la clave EC fallaron ambos con Token signer mismatch. Los tokens sin firmar ni siquiera se pueden analizar (The JWT string is missing the Signature part). Los tamaños de clave se validan al firmar —ES256 rechazó una clave P-384 (The length of the provided key is different than 256 bits) y HS256 rechazó una clave de 40 bits—, lo que impide que lleguen a producirse tokens mal formados.

Las comprobaciones de tiempo también son restricciones, así que solo se ejecutan si se incluyen: un token que había caducado una hora antes superó SignedWith por sí solo. LooseValidAt comprueba exp, nbf e iat cuando están presentes y aceptó un token sin exp; StrictValidAt exige los tres: un token sin iat falló con "Issued At" claim missing, y uno con iat y exp pero sin nbf, con "Not Before" claim missing. Ambas admiten un intervalo de tolerancia opcional, cero por defecto. El ejemplo fija los tres claims y comprueba la firma, el emisor y la validez estricta en una sola llamada.

La 5.6.1 admite HS, RS y ES de 256/384/512 y EdDSA; no hay firmantes PS* ni ES256K, así que los tokens de emisores que usan RSA-PSS necesitan web-token. Las firmas ES256 son 64 bytes de r‖s en bruto, y una firma DER falla con Token signature mismatch; el decodificador de JWT muestra qué codificación lleva un token.

Datos y las comprobaciones que los respaldan

Nada de lo que sigue está copiado de la documentación. La comprobación content/libraries/verify/lcobucci-jwt/run.sh ejecutó lcobucci/jwt 5.6.1 en la imagen de contenedor php:8.5.11-cli-trixie el 20 de octubre de 2026; la página debe coincidir con lo que imprimió, y cada dato cita esas líneas.

  • Algoritmos JWS

    HS256HS384HS512RS256RS384RS512ES256ES384ES512EdDSA

    Con HS, RS y ES de 256/384/512 y EdDSA se firmó y se verificó; no hay clases de firmante para RSA-PSS ni para ES256K.

    Registrado por la comprobación
    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
  • Codificación de la firma ES256

    raw

    Las firmas ES256 son 64 bytes de r‖s en bruto.

    Registrado por la comprobación
    fact jws-ecdsa-encoding = raw
  • Firma DER dentro de un JWS

    rejected

    Una firma ES256 codificada en DER falla con Token signature mismatch.

    Registrado por la comprobación
    fact jws-der-signature = rejected
    evidence jws-der-signature: Lcobucci\JWT\Validation\RequiredConstraintsViolated: The token violates some mandatory constraints, details: - Token signature mismatch
  • Lista de algoritmos permitidos

    required

    SignedWith fija el firmante: un token RS256 contra un firmante ES256 falló con Token signer mismatch, y assert() sin restricciones lanza NoConstraintsGiven.

    Registrado por la comprobación
    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.
  • Tokens sin firma (alg none)

    rejected

    Un token sin firmar no se puede analizar: The JWT string is missing the Signature part.

    Registrado por la comprobación
    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
  • Clave pública usada como secreto HMAC

    rejected

    Un token HS256 con el texto PEM como clave no superó SignedWith con el firmante ES256 y falló con Token signer mismatch.

    Registrado por la comprobación
    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
  • Curva cotejada con alg

    enforced

    El firmante ES256 rechazó una clave P-384: The length of the provided key is different than 256 bits, 384 bits provided.

    Registrado por la comprobación
    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 validados al decodificar

    no

    Ni el análisis ni SignedWith comprueban el tiempo: pasó un token caducado una hora antes.

    Registrado por la comprobación
    fact jwt-claims-validated-on-decode = no
    evidence jwt-claims-validated-on-decode: SignedWith only, token expired an hour ago: ok
  • Desfase de reloj predeterminado

    0s

    LooseValidAt y StrictValidAt no admiten ninguna tolerancia salvo que se les pase un intervalo.

    Registrado por la comprobación
    fact jwt-clock-skew = 0s
  • Claim de caducidad obligatorio

    no

    LooseValidAt aceptó un token sin exp; StrictValidAt exige iat, nbf y exp.

    Registrado por la comprobación
    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
  • Secreto HMAC corto

    rejected

    HS256 rechazó una clave de 5 bytes: Key provided is shorter than 256 bits, only 40 bits provided.

    Registrado por la comprobación
    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

Uso

Emite un token ES256 y comprueba la firma, el emisor y la validez estricta con 30 segundos de tolerancia.
<?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";

El arnés de pruebas de fragmentos de código ejecuta cada ejemplo tal cual en la imagen de contenedor php:8.5.11-cli-trixie, y la ejecución falla si la salida no coincide.

Configuración
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
Comando
php usage.php
La salida debe coincidir (expresión regular)
sub: user-42

Bibliotecas