779 lines
27 KiB
PHP
779 lines
27 KiB
PHP
<?php
|
|
|
|
namespace Firebase\JWT;
|
|
|
|
use DomainException;
|
|
use InvalidArgumentException;
|
|
use PHPUnit\Framework\TestCase;
|
|
use stdClass;
|
|
use TypeError;
|
|
use UnexpectedValueException;
|
|
|
|
class JWTTest extends TestCase
|
|
{
|
|
private Key $hmacKey;
|
|
|
|
public function setUp(): void
|
|
{
|
|
$this->hmacKey = $this->generateHmac256();
|
|
}
|
|
|
|
public function testUrlSafeCharacters()
|
|
{
|
|
$encoded = JWT::encode(['message' => 'f?'], $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
$expected = new stdClass();
|
|
$expected->message = 'f?';
|
|
$this->assertEquals($expected, JWT::decode($encoded, $this->hmacKey));
|
|
}
|
|
|
|
public function testMalformedUtf8StringsFail()
|
|
{
|
|
$this->expectException(DomainException::class);
|
|
JWT::encode(['message' => pack('c', 128)], $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
}
|
|
|
|
public function testInvalidKeyOpensslSignFail()
|
|
{
|
|
$this->expectException(DomainException::class);
|
|
JWT::sign('message', 'invalid key', 'openssl');
|
|
}
|
|
|
|
public function testMalformedJsonThrowsException()
|
|
{
|
|
$this->expectException(DomainException::class);
|
|
JWT::jsonDecode('this is not valid JSON string');
|
|
}
|
|
|
|
public function testExpiredToken()
|
|
{
|
|
$this->expectException(ExpiredException::class);
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'exp' => time() - 20, // time in the past
|
|
];
|
|
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
JWT::decode($encoded, $this->hmacKey);
|
|
}
|
|
|
|
public function testBeforeValidTokenWithNbf()
|
|
{
|
|
$this->expectException(BeforeValidException::class);
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'nbf' => time() + 20, // time in the future
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
JWT::decode($encoded, $this->hmacKey);
|
|
}
|
|
|
|
public function testBeforeValidTokenWithIat()
|
|
{
|
|
$this->expectException(BeforeValidException::class);
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'iat' => time() + 20, // time in the future
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
JWT::decode($encoded, $this->hmacKey);
|
|
}
|
|
|
|
public function testValidToken()
|
|
{
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'exp' => time() + JWT::$leeway + 20, // time in the future
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
$decoded = JWT::decode($encoded, $this->hmacKey);
|
|
$this->assertSame($decoded->message, 'abc');
|
|
}
|
|
|
|
/**
|
|
* @runInSeparateProcess
|
|
*/
|
|
public function testValidTokenWithLeeway()
|
|
{
|
|
JWT::$leeway = 60;
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'exp' => time() - 20, // time in the past
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
$decoded = JWT::decode($encoded, $this->hmacKey);
|
|
$this->assertSame($decoded->message, 'abc');
|
|
}
|
|
|
|
/**
|
|
* @runInSeparateProcess
|
|
*/
|
|
public function testExpiredTokenWithLeeway()
|
|
{
|
|
$this->expectException(ExpiredException::class);
|
|
JWT::$leeway = 60;
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'exp' => time() - 70, // time far in the past
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
$decoded = JWT::decode($encoded, $this->hmacKey);
|
|
$this->assertSame($decoded->message, 'abc');
|
|
}
|
|
|
|
public function testExpiredExceptionPayload()
|
|
{
|
|
$this->expectException(ExpiredException::class);
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'exp' => time() - 100, // time in the past
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
try {
|
|
JWT::decode($encoded, $this->hmacKey);
|
|
} catch (ExpiredException $e) {
|
|
$exceptionPayload = (array) $e->getPayload();
|
|
$this->assertEquals($exceptionPayload, $payload);
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @runInSeparateProcess
|
|
*/
|
|
public function testExpiredExceptionTimestamp()
|
|
{
|
|
$this->expectException(ExpiredException::class);
|
|
|
|
JWT::$timestamp = 98765;
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'exp' => 1234,
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
|
|
try {
|
|
JWT::decode($encoded, $this->hmacKey);
|
|
} catch (ExpiredException $e) {
|
|
$exTimestamp = $e->getTimestamp();
|
|
$this->assertSame(98765, $exTimestamp);
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
public function testBeforeValidExceptionPayload()
|
|
{
|
|
$this->expectException(BeforeValidException::class);
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'iat' => time() + 100, // time in the future
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
try {
|
|
JWT::decode($encoded, $this->hmacKey);
|
|
} catch (BeforeValidException $e) {
|
|
$exceptionPayload = (array) $e->getPayload();
|
|
$this->assertEquals($exceptionPayload, $payload);
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
public function testValidTokenWithNbf()
|
|
{
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'iat' => time(),
|
|
'exp' => time() + 20, // time in the future
|
|
'nbf' => time() - 20
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
$decoded = JWT::decode($encoded, $this->hmacKey);
|
|
$this->assertSame($decoded->message, 'abc');
|
|
}
|
|
|
|
/**
|
|
* @runInSeparateProcess
|
|
*/
|
|
public function testValidTokenWithNbfLeeway()
|
|
{
|
|
JWT::$leeway = 60;
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'nbf' => time() + 20, // not before in near (leeway) future
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
$decoded = JWT::decode($encoded, $this->hmacKey);
|
|
$this->assertSame($decoded->message, 'abc');
|
|
}
|
|
|
|
/**
|
|
* @runInSeparateProcess
|
|
*/
|
|
public function testInvalidTokenWithNbfLeeway()
|
|
{
|
|
JWT::$leeway = 60;
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'nbf' => time() + 65, // not before too far in future
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
$this->expectException(BeforeValidException::class);
|
|
$this->expectExceptionMessage('Cannot handle token with nbf prior to');
|
|
JWT::decode($encoded, $this->hmacKey);
|
|
}
|
|
|
|
public function testValidTokenWithNbfIgnoresIat()
|
|
{
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'nbf' => time() - 20, // time in the future
|
|
'iat' => time() + 20, // time in the past
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
$decoded = JWT::decode($encoded, $this->hmacKey);
|
|
$this->assertEquals('abc', $decoded->message);
|
|
}
|
|
|
|
public function testValidTokenWithNbfMicrotime()
|
|
{
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'nbf' => microtime(true), // use microtime
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
$decoded = JWT::decode($encoded, $this->hmacKey);
|
|
$this->assertEquals('abc', $decoded->message);
|
|
}
|
|
|
|
public function testInvalidTokenWithNbfMicrotime()
|
|
{
|
|
$this->expectException(BeforeValidException::class);
|
|
$this->expectExceptionMessage('Cannot handle token with nbf prior to');
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'nbf' => microtime(true) + 20, // use microtime in the future
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
JWT::decode($encoded, $this->hmacKey);
|
|
}
|
|
|
|
/**
|
|
* @runInSeparateProcess
|
|
*/
|
|
public function testValidTokenWithIatLeeway()
|
|
{
|
|
JWT::$leeway = 60;
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'iat' => time() + 20, // issued in near (leeway) future
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
$decoded = JWT::decode($encoded, $this->hmacKey);
|
|
$this->assertSame($decoded->message, 'abc');
|
|
}
|
|
|
|
/**
|
|
* @runInSeparateProcess
|
|
*/
|
|
public function testInvalidTokenWithIatLeeway()
|
|
{
|
|
JWT::$leeway = 60;
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'iat' => time() + 65, // issued too far in future
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
$this->expectException(BeforeValidException::class);
|
|
$this->expectExceptionMessage('Cannot handle token with iat prior to');
|
|
JWT::decode($encoded, $this->hmacKey);
|
|
}
|
|
|
|
public function testValidTokenWithIatMicrotime()
|
|
{
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'iat' => microtime(true), // use microtime
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
$decoded = JWT::decode($encoded, $this->hmacKey);
|
|
$this->assertEquals('abc', $decoded->message);
|
|
}
|
|
|
|
public function testInvalidTokenWithIatMicrotime()
|
|
{
|
|
$this->expectException(BeforeValidException::class);
|
|
$this->expectExceptionMessage('Cannot handle token with iat prior to');
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'iat' => microtime(true) + 20, // use microtime in the future
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
JWT::decode($encoded, $this->hmacKey);
|
|
}
|
|
|
|
public function testInvalidToken()
|
|
{
|
|
$encodeKey = $this->generateHmac256();
|
|
$decodeKey = $this->generateHmac256();
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'exp' => time() + 20, // time in the future
|
|
];
|
|
$encoded = JWT::encode($payload, $encodeKey->getKeyMaterial(), $encodeKey->getAlgorithm());
|
|
$this->expectException(SignatureInvalidException::class);
|
|
JWT::decode($encoded, $decodeKey);
|
|
}
|
|
|
|
public function testNullKeyFails()
|
|
{
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'exp' => time() + JWT::$leeway + 20, // time in the future
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
$this->expectException(TypeError::class);
|
|
JWT::decode($encoded, new Key(null, 'HS256'));
|
|
}
|
|
|
|
public function testEmptyKeyFails()
|
|
{
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'exp' => time() + JWT::$leeway + 20, // time in the future
|
|
];
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
$this->expectException(InvalidArgumentException::class);
|
|
JWT::decode($encoded, new Key('', 'HS256'));
|
|
}
|
|
|
|
public function testKIDChooser()
|
|
{
|
|
$keys = [
|
|
'0' => $this->generateHmac256(),
|
|
'1' => $this->generateHmac256(),
|
|
'2' => $this->generateHmac256()
|
|
];
|
|
$msg = JWT::encode(['message' => 'abc'], $keys['0']->getKeyMaterial(), 'HS256', '0');
|
|
$decoded = JWT::decode($msg, $keys);
|
|
$expected = new stdClass();
|
|
$expected->message = 'abc';
|
|
$this->assertEquals($decoded, $expected);
|
|
}
|
|
|
|
public function testArrayAccessKIDChooser()
|
|
{
|
|
$keys = [
|
|
'0' => $this->generateHmac256(),
|
|
'1' => $this->generateHmac256(),
|
|
'2' => $this->generateHmac256()
|
|
];
|
|
$msg = JWT::encode(['message' => 'abc'], $keys['0']->getKeyMaterial(), 'HS256', '0');
|
|
$decoded = JWT::decode($msg, $keys);
|
|
$expected = new stdClass();
|
|
$expected->message = 'abc';
|
|
$this->assertEquals($decoded, $expected);
|
|
}
|
|
|
|
public function testNoneAlgorithm()
|
|
{
|
|
$msg = JWT::encode(['message' => 'abc'], $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
$this->expectException(UnexpectedValueException::class);
|
|
JWT::decode($msg, new Key($this->hmacKey->getKeyMaterial(), 'none'));
|
|
}
|
|
|
|
public function testIncorrectAlgorithm()
|
|
{
|
|
$msg = JWT::encode(['message' => 'abc'], $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
$this->expectException(UnexpectedValueException::class);
|
|
// TODO: Generate proper RS256 key
|
|
JWT::decode($msg, new Key($this->hmacKey->getKeyMaterial(), 'RS256'));
|
|
}
|
|
|
|
public function testEmptyAlgorithm()
|
|
{
|
|
$msg = JWT::encode(['message' => 'abc'], $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
$this->expectException(InvalidArgumentException::class);
|
|
JWT::decode($msg, new Key($this->hmacKey->getKeyMaterial(), ''));
|
|
}
|
|
|
|
public function testAdditionalHeaders()
|
|
{
|
|
$msg = JWT::encode(['message' => 'abc'], $this->hmacKey->getKeyMaterial(), 'HS256', null, ['cty' => 'test-eit;v=1']);
|
|
$expected = new stdClass();
|
|
$expected->message = 'abc';
|
|
$this->assertEquals(JWT::decode($msg, $this->hmacKey), $expected);
|
|
}
|
|
|
|
public function testInvalidSegmentCount()
|
|
{
|
|
$this->expectException(UnexpectedValueException::class);
|
|
JWT::decode('brokenheader.brokenbody', $this->hmacKey);
|
|
}
|
|
|
|
public function testInvalidSignatureEncoding()
|
|
{
|
|
$msg = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwibmFtZSI6ImZvbyJ9.Q4Kee9E8o0Xfo4ADXvYA8t7dN_X_bU9K5w6tXuiSjlUxx';
|
|
$this->expectException(UnexpectedValueException::class);
|
|
JWT::decode($msg, $this->hmacKey);
|
|
}
|
|
|
|
public function testHSEncodeDecode()
|
|
{
|
|
$msg = JWT::encode(['message' => 'abc'], $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
$expected = new stdClass();
|
|
$expected->message = 'abc';
|
|
$this->assertEquals(JWT::decode($msg, $this->hmacKey), $expected);
|
|
}
|
|
|
|
public function testRSEncodeDecode()
|
|
{
|
|
$privKey = openssl_pkey_new([
|
|
'digest_alg' => 'sha256',
|
|
'private_key_bits' => 2048,
|
|
'private_key_type' => OPENSSL_KEYTYPE_RSA
|
|
]);
|
|
$msg = JWT::encode(['message' => 'abc'], $privKey, 'RS256');
|
|
$pubKey = openssl_pkey_get_details($privKey);
|
|
$pubKey = $pubKey['key'];
|
|
$decoded = JWT::decode($msg, new Key($pubKey, 'RS256'));
|
|
$expected = new stdClass();
|
|
$expected->message = 'abc';
|
|
$this->assertEquals($decoded, $expected);
|
|
}
|
|
|
|
public function testEdDsaEncodeDecode()
|
|
{
|
|
$keyPair = sodium_crypto_sign_keypair();
|
|
$privKey = base64_encode(sodium_crypto_sign_secretkey($keyPair));
|
|
|
|
$payload = ['foo' => 'bar'];
|
|
$msg = JWT::encode($payload, $privKey, 'EdDSA');
|
|
|
|
$pubKey = base64_encode(sodium_crypto_sign_publickey($keyPair));
|
|
$decoded = JWT::decode($msg, new Key($pubKey, 'EdDSA'));
|
|
$this->assertSame('bar', $decoded->foo);
|
|
}
|
|
|
|
public function testInvalidEdDsaEncodeDecode()
|
|
{
|
|
$keyPair = sodium_crypto_sign_keypair();
|
|
$privKey = base64_encode(sodium_crypto_sign_secretkey($keyPair));
|
|
|
|
$payload = ['foo' => 'bar'];
|
|
$msg = JWT::encode($payload, $privKey, 'EdDSA');
|
|
|
|
// Generate a different key.
|
|
$keyPair = sodium_crypto_sign_keypair();
|
|
$pubKey = base64_encode(sodium_crypto_sign_publickey($keyPair));
|
|
$this->expectException(SignatureInvalidException::class);
|
|
JWT::decode($msg, new Key($pubKey, 'EdDSA'));
|
|
}
|
|
|
|
public function testRSEncodeDecodeWithPassphrase()
|
|
{
|
|
$privateKey = openssl_pkey_get_private(
|
|
file_get_contents(__DIR__ . '/data/rsa-with-passphrase.pem'),
|
|
'passphrase'
|
|
);
|
|
|
|
$jwt = JWT::encode(['message' => 'abc'], $privateKey, 'RS256');
|
|
$keyDetails = openssl_pkey_get_details($privateKey);
|
|
$pubKey = $keyDetails['key'];
|
|
$decoded = JWT::decode($jwt, new Key($pubKey, 'RS256'));
|
|
$expected = new stdClass();
|
|
$expected->message = 'abc';
|
|
$this->assertEquals($decoded, $expected);
|
|
}
|
|
|
|
public function testDecodesEmptyArrayAsObject()
|
|
{
|
|
$key = 'yma6Hq4XQegCVND8ef23OYgxSrC3IKqk';
|
|
$payload = [];
|
|
$jwt = JWT::encode($payload, $key, 'HS256');
|
|
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
|
|
$this->assertEquals((object) $payload, $decoded);
|
|
}
|
|
|
|
public function testDecodesArraysInJWTAsArray()
|
|
{
|
|
$key = 'yma6Hq4XQegCVND8ef23OYgxSrC3IKqk';
|
|
$payload = ['foo' => [1, 2, 3]];
|
|
$jwt = JWT::encode($payload, $key, 'HS256');
|
|
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
|
|
$this->assertSame($payload['foo'], $decoded->foo);
|
|
}
|
|
|
|
/**
|
|
* @runInSeparateProcess
|
|
* @dataProvider provideEncodeDecode
|
|
*/
|
|
public function testEncodeDecode($privateKeyFile, $publicKeyFile, $alg)
|
|
{
|
|
$privateKey = file_get_contents($privateKeyFile);
|
|
$payload = ['foo' => 'bar'];
|
|
$encoded = JWT::encode($payload, $privateKey, $alg);
|
|
|
|
// Verify decoding succeeds
|
|
$publicKey = file_get_contents($publicKeyFile);
|
|
$decoded = JWT::decode($encoded, new Key($publicKey, $alg));
|
|
|
|
$this->assertSame('bar', $decoded->foo);
|
|
}
|
|
|
|
public function provideEncodeDecode()
|
|
{
|
|
return [
|
|
[__DIR__ . '/data/ecdsa-private.pem', __DIR__ . '/data/ecdsa-public.pem', 'ES256'],
|
|
[__DIR__ . '/data/ecdsa384-private.pem', __DIR__ . '/data/ecdsa384-public.pem', 'ES384'],
|
|
[__DIR__ . '/data/rsa1-private.pem', __DIR__ . '/data/rsa1-public.pub', 'RS512'],
|
|
[__DIR__ . '/data/ed25519-1.sec', __DIR__ . '/data/ed25519-1.pub', 'EdDSA'],
|
|
[__DIR__ . '/data/secp256k1-private.pem', __DIR__ . '/data/secp256k1-public.pem', 'ES256K'],
|
|
];
|
|
}
|
|
|
|
public function testEncodeDecodeWithOpenSSLAsymmetricKey()
|
|
{
|
|
$pem = file_get_contents(__DIR__ . '/data/rsa1-public.pub');
|
|
$keyMaterial = openssl_pkey_get_public($pem);
|
|
$privateKey = file_get_contents(__DIR__ . '/data/rsa1-private.pem');
|
|
|
|
$payload = ['foo' => 'bar'];
|
|
$encoded = JWT::encode($payload, $privateKey, 'RS512');
|
|
|
|
// Verify decoding succeeds
|
|
$decoded = JWT::decode($encoded, new Key($keyMaterial, 'RS512'));
|
|
|
|
$this->assertSame('bar', $decoded->foo);
|
|
}
|
|
|
|
public function testGetHeaders()
|
|
{
|
|
$payload = [
|
|
'message' => 'abc',
|
|
'exp' => time() + JWT::$leeway + 20, // time in the future
|
|
];
|
|
$headers = new stdClass();
|
|
|
|
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
JWT::decode($encoded, $this->hmacKey, $headers);
|
|
|
|
$this->assertEquals($headers->typ, 'JWT');
|
|
$this->assertEquals($headers->alg, 'HS256');
|
|
}
|
|
|
|
public function testAdditionalHeaderOverrides()
|
|
{
|
|
$msg = JWT::encode(
|
|
['message' => 'abc'],
|
|
$this->hmacKey->getKeyMaterial(),
|
|
'HS256',
|
|
'my_key_id',
|
|
[
|
|
'cty' => 'test-eit;v=1',
|
|
'typ' => 'JOSE', // override type header
|
|
'kid' => 'not_my_key_id', // should not override $key param
|
|
'alg' => 'BAD', // should not override $alg param
|
|
]
|
|
);
|
|
$headers = new stdClass();
|
|
JWT::decode($msg, $this->hmacKey, $headers);
|
|
$this->assertEquals('test-eit;v=1', $headers->cty, 'additional field works');
|
|
$this->assertEquals('JOSE', $headers->typ, 'typ override works');
|
|
$this->assertEquals('my_key_id', $headers->kid, 'key param not overridden');
|
|
$this->assertEquals('HS256', $headers->alg, 'alg param not overridden');
|
|
}
|
|
|
|
public function testDecodeExpectsIntegerIat()
|
|
{
|
|
$this->expectException(UnexpectedValueException::class);
|
|
$this->expectExceptionMessage('Payload iat must be a number');
|
|
|
|
$payload = JWT::encode(['iat' => 'not-an-int'], $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
JWT::decode($payload, $this->hmacKey);
|
|
}
|
|
|
|
public function testDecodeExpectsIntegerNbf()
|
|
{
|
|
$this->expectException(UnexpectedValueException::class);
|
|
$this->expectExceptionMessage('Payload nbf must be a number');
|
|
|
|
$payload = JWT::encode(['nbf' => 'not-an-int'], $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
JWT::decode($payload, $this->hmacKey);
|
|
}
|
|
|
|
public function testDecodeExpectsIntegerExp()
|
|
{
|
|
$this->expectException(UnexpectedValueException::class);
|
|
$this->expectExceptionMessage('Payload exp must be a number');
|
|
|
|
$payload = JWT::encode(['exp' => 'not-an-int'], $this->hmacKey->getKeyMaterial(), 'HS256');
|
|
JWT::decode($payload, $this->hmacKey);
|
|
}
|
|
|
|
public function testRsaKeyLengthValidationThrowsException(): void
|
|
{
|
|
$this->expectException(DomainException::class);
|
|
$this->expectExceptionMessage('Provided key is too short');
|
|
|
|
// Generate an RSA key that is smaller than the 2048-bit minimum
|
|
$shortRsaKey = openssl_pkey_new([
|
|
'private_key_bits' => 1024,
|
|
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
|
]);
|
|
|
|
self::assertNotFalse($shortRsaKey, 'Failed to generate a short RSA key for testing.');
|
|
$payload = ['message' => 'abc'];
|
|
JWT::encode($payload, $shortRsaKey, 'RS256');
|
|
}
|
|
|
|
/** @dataProvider provideHmac */
|
|
public function testHmacKeyLengthValidationThrowsExceptionEncode(string $alg, int $minLength): void
|
|
{
|
|
$this->expectException(DomainException::class);
|
|
$this->expectExceptionMessage('Provided key is too short');
|
|
|
|
$tooShortKeyBytes = str_repeat('b', $minLength - 1);
|
|
$payload = ['message' => 'abc'];
|
|
|
|
JWT::encode($payload, $tooShortKeyBytes, $alg);
|
|
}
|
|
|
|
/** @dataProvider provideHmac */
|
|
public function testHmacKeyLengthValidationThrowsExceptionDecode(string $alg, int $minLength): void
|
|
{
|
|
$this->expectException(DomainException::class);
|
|
$this->expectExceptionMessage('Provided key is too short');
|
|
|
|
$tooShortKeyBytes = str_repeat('b', $minLength - 1);
|
|
$payload = ['message' => 'abc'];
|
|
|
|
$validKeyBytes = str_repeat('b', $minLength);
|
|
$encoded = JWT::encode($payload, $validKeyBytes, $alg);
|
|
|
|
JWT::decode($encoded, new Key($tooShortKeyBytes, $alg));
|
|
}
|
|
|
|
/** @dataProvider provideHmac */
|
|
public function testHmacKeyLengthValidationPassesWithCorrectLength(string $alg, int $minLength): void
|
|
{
|
|
$payload = ['message' => 'test hmac length'];
|
|
|
|
// Test with a key that is exactly the required length
|
|
$minKeyBytes = str_repeat('b', $minLength);
|
|
$encoded48 = JWT::encode($payload, $minKeyBytes, $alg);
|
|
$decoded48 = JWT::decode($encoded48, new Key($minKeyBytes, $alg));
|
|
$this->assertEquals($payload['message'], $decoded48->message);
|
|
|
|
// Test with a key that is longer than the required length
|
|
$largeKeyBytes = str_repeat('c', $minLength * 2); // Longer than min bytes
|
|
$encoded64 = JWT::encode($payload, $largeKeyBytes, $alg);
|
|
$decoded64 = JWT::decode($encoded64, new Key($largeKeyBytes, $alg));
|
|
$this->assertEquals($payload['message'], $decoded64->message);
|
|
}
|
|
|
|
public function provideHmac()
|
|
{
|
|
return [
|
|
['HS384', 48],
|
|
['HS256', 32],
|
|
];
|
|
}
|
|
|
|
public function testEdDsaHandlesBase64UrlKeys()
|
|
{
|
|
if (!\extension_loaded('sodium')) {
|
|
$this->markTestSkipped('libsodium is not available');
|
|
}
|
|
|
|
// Generate a deterministic Ed25519 keypair using a specific seed. The byte "\xfb"
|
|
// translates to '+' and '/' in standard base64, which become '-' and '_' in Base64URL.
|
|
// This guarantees our keys will contain the URL-safe characters that get incorrectly
|
|
// stripped by base64_decode().
|
|
$seed = str_repeat("\xfb", 32);
|
|
$keyPair = sodium_crypto_sign_seed_keypair($seed);
|
|
|
|
$secretKey = sodium_crypto_sign_secretkey($keyPair);
|
|
$publicKey = sodium_crypto_sign_publickey($keyPair);
|
|
|
|
// Convert the raw keys to Base64URL encoded strings
|
|
$secretKeyB64u = JWT::urlsafeB64Encode($secretKey);
|
|
$publicKeyB64u = JWT::urlsafeB64Encode($publicKey);
|
|
|
|
// Ensure our test keys actually contain the characters that get
|
|
// incorrectly stripped by a standard base64_decode().
|
|
$this->assertTrue(strpos($secretKeyB64u, '-') !== false || strpos($secretKeyB64u, '_') !== false);
|
|
$this->assertTrue(strpos($publicKeyB64u, '-') !== false || strpos($publicKeyB64u, '_') !== false);
|
|
|
|
// Test Encoding
|
|
$token = JWT::encode(['issue' => 596], $secretKeyB64u, 'EdDSA');
|
|
$this->assertIsString($token);
|
|
|
|
// Test Decoding
|
|
$decoded = JWT::decode($token, new Key($publicKeyB64u, 'EdDSA'));
|
|
$this->assertSame(596, $decoded->issue);
|
|
}
|
|
|
|
/** @dataProvider provideEcKeyInvalidLength */
|
|
public function testEcKeyLengthValidationThrowsExceptionEncode(string $keyFile, string $alg): void
|
|
{
|
|
$this->expectException(DomainException::class);
|
|
$this->expectExceptionMessage('Provided key is too short');
|
|
|
|
$tooShortEcKey = file_get_contents(__DIR__ . '/data/' . $keyFile);
|
|
$payload = ['message' => 'abc'];
|
|
|
|
JWT::encode($payload, $tooShortEcKey, $alg);
|
|
}
|
|
|
|
public function testEcKeyLengthValidationThrowsExceptionDecode(): void
|
|
{
|
|
$this->expectException(DomainException::class);
|
|
$this->expectExceptionMessage('Provided key is too short');
|
|
|
|
$payload = ['message' => 'abc'];
|
|
|
|
$validEcKeyBytes = file_get_contents(__DIR__ . '/data/ecdsa384-private.pem');
|
|
$encoded = JWT::encode($payload, $validEcKeyBytes, 'ES256');
|
|
|
|
$tooShortEcKey = file_get_contents(__DIR__ . '/data/ecdsa192-public.pem');
|
|
JWT::decode($encoded, new Key($tooShortEcKey, 'ES256'));
|
|
}
|
|
|
|
/** @dataProvider provideEcKey */
|
|
public function testEcKeyLengthValidationPassesWithCorrectLength(
|
|
string $privateKeyFile,
|
|
string $publicKeyFile,
|
|
string $alg
|
|
): void {
|
|
$payload = ['message' => 'test hmac length'];
|
|
|
|
// Test with a key that is the required length
|
|
$privateKeyBytes = file_get_contents(__DIR__ . '/data/' . $privateKeyFile);
|
|
$encoded48 = JWT::encode($payload, $privateKeyBytes, $alg);
|
|
|
|
$publicKeyBytes = file_get_contents(__DIR__ . '/data/' . $publicKeyFile);
|
|
$decoded48 = JWT::decode($encoded48, new Key($publicKeyBytes, $alg));
|
|
$this->assertEquals($payload['message'], $decoded48->message);
|
|
}
|
|
|
|
public function provideEcKeyInvalidLength()
|
|
{
|
|
return [
|
|
['ecdsa192-private.pem', 'ES256'],
|
|
['ecdsa-private.pem', 'ES384'],
|
|
];
|
|
}
|
|
|
|
public function provideEcKey()
|
|
{
|
|
return [
|
|
['ecdsa-private.pem', 'ecdsa-public.pem', 'ES256'],
|
|
['ecdsa384-private.pem', 'ecdsa384-public.pem', 'ES384'],
|
|
];
|
|
}
|
|
|
|
private function generateHmac256(): Key
|
|
{
|
|
return new Key(random_bytes(32), 'HS256');
|
|
}
|
|
}
|