The start of something beautiful
This commit is contained in:
+141
@@ -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);
|
||||
}
|
||||
}
|
||||
+126
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user