Skip to content

Commit

Permalink
Merge branch 'main' into more-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
bshaffer committed Jun 28, 2023
2 parents 58ff79b + 48b0210 commit f844418
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 12 deletions.
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
# Changelog

## [6.8.0](https://github.com/firebase/php-jwt/compare/v6.7.0...v6.8.0) (2023-06-14)


### Features

* add support for P-384 curve ([#515](https://github.com/firebase/php-jwt/issues/515)) ([5de4323](https://github.com/firebase/php-jwt/commit/5de4323f4baf4d70bca8663bd87682a69c656c3d))


### Bug Fixes

* handle invalid http responses ([#508](https://github.com/firebase/php-jwt/issues/508)) ([91c39c7](https://github.com/firebase/php-jwt/commit/91c39c72b22fc3e1191e574089552c1f2041c718))

## [6.7.0](https://github.com/firebase/php-jwt/compare/v6.6.0...v6.7.0) (2023-06-14)


### Features

* add ed25519 support to JWK (public keys) ([#452](https://github.com/firebase/php-jwt/issues/452)) ([e53979a](https://github.com/firebase/php-jwt/commit/e53979abae927de916a75b9d239cfda8ce32be2a))

## [6.6.0](https://github.com/firebase/php-jwt/compare/v6.5.0...v6.6.0) (2023-06-13)


### Features

* allow get headers when decoding token ([#442](https://github.com/firebase/php-jwt/issues/442)) ([fb85f47](https://github.com/firebase/php-jwt/commit/fb85f47cfaeffdd94faf8defdf07164abcdad6c3))


### Bug Fixes

* only check iat if nbf is not used ([#493](https://github.com/firebase/php-jwt/issues/493)) ([398ccd2](https://github.com/firebase/php-jwt/commit/398ccd25ea12fa84b9e4f1085d5ff448c21ec797))

## [6.5.0](https://github.com/firebase/php-jwt/compare/v6.4.0...v6.5.0) (2023-05-12)


Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@ $payload = [
*/
$jwt = JWT::encode($payload, $key, 'HS256');
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));

print_r($decoded);

// Pass a stdClass in as the third parameter to get the decoded header values
$decoded = JWT::decode($jwt, new Key($key, 'HS256'), $headers = new stdClass());
print_r($headers);

/*
NOTE: This will now be an object instead of an associative array. To get
an associative array, you will need to cast it as such:
Expand Down
10 changes: 10 additions & 0 deletions src/CachedKeySet.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,16 @@ private function keyIdExists(string $keyId): bool
}
$request = $this->httpFactory->createRequest('GET', $this->jwksUri);
$jwksResponse = $this->httpClient->sendRequest($request);
if ($jwksResponse->getStatusCode() !== 200) {
throw new UnexpectedValueException(
sprintf('HTTP Error: %d %s for URI "%s"',
$jwksResponse->getStatusCode(),
$jwksResponse->getReasonPhrase(),
$this->jwksUri,
),
$jwksResponse->getStatusCode()
);
}
$this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody());

if (!isset($this->keySet[$keyId])) {
Expand Down
32 changes: 29 additions & 3 deletions src/JWK.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,16 @@ class JWK
private const EC_CURVES = [
'P-256' => '1.2.840.10045.3.1.7', // Len: 64
'secp256k1' => '1.3.132.0.10', // Len: 64
// 'P-384' => '1.3.132.0.34', // Len: 96 (not yet supported)
'P-384' => '1.3.132.0.34', // Len: 96
// 'P-521' => '1.3.132.0.35', // Len: 132 (not supported)
];

// For keys with "kty" equal to "OKP" (Octet Key Pair), the "crv" parameter must contain the key subtype.
// This library supports the following subtypes:
private const OKP_SUBTYPES = [
'Ed25519' => true, // RFC 8037
];

/**
* Parse a set of JWK keys
*
Expand Down Expand Up @@ -145,8 +151,28 @@ public static function parseKey(array $jwk, string $defaultAlg = null): ?Key

$publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']);
return new Key($publicKey, $jwk['alg']);
case 'OKP':
if (isset($jwk['d'])) {
// The key is actually a private key
throw new UnexpectedValueException('Key data must be for a public key');
}

if (!isset($jwk['crv'])) {
throw new UnexpectedValueException('crv not set');
}

if (empty(self::OKP_SUBTYPES[$jwk['crv']])) {
throw new DomainException('Unrecognised or unsupported OKP key subtype');
}

if (empty($jwk['x'])) {
throw new UnexpectedValueException('x not set');
}

// This library works internally with EdDSA keys (Ed25519) encoded in standard base64.
$publicKey = JWT::convertBase64urlToBase64($jwk['x']);
return new Key($publicKey, $jwk['alg']);
default:
// Currently only RSA is supported
break;
}

Expand All @@ -156,7 +182,7 @@ public static function parseKey(array $jwk, string $defaultAlg = null): ?Key
/**
* Converts the EC JWK values to pem format.
*
* @param string $crv The EC curve (only P-256 is supported)
* @param string $crv The EC curve (only P-256 & P-384 is supported)
* @param string $x The EC x-coordinate
* @param string $y The EC y-coordinate
*
Expand Down
28 changes: 24 additions & 4 deletions src/JWT.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class JWT
* Supported algorithms are 'ES384','ES256',
* 'HS256', 'HS384', 'HS512', 'RS256', 'RS384'
* and 'RS512'.
* @param stdClass $headers Optional. Populates stdClass with headers.
*
* @return stdClass The JWT's payload as a PHP object
*
Expand All @@ -94,7 +95,8 @@ class JWT
*/
public static function decode(
string $jwt,
$keyOrKeyArray
$keyOrKeyArray,
stdClass &$headers = null
): stdClass {
// Validate JWT
$timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp;
Expand All @@ -111,6 +113,9 @@ public static function decode(
if (null === ($header = static::jsonDecode($headerRaw))) {
throw new UnexpectedValueException('Invalid header encoding');
}
if ($headers !== null) {
$headers = $header;
}
$payloadRaw = static::urlsafeB64Decode($bodyb64);
if (null === ($payload = static::jsonDecode($payloadRaw))) {
throw new UnexpectedValueException('Invalid claims encoding');
Expand Down Expand Up @@ -215,7 +220,7 @@ public static function encode(
*
* @param string $msg The message to sign
* @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
* @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256',
* @param string $alg Supported algorithms are 'EdDSA', 'ES384', 'ES256', 'ES256K', 'HS256',
* 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
*
* @return string An encrypted message
Expand Down Expand Up @@ -278,7 +283,7 @@ public static function sign(
*
* @param string $msg The original message (header and body)
* @param string $signature The original signature
* @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey
* @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For Ed*, ES*, HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey
* @param string $alg The algorithm
*
* @return bool
Expand Down Expand Up @@ -399,13 +404,28 @@ public static function jsonEncode(array $input): string
* @throws InvalidArgumentException invalid base64 characters
*/
public static function urlsafeB64Decode(string $input): string
{
return \base64_decode(self::convertBase64UrlToBase64($input));
}

/**
* Convert a string in the base64url (URL-safe Base64) encoding to standard base64.
*
* @param string $input A Base64 encoded string with URL-safe characters (-_ and no padding)
*
* @return string A Base64 encoded string with standard characters (+/) and padding (=), when
* needed.
*
* @see https://www.rfc-editor.org/rfc/rfc4648
*/
public static function convertBase64UrlToBase64(string $input): string
{
$remainder = \strlen($input) % 4;
if ($remainder) {
$padlen = 4 - $remainder;
$input .= \str_repeat('=', $padlen);
}
return \base64_decode(\strtr($input, '-_', '+/'));
return \strtr($input, '-_', '+/');
}

/**
Expand Down
32 changes: 32 additions & 0 deletions tests/CachedKeySetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,35 @@ public function testOutOfBoundsThrowsException()
$cachedKeySet['bar'];
}

public function testInvalidHttpResponseThrowsException()
{
$this->expectException(\UnexpectedValueException::class);
$this->expectExceptionMessage('HTTP Error: 404 URL not found');
$this->expectExceptionCode(404);

$response = $this->prophesize('Psr\Http\Message\ResponseInterface');
$response->getStatusCode()
->shouldBeCalled()
->willReturn(404);
$response->getReasonPhrase()
->shouldBeCalledTimes(1)
->willReturn('URL not found');

$http = $this->prophesize(ClientInterface::class);
$http->sendRequest(Argument::any())
->shouldBeCalledTimes(1)
->willReturn($response->reveal());

$cachedKeySet = new CachedKeySet(
$this->testJwksUri,
$http->reveal(),
$this->getMockHttpFactory(),
$this->getMockEmptyCache()
);

isset($cachedKeySet[0]);
}

public function testWithExistingKeyId()
{
$cachedKeySet = new CachedKeySet(
Expand Down Expand Up @@ -382,6 +411,9 @@ private function getMockHttpClient($testJwks, int $timesCalled = 1)
$response->getBody()
->shouldBeCalledTimes($timesCalled)
->willReturn($body->reveal());
$response->getStatusCode()
->shouldBeCalledTimes($timesCalled)
->willReturn(200);

$http = $this->prophesize(ClientInterface::class);
$http->sendRequest(Argument::any())
Expand Down
10 changes: 6 additions & 4 deletions tests/JWKTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,11 @@ public function testDecodeByJwkKeySetTokenExpired()
/**
* @dataProvider provideDecodeByJwkKeySet
*/
public function testDecodeByJwkKeySet($pemFile, $jwkFile, $alg)
public function testDecodeByJwkKeySet($pemFile, $jwkFile, $alg, $keyId)
{
$privKey1 = file_get_contents(__DIR__ . '/data/' . $pemFile);
$payload = ['sub' => 'foo', 'exp' => strtotime('+10 seconds')];
$msg = JWT::encode($payload, $privKey1, $alg, 'jwk1');
$msg = JWT::encode($payload, $privKey1, $alg, $keyId);

$jwkSet = json_decode(
file_get_contents(__DIR__ . '/data/' . $jwkFile),
Expand All @@ -149,8 +149,10 @@ public function testDecodeByJwkKeySet($pemFile, $jwkFile, $alg)
public function provideDecodeByJwkKeySet()
{
return [
['rsa1-private.pem', 'rsa-jwkset.json', 'RS256'],
['ecdsa256-private.pem', 'ec-jwkset.json', 'ES256'],
['rsa1-private.pem', 'rsa-jwkset.json', 'RS256', 'jwk1'],
['ecdsa256-private.pem', 'ec-jwkset.json', 'ES256', 'jwk1'],
['ecdsa384-private.pem', 'ec-jwkset.json', 'ES384', 'jwk4'],
['ed25519-1.sec', 'ed25519-jwkset.json', 'EdDSA', 'jwk1'],
];
}

Expand Down
15 changes: 15 additions & 0 deletions tests/JWTTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -465,4 +465,19 @@ public function testEncodeDecodeWithResource()

$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, 'my_key', 'HS256');
JWT::decode($encoded, new Key('my_key', 'HS256'), $headers);

$this->assertEquals($headers->typ, 'JWT');
$this->assertEquals($headers->alg, 'HS256');
}
}
9 changes: 9 additions & 0 deletions tests/data/ec-jwkset.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@
"x": "EFpwNuP322bU3WP1DtJgx67L0CUV1MxNixqPVMH2L9Q",
"y": "_fSTbijIJjpsqL16cIEvxxf3MaYMY8MbqEq066yV9ls",
"alg": "ES256K"
},
{
"kty": "EC",
"use": "sig",
"crv": "P-384",
"kid": "jwk4",
"x": "FhXXcyKmWkTkdVbWYYU3dtJqpJ0JmLGftEdNzUEFEKSU5MlnLr_FjcneszvXAqEB",
"y": "M4veJF_dO_zhFk44bh_ELXbp0_nn9QaViVtQpuTvpu29eefx6PfUMqX0K--IS4NQ",
"alg": "ES384"
}
]
}
11 changes: 11 additions & 0 deletions tests/data/ed25519-jwkset.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"keys": [
{
"kid": "jwk1",
"alg": "EdDSA",
"kty": "OKP",
"crv": "Ed25519",
"x": "uOSJMhbKSG4V5xUHS7B9YHmVg_1yVd-G-Io6oBFhSfY"
}
]
}

0 comments on commit f844418

Please sign in to comment.