The start of something beautiful

This commit is contained in:
2024-09-11 22:48:07 -06:00
parent 45acea47f3
commit f5997ee5ec
5614 changed files with 630696 additions and 0 deletions
@@ -0,0 +1,141 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\WebToken;
use Jose\Bundle\JoseFramework\Services\JWEBuilder;
use Jose\Bundle\JoseFramework\Services\JWEBuilderFactory;
use Jose\Bundle\JoseFramework\Services\JWSBuilder;
use Jose\Bundle\JoseFramework\Services\JWSBuilderFactory;
use Jose\Component\Core\JWK;
use Jose\Component\Encryption\Serializer\CompactSerializer as JweCompactSerializer;
use Jose\Component\Signature\Serializer\CompactSerializer as JwsCompactSerializer;
use Lexik\Bundle\JWTAuthenticationBundle\Event\BeforeJWEComputationEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
final class AccessTokenBuilder
{
/**
* @var JWSBuilder
*/
private $jwsBuilder;
/**
* @var null|JWEBuilder
*/
private $jweBuilder = null;
/**
* @var JWK
*/
private $signatureKey;
/**
* @var JWK|null
*/
private $encryptionKey;
/**
* @var string
*/
private $signatureAlgorithm;
/**
* @var string|null
*/
private $keyEncryptionAlgorithm;
/**
* @var string|null
*/
private $contentEncryptionAlgorithm;
/**
* @var EventDispatcherInterface
*/
private $dispatcher;
public function __construct(
EventDispatcherInterface $dispatcher,
JWSBuilderFactory $jwsBuilderFactory,
?JWEBuilderFactory $jweBuilderFactory,
string $signatureAlgorithm,
string $signatureKey,
?string $keyEncryptionAlgorithm,
?string $contentEncryptionAlgorithm,
?string $encryptionKey
) {
$this->jwsBuilder = $jwsBuilderFactory->create([$signatureAlgorithm]);
if ($jweBuilderFactory !== null && $keyEncryptionAlgorithm !== null && $contentEncryptionAlgorithm !== null) {
$this->jweBuilder = $jweBuilderFactory->create([$keyEncryptionAlgorithm, $contentEncryptionAlgorithm]);
}
$this->signatureKey = JWK::createFromJson($signatureKey);
$this->encryptionKey = $encryptionKey ? JWK::createFromJson($encryptionKey) : null;
$this->signatureAlgorithm = $signatureAlgorithm;
$this->keyEncryptionAlgorithm = $keyEncryptionAlgorithm;
$this->contentEncryptionAlgorithm = $contentEncryptionAlgorithm;
$this->dispatcher = $dispatcher;
}
public function build(array $header, array $claims): string
{
$token = $this->buildJWS($header, $claims);
if ($this->jweBuilder !== null) {
$token = $this->buildJWE($claims, $token);
}
return $token;
}
/**
* @param array<string, mixed> $header
* @param array<string, mixed> $claims
*/
private function buildJWS(array $header, array $claims): string
{
$header['alg'] = $this->signatureAlgorithm;
if ($this->signatureKey->has('kid')) {
$header['kid'] = $this->signatureKey->get('kid');
}
$jws = $this->jwsBuilder
->create()
->withPayload(json_encode($claims, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE))
->addSignature($this->signatureKey, $header)
->build()
;
$token = (new JwsCompactSerializer())->serialize($jws);
return $token;
}
/**
* @param array<string, mixed> $header
*/
private function buildJWE(array $claims, string $payload): string
{
$header = [
'alg' => $this->keyEncryptionAlgorithm,
'enc' => $this->contentEncryptionAlgorithm,
'cty' => 'JWT',
'typ' => 'JWT',
];
if ($this->encryptionKey->has('kid')) {
$header['kid'] = $this->encryptionKey->get('kid');
}
foreach (['exp', 'iat', 'nbf'] as $claim) {
if (array_key_exists($claim, $claims)) {
$header[$claim] = $claims[$claim];
}
}
$event = $this->dispatcher->dispatch(new BeforeJWEComputationEvent($header), Events::BEFORE_JWE_COMPUTATION);
$jwe = $this->jweBuilder
->create()
->withPayload($payload)
->withSharedProtectedHeader($event->getHeader())
->addRecipient($this->encryptionKey)
->build()
;
return (new JweCompactSerializer())->serialize($jwe);
}
}
@@ -0,0 +1,126 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\WebToken;
use Jose\Bundle\JoseFramework\Services\ClaimCheckerManager;
use Jose\Bundle\JoseFramework\Services\ClaimCheckerManagerFactory;
use Jose\Bundle\JoseFramework\Services\HeaderCheckerManager;
use Jose\Bundle\JoseFramework\Services\JWELoader;
use Jose\Bundle\JoseFramework\Services\JWELoaderFactory;
use Jose\Bundle\JoseFramework\Services\JWSLoader;
use Jose\Bundle\JoseFramework\Services\JWSLoaderFactory;
use Jose\Component\Checker\InvalidClaimException;
use Jose\Component\Checker\MissingMandatoryClaimException;
use Jose\Component\Core\JWKSet;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
final class AccessTokenLoader
{
private $jwsLoader;
private $jwsHeaderCheckerManager;
private $claimCheckerManager;
private $jweLoader;
private $signatureKeyset;
private $encryptionKeyset;
/**
* @var string[]
*/
private $mandatoryClaims;
private $continueOnDecryptionFailure;
public function __construct(
JWSLoaderFactory $jwsLoaderFactory,
?JWELoaderFactory $jweLoaderFactory,
ClaimCheckerManagerFactory $claimCheckerManagerFactory,
array $claimChecker,
array $jwsHeaderChecker,
array $mandatoryClaims,
array $signatureAlgorithms,
string $signatureKeyset,
?bool $continueOnDecryptionFailure,
?array $jweHeaderChecker,
?array $keyEncryptionAlgorithms,
?array $contentEncryptionAlgorithms,
?string $encryptionKeyset
) {
$this->jwsLoader = $jwsLoaderFactory->create(['jws_compact'], $signatureAlgorithms, $jwsHeaderChecker);
if ($jweLoaderFactory !== null && $keyEncryptionAlgorithms !== null && $contentEncryptionAlgorithms !== null && $jweHeaderChecker !== null) {
$this->jweLoader = $jweLoaderFactory->create(['jwe_compact'], array_merge($keyEncryptionAlgorithms, $contentEncryptionAlgorithms), null, null, $jweHeaderChecker);
$this->continueOnDecryptionFailure = $continueOnDecryptionFailure;
}
$this->signatureKeyset = JWKSet::createFromJson($signatureKeyset);
$this->encryptionKeyset = $encryptionKeyset ? JWKSet::createFromJson($encryptionKeyset) : null;
$this->claimCheckerManager = $claimCheckerManagerFactory->create($claimChecker);
$this->mandatoryClaims = $mandatoryClaims;
}
public function load(string $token): array
{
$token = $this->loadJWE($token);
$data = $this->loadJWS($token);
try {
$this->claimCheckerManager->check($data, $this->mandatoryClaims);
} catch (MissingMandatoryClaimException|InvalidClaimException $e) {
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, $e->getMessage(), $e, $data);
} catch (\Throwable $e) {
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Unable to load the token', $e, $data);
}
return $data;
}
/**
* @return array<string, mixed>
*/
private function loadJWS(string $token): array
{
$payload = null;
$data = null;
$signature = null;
try {
$jws = $this->jwsLoader->loadAndVerifyWithKeySet($token, $this->signatureKeyset, $signature);
} catch (\Throwable $e) {
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid token. The token cannot be loaded or the signature cannot be verified.');
}
if ($signature !== 0) {
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid token. The token shall contain only one signature.');
}
$payload = $jws->getPayload();
if (!$payload) {
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid payload. The token shall contain claims.');
}
$data = json_decode($payload, true);
if (!is_array($data)) {
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid payload. The token shall contain claims.');
}
return $data;
}
private function loadJWE(string $token): string
{
if (!$this->jweLoader) {
return $token;
}
$recipient = null;
try {
$jwe = $this->jweLoader->loadAndDecryptWithKeySet($token, $this->encryptionKeyset, $recipient);
} catch (\Throwable $e) {
if ($this->continueOnDecryptionFailure === true) {
return $token;
}
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid token. The token cannot be decrypted.', $e);
}
$token = $jwe->getPayload();
if (!$token || $recipient !== 0) {
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid token. The token has no valid content.');
}
return $token;
}
}