-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add EncodingOptions class for encapsulate JwtEncoder::encode() …
…options
- Loading branch information
1 parent
4a3c110
commit 146318a
Showing
4 changed files
with
392 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
<?php | ||
|
||
namespace B2pweb\Jwt; | ||
|
||
use InvalidArgumentException; | ||
use Jose\Component\Core\JWK; | ||
use Jose\Component\Core\JWKSet; | ||
|
||
/** | ||
* Options for encoding a JWT | ||
* | ||
* @see JwtEncoder::encode() | ||
*/ | ||
final class EncodingOptions | ||
{ | ||
/** | ||
* @var JWKSet | ||
*/ | ||
private $keySet; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
private $algorithm; | ||
|
||
/** | ||
* @var string|null | ||
*/ | ||
private $kid; | ||
|
||
/** | ||
* @var array<string, mixed> | ||
*/ | ||
private $headers = []; | ||
|
||
/** | ||
* @param JWKSet $keySet | ||
* @param string $algorithm | ||
* @param string|null $kid | ||
* @param array<string, mixed> $headers | ||
*/ | ||
public function __construct(JWKSet $keySet, string $algorithm = 'RS256', ?string $kid = null, array $headers = []) | ||
{ | ||
$this->keySet = $keySet; | ||
$this->algorithm = $algorithm; | ||
$this->kid = $kid; | ||
$this->headers = $headers; | ||
} | ||
|
||
/** | ||
* Get the requested signature algorithm | ||
* | ||
* @return string | ||
*/ | ||
public function algorithm(): string | ||
{ | ||
return $this->algorithm; | ||
} | ||
|
||
/** | ||
* Define the signature algorithm | ||
* | ||
* @param string $algorithm | ||
* | ||
* @return $this | ||
*/ | ||
public function setAlgorithm(string $algorithm): EncodingOptions | ||
{ | ||
$this->algorithm = $algorithm; | ||
return $this; | ||
} | ||
|
||
/** | ||
* Get the defined key id | ||
* | ||
* @return string|null | ||
*/ | ||
public function kid(): ?string | ||
{ | ||
return $this->kid; | ||
} | ||
|
||
/** | ||
* Define the key id | ||
* | ||
* @param string|null $kid | ||
* @return $this | ||
*/ | ||
public function setKid(?string $kid): EncodingOptions | ||
{ | ||
$this->kid = $kid; | ||
return $this; | ||
} | ||
|
||
/** | ||
* Define additional headers | ||
* | ||
* @param array<string, mixed> $headers | ||
* @return $this | ||
*/ | ||
public function setHeaders(array $headers): EncodingOptions | ||
{ | ||
$this->headers = $headers; | ||
return $this; | ||
} | ||
|
||
/** | ||
* Define an additional header | ||
* | ||
* @param string $key The header key | ||
* @param mixed $value The header value | ||
* | ||
* @return $this | ||
*/ | ||
public function setHeader(string $key, $value): EncodingOptions | ||
{ | ||
$this->headers[$key] = $value; | ||
return $this; | ||
} | ||
|
||
/** | ||
* Try to select a signature key | ||
* | ||
* @param JWA $algorithms The supported algorithms | ||
* @return JWK | ||
* | ||
* @throws InvalidArgumentException If there is no valid key, or requested algorithm is not supported | ||
*/ | ||
public function selectSignatureKey(JWA $algorithms): JWK | ||
{ | ||
$key = $this->keySet->selectKey( | ||
'sig', | ||
$algorithms->manager()->get($this->algorithm), | ||
$this->kid ? ['kid' => $this->kid] : [] | ||
); | ||
|
||
if (!$key) { | ||
throw new InvalidArgumentException('Cannot found any valid key'); | ||
} | ||
|
||
return $key; | ||
} | ||
|
||
/** | ||
* Get the signature headers | ||
* | ||
* @return array<string, mixed> | ||
*/ | ||
public function headers(): array | ||
{ | ||
$headers = $this->headers; | ||
$headers['alg'] = $this->algorithm; | ||
|
||
if ($this->kid) { | ||
$headers['kid'] = $this->kid; | ||
} | ||
|
||
return $headers; | ||
} | ||
|
||
/** | ||
* Create options from a key | ||
* Algorithm and kid will be resolved from the key | ||
* | ||
* @param JWK $key | ||
* @return self | ||
*/ | ||
public static function fromKey(JWK $key): self | ||
{ | ||
$options = new self(new JWKSet([$key])); | ||
|
||
if ($key->has('alg')) { | ||
$options->setAlgorithm((string) $key->get('alg')); | ||
Check warning on line 173 in src/EncodingOptions.php GitHub Actions / Analysis
|
||
} | ||
|
||
if ($key->has('kid')) { | ||
$options->setKid((string) $key->get('kid')); | ||
Check warning on line 177 in src/EncodingOptions.php GitHub Actions / Analysis
|
||
} | ||
|
||
return $options; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
<?php | ||
|
||
namespace B2pweb\Jwt\Tests; | ||
|
||
use B2pweb\Jwt\EncodingOptions; | ||
use B2pweb\Jwt\JWA; | ||
use B2pweb\Jwt\JwtEncoder; | ||
use Jose\Component\Core\JWKSet; | ||
use Jose\Component\KeyManagement\JWKFactory; | ||
use PHPUnit\Framework\TestCase; | ||
|
||
class EncodingOptionsTest extends TestCase | ||
{ | ||
/** | ||
* @var string | ||
*/ | ||
private $publicKey; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
private $privateKey; | ||
|
||
protected function setUp(): void | ||
{ | ||
$this->publicKey = __DIR__.'/assets/public.key'; | ||
$this->privateKey = __DIR__.'/assets/private.key'; | ||
} | ||
|
||
public function test_getter_setter() | ||
{ | ||
$options = new EncodingOptions(new JWKSet([ | ||
JWKFactory::createFromKeyFile($this->privateKey, null, ['use' => 'sig', 'alg' => 'RS256']), | ||
])); | ||
|
||
$this->assertSame('RS256', $options->algorithm()); | ||
$this->assertNull($options->kid()); | ||
$this->assertSame(['alg' => 'RS256'], $options->headers()); | ||
|
||
$options | ||
->setAlgorithm('RS512') | ||
->setKid('foo') | ||
->setHeaders(['foo' => 'bar']) | ||
; | ||
|
||
$this->assertSame('RS512', $options->algorithm()); | ||
$this->assertSame('foo', $options->kid()); | ||
$this->assertSame(['foo' => 'bar', 'alg' => 'RS512', 'kid' => 'foo'], $options->headers()); | ||
|
||
$options->setHeader('a', 'b'); | ||
$this->assertSame(['foo' => 'bar', 'a' => 'b', 'alg' => 'RS512', 'kid' => 'foo'], $options->headers()); | ||
} | ||
|
||
public function test_selectSignatureKey() | ||
{ | ||
$options = new EncodingOptions(new JWKSet([ | ||
$k1 = JWKFactory::createFromKeyFile($this->privateKey, null, ['use' => 'sig', 'alg' => 'RS256', 'kid' => 'k1']), | ||
$k2 = JWKFactory::createFromKeyFile(__DIR__.'/assets/other.key', null, ['use' => 'sig', 'alg' => 'RS256', 'kid' => 'k2']), | ||
$k3 = JWKFactory::createFromSecret('azertyuiopazertyuiopazertyuiopazertyuiopazertyuiopazertyuiopazertyuiop', ['alg' => 'HS256', 'kid' => 'k3']), | ||
$k4 = JWKFactory::createFromKeyFile(__DIR__.'/assets/other.key', null, ['use' => 'enc', 'alg' => 'RS256', 'kid' => 'k4']), | ||
])); | ||
|
||
$this->assertSame($k1, $options->selectSignatureKey(new JWA())); | ||
$this->assertSame($k3, $options->setAlgorithm('HS256')->selectSignatureKey(new JWA())); | ||
$this->assertSame($k2, $options->setAlgorithm('RS256')->setKid('k2')->selectSignatureKey(new JWA())); | ||
|
||
try { | ||
$options->setAlgorithm('RS256')->setKid('k4')->selectSignatureKey(new JWA()); | ||
$this->fail('Expected exception not thrown'); | ||
} catch (\InvalidArgumentException $e) { | ||
$this->assertSame('Cannot found any valid key', $e->getMessage()); | ||
} | ||
|
||
try { | ||
$options->setAlgorithm('invalid')->setKid(null)->selectSignatureKey(new JWA()); | ||
$this->fail('Expected exception not thrown'); | ||
} catch (\InvalidArgumentException $e) { | ||
$this->assertSame('The algorithm "invalid" is not supported.', $e->getMessage()); | ||
} | ||
|
||
try { | ||
$options->setAlgorithm('RS256')->setKid('invalid')->selectSignatureKey(new JWA()); | ||
$this->fail('Expected exception not thrown'); | ||
} catch (\InvalidArgumentException $e) { | ||
$this->assertSame('Cannot found any valid key', $e->getMessage()); | ||
} | ||
|
||
try { | ||
$options->setAlgorithm('RS512')->setKid(null)->selectSignatureKey(new JWA()); | ||
$this->fail('Expected exception not thrown'); | ||
} catch (\InvalidArgumentException $e) { | ||
$this->assertSame('Cannot found any valid key', $e->getMessage()); | ||
} | ||
} | ||
|
||
public function test_fromKey() | ||
{ | ||
$options = EncodingOptions::fromKey($key = JWKFactory::createFromKeyFile($this->privateKey, null, ['use' => 'sig'])); | ||
$this->assertSame('RS256', $options->algorithm()); | ||
$this->assertSame($key, $options->selectSignatureKey(new JWA())); | ||
$this->assertNull($options->kid()); | ||
$this->assertSame(['alg' => 'RS256'], $options->headers()); | ||
|
||
$options = EncodingOptions::fromKey($key = JWKFactory::createFromKeyFile($this->privateKey, null, ['use' => 'sig', 'alg' => 'RS512'])); | ||
$this->assertSame('RS512', $options->algorithm()); | ||
$this->assertSame($key, $options->selectSignatureKey(new JWA())); | ||
$this->assertNull($options->kid()); | ||
$this->assertSame(['alg' => 'RS512'], $options->headers()); | ||
|
||
$options = EncodingOptions::fromKey($key = JWKFactory::createFromKeyFile($this->privateKey, null, ['use' => 'sig', 'alg' => 'RS512', 'kid' => 'foo'])); | ||
$this->assertSame('RS512', $options->algorithm()); | ||
$this->assertSame($key, $options->selectSignatureKey(new JWA())); | ||
$this->assertSame('foo', $options->kid()); | ||
$this->assertSame(['alg' => 'RS512', 'kid' => 'foo'], $options->headers()); | ||
} | ||
} |
Oops, something went wrong.