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,66 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\BlockedToken;
use DateInterval;
use DateTimeImmutable;
use DateTimeZone;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\MissingClaimException;
use Lexik\Bundle\JWTAuthenticationBundle\Services\BlockedTokenManagerInterface;
use Psr\Cache\CacheItemPoolInterface;
class CacheItemPoolBlockedTokenManager implements BlockedTokenManagerInterface
{
private $cacheJwt;
public function __construct(CacheItemPoolInterface $cacheJwt)
{
$this->cacheJwt = $cacheJwt;
}
public function add(array $payload): bool
{
if (!isset($payload['exp'])) {
throw new MissingClaimException('exp');
}
$expiration = new DateTimeImmutable('@' . $payload['exp'], new DateTimeZone('UTC'));
$now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
// If the token is already expired, there's no point in adding it to storage
if ($expiration <= $now) {
return false;
}
$cacheExpiration = $expiration->add(new DateInterval('PT5M'));
if (!isset($payload['jti'])) {
throw new MissingClaimException('jti');
}
$cacheItem = $this->cacheJwt->getItem($payload['jti']);
$cacheItem->set([]);
$cacheItem->expiresAt($cacheExpiration);
$this->cacheJwt->save($cacheItem);
return true;
}
public function has(array $payload): bool
{
if (!isset($payload['jti'])) {
throw new MissingClaimException('jti');
}
return $this->cacheJwt->hasItem($payload['jti']);
}
public function remove(array $payload): void
{
if (!isset($payload['jti'])) {
throw new MissingClaimException('jti');
}
$this->cacheJwt->deleteItem($payload['jti']);
}
}
@@ -0,0 +1,23 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\MissingClaimException;
interface BlockedTokenManagerInterface
{
/**
* @throws MissingClaimException if required claims do not exist in the payload
*/
public function add(array $payload): bool;
/**
* @throws MissingClaimException if required claims do not exist in the payload
*/
public function has(array $payload): bool;
/**
* @throws MissingClaimException if required claims do not exist in the payload
*/
public function remove(array $payload): void;
}
@@ -0,0 +1,24 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\JWSProvider;
use Lexik\Bundle\JWTAuthenticationBundle\Signature\CreatedJWS;
use Lexik\Bundle\JWTAuthenticationBundle\Signature\LoadedJWS;
/**
* Interface for classes that are able to create and load JSON web signatures (JWS).
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
interface JWSProviderInterface
{
/**
* Creates a new JWS signature from a given payload.
*/
public function create(array $payload, array $header = []): CreatedJWS;
/**
* Loads an existing JWS signature from a given JWT token.
*/
public function load(string $token): LoadedJWS;
}
@@ -0,0 +1,228 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\JWSProvider;
use Lcobucci\Clock\Clock;
use Lcobucci\Clock\SystemClock;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Encoding\ChainedFormatter;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Ecdsa;
use Lcobucci\JWT\Signer\Hmac;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Hmac\Sha384;
use Lcobucci\JWT\Signer\Hmac\Sha512;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\Token\Builder as JWTBuilder;
use Lcobucci\JWT\Token\Parser as JWTParser;
use Lcobucci\JWT\Token\RegisteredClaims;
use Lcobucci\JWT\Validation\Constraint\LooseValidAt;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\Constraint\ValidAt;
use Lcobucci\JWT\Validation\Validator;
use Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader\KeyLoaderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Signature\CreatedJWS;
use Lexik\Bundle\JWTAuthenticationBundle\Signature\LoadedJWS;
/**
* @final
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class LcobucciJWSProvider implements JWSProviderInterface
{
private KeyLoaderInterface $keyLoader;
private Clock $clock;
private Signer $signer;
private ?int $ttl;
private ?int $clockSkew;
private bool $allowNoExpiration;
/**
* @throws \InvalidArgumentException If the given crypto engine is not supported
*/
public function __construct(KeyLoaderInterface $keyLoader, string $signatureAlgorithm, ?int $ttl, ?int $clockSkew, bool $allowNoExpiration = false, Clock $clock = null)
{
if (null === $clock) {
$clock = new SystemClock(new \DateTimeZone('UTC'));
}
$this->keyLoader = $keyLoader;
$this->clock = $clock;
$this->signer = $this->getSignerForAlgorithm($signatureAlgorithm);
$this->ttl = $ttl;
$this->clockSkew = $clockSkew;
$this->allowNoExpiration = $allowNoExpiration;
}
/**
* {@inheritdoc}
*/
public function create(array $payload, array $header = []): CreatedJWS
{
$jws = new JWTBuilder(new JoseEncoder(), ChainedFormatter::default());
foreach ($header as $k => $v) {
$jws = $jws->withHeader($k, $v);
}
$now = time();
$issuedAt = $payload['iat'] ?? $now;
unset($payload['iat']);
$jws = $jws->issuedAt(!$issuedAt instanceof \DateTimeImmutable ? new \DateTimeImmutable("@{$issuedAt}") : $issuedAt);
$exp = null;
if (null !== $this->ttl || isset($payload['exp'])) {
$exp = $payload['exp'] ?? $now + $this->ttl;
unset($payload['exp']);
}
if ($exp) {
$jws = $jws->expiresAt(!$exp instanceof \DateTimeImmutable ? new \DateTimeImmutable("@{$exp}") : $exp);
}
if (isset($payload['sub'])) {
$jws = $jws->relatedTo($payload['sub']);
unset($payload['sub']);
}
$jws = $this->addStandardClaims($jws, $payload);
foreach ($payload as $name => $value) {
$jws = $jws->withClaim($name, $value);
}
$e = $token = null;
try {
$token = $this->getSignedToken($jws);
} catch (\InvalidArgumentException) {
}
return new CreatedJWS((string) $token, null === $e);
}
/**
* {@inheritdoc}
*/
public function load(string $token): LoadedJWS
{
$jws = (new JWTParser(new JoseEncoder()))->parse($token);
$payload = [];
foreach ($jws->claims()->all() as $name => $value) {
if ($value instanceof \DateTimeInterface) {
$value = $value->getTimestamp();
}
$payload[$name] = $value;
}
return new LoadedJWS(
$payload,
$this->verify($jws),
false == $this->allowNoExpiration,
$jws->headers()->all(),
$this->clockSkew
);
}
private function getSignerForAlgorithm($signatureAlgorithm): Signer
{
$signerMap = [
'HS256' => Sha256::class,
'HS384' => Sha384::class,
'HS512' => Sha512::class,
'RS256' => Signer\Rsa\Sha256::class,
'RS384' => Signer\Rsa\Sha384::class,
'RS512' => Signer\Rsa\Sha512::class,
'ES256' => Signer\Ecdsa\Sha256::class,
'ES384' => Signer\Ecdsa\Sha384::class,
'ES512' => Signer\Ecdsa\Sha512::class,
];
if (!isset($signerMap[$signatureAlgorithm])) {
throw new \InvalidArgumentException(sprintf('The algorithm "%s" is not supported by %s', $signatureAlgorithm, self::class));
}
$signerClass = $signerMap[$signatureAlgorithm];
if (is_subclass_of($signerClass, Ecdsa::class) && method_exists($signerClass, 'create')) {
return $signerClass::create();
}
return new $signerClass();
}
private function getSignedToken(Builder $jws): string
{
$key = InMemory::plainText($this->keyLoader->loadKey(KeyLoaderInterface::TYPE_PRIVATE), $this->signer instanceof Hmac ? '' : (string) $this->keyLoader->getPassphrase());
$token = $jws->getToken($this->signer, $key);
return $token->toString();
}
private function verify(Token $jwt): bool
{
$key = InMemory::plainText($this->signer instanceof Hmac ? $this->keyLoader->loadKey(KeyLoaderInterface::TYPE_PRIVATE) : $this->keyLoader->loadKey(KeyLoaderInterface::TYPE_PUBLIC));
$validator = new Validator();
$isValid = $validator->validate(
$jwt,
new LooseValidAt($this->clock, new \DateInterval("PT{$this->clockSkew}S")),
new SignedWith($this->signer, $key)
);
$publicKeys = $this->keyLoader->getAdditionalPublicKeys();
if ($isValid || $this->signer instanceof Hmac || empty($publicKeys)) {
return $isValid;
}
// If the key used to verify the token is invalid, and it's not Hmac algorithm, try with additional public keys
foreach ($publicKeys as $key) {
$isValid = $validator->validate(
$jwt,
new LooseValidAt($this->clock, new \DateInterval("PT{$this->clockSkew}S")),
new SignedWith($this->signer, InMemory::plainText($key))
);
if ($isValid) {
return true;
}
}
return false;
}
private function addStandardClaims(Builder $builder, array &$payload): Builder
{
$mutatorMap = [
RegisteredClaims::AUDIENCE => 'permittedFor',
RegisteredClaims::ID => 'identifiedBy',
RegisteredClaims::ISSUER => 'issuedBy',
RegisteredClaims::NOT_BEFORE => 'canOnlyBeUsedAfter',
];
foreach ($payload as $claim => $value) {
if (!isset($mutatorMap[$claim])) {
continue;
}
$mutator = $mutatorMap[$claim];
unset($payload[$claim]);
if (\is_array($value)) {
$builder = \call_user_func_array([$builder, $mutator], $value);
continue;
}
$builder = $builder->{$mutator}($value);
}
return $builder;
}
}
@@ -0,0 +1,148 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services;
use Lexik\Bundle\JWTAuthenticationBundle\Encoder\HeaderAwareJWTEncoderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTEncodedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTEncodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichment\NullEnrichment;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\InMemoryUser;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Provides convenient methods to manage JWT creation/verification.
*
* @author Nicolas Cabot <n.cabot@lexik.fr>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class JWTManager implements JWTTokenManagerInterface
{
protected JWTEncoderInterface $jwtEncoder;
protected EventDispatcherInterface $dispatcher;
protected string $userIdClaim;
private $payloadEnrichment;
public function __construct(JWTEncoderInterface $encoder, EventDispatcherInterface $dispatcher, string $userIdClaim, PayloadEnrichmentInterface $payloadEnrichment = null)
{
$this->jwtEncoder = $encoder;
$this->dispatcher = $dispatcher;
$this->userIdClaim = $userIdClaim;
$this->payloadEnrichment = $payloadEnrichment ?? new NullEnrichment();
}
/**
* @return string The JWT token
*
* @throws JWTEncodeFailureException
*/
public function create(UserInterface $user): string
{
$payload = ['roles' => $user->getRoles()];
$this->addUserIdentityToPayload($user, $payload);
$this->payloadEnrichment->enrich($user, $payload);
return $this->generateJwtStringAndDispatchEvents($user, $payload);
}
/**
* @return string The JWT token
*
* @throws JWTEncodeFailureException
*/
public function createFromPayload(UserInterface $user, array $payload = []): string
{
$payload = array_merge(['roles' => $user->getRoles()], $payload);
$this->addUserIdentityToPayload($user, $payload);
$this->payloadEnrichment->enrich($user, $payload);
return $this->generateJwtStringAndDispatchEvents($user, $payload);
}
/**
* @return string The JWT token
*
* @throws JWTEncodeFailureException
*/
private function generateJwtStringAndDispatchEvents(UserInterface $user, array $payload): string
{
$jwtCreatedEvent = new JWTCreatedEvent($payload, $user);
$this->dispatcher->dispatch($jwtCreatedEvent, Events::JWT_CREATED);
if ($this->jwtEncoder instanceof HeaderAwareJWTEncoderInterface) {
$jwtString = $this->jwtEncoder->encode($jwtCreatedEvent->getData(), $jwtCreatedEvent->getHeader());
} else {
$jwtString = $this->jwtEncoder->encode($jwtCreatedEvent->getData());
}
$jwtEncodedEvent = new JWTEncodedEvent($jwtString);
$this->dispatcher->dispatch($jwtEncodedEvent, Events::JWT_ENCODED);
return $jwtString;
}
/**
* {@inheritdoc}
*
* @throws JWTDecodeFailureException
*/
public function decode(TokenInterface $token): array|bool
{
if (!($payload = $this->jwtEncoder->decode($token->getCredentials()))) {
return false;
}
$event = new JWTDecodedEvent($payload);
$this->dispatcher->dispatch($event, Events::JWT_DECODED);
if (!$event->isValid()) {
return false;
}
return $event->getPayload();
}
/**
* {@inheritdoc}
*
* @throws JWTDecodeFailureException
*/
public function parse(string $token): array
{
$payload = $this->jwtEncoder->decode($token);
$event = new JWTDecodedEvent($payload);
$this->dispatcher->dispatch($event, Events::JWT_DECODED);
if (!$event->isValid()) {
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'The token was marked as invalid by an event listener after successful decoding.');
}
return $event->getPayload();
}
/**
* Add user identity to payload, username by default.
* Override this if you need to identify it by another property.
*/
protected function addUserIdentityToPayload(UserInterface $user, array &$payload): void
{
$accessor = PropertyAccess::createPropertyAccessor();
$payload[$this->userIdClaim] = $accessor->getValue($user, $accessor->isReadable($user, $this->userIdClaim) ? $this->userIdClaim : 'user_identifier');
}
public function getUserIdClaim(): string
{
return $this->userIdClaim;
}
}
@@ -0,0 +1,42 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* JWTTokenManagerInterface must be implemented by classes able to create/decode
* JWT tokens.
*
* @author Robin Chalas <robin.chalas@gmail.com>
* @author Eric Lannez <eric.lannez@gmail.com>
*/
interface JWTTokenManagerInterface
{
/**
* @return string The JWT token
*/
public function create(UserInterface $user);
public function createFromPayload(UserInterface $user, array $payload = []): string;
/**
* @return array|false The JWT token payload or false if an error occurs
* @throws JWTDecodeFailureException
*/
public function decode(TokenInterface $token): array|bool;
/**
* Parses a raw JWT token and returns its payload
*/
public function parse(string $token): array;
/**
* Returns the claim used as identifier to load an user from a JWT payload.
*
* @return string
*/
public function getUserIdClaim();
}
@@ -0,0 +1,90 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader;
/**
* Abstract class for key loaders.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*
* @internal
*/
abstract class AbstractKeyLoader implements KeyLoaderInterface
{
private ?string $signingKey;
private ?string $publicKey;
private ?string $passphrase;
private array $additionalPublicKeys;
public function __construct(?string $signingKey = null, ?string $publicKey = null, ?string $passphrase = null, array $additionalPublicKeys = [])
{
$this->signingKey = $signingKey;
$this->publicKey = $publicKey;
$this->passphrase = $passphrase;
$this->additionalPublicKeys = $additionalPublicKeys;
}
/**
* {@inheritdoc}
*/
public function getPassphrase(): ?string
{
return $this->passphrase;
}
public function getSigningKey(): ?string
{
return $this->signingKey && is_file($this->signingKey) ? $this->readKey(self::TYPE_PRIVATE) : $this->signingKey;
}
public function getPublicKey(): ?string
{
return $this->publicKey && is_file($this->publicKey) ? $this->readKey(self::TYPE_PUBLIC) : $this->publicKey;
}
public function getAdditionalPublicKeys(): array
{
$contents = [];
foreach ($this->additionalPublicKeys as $key) {
if (!$key || !is_file($key) || !is_readable($key)) {
throw new \RuntimeException(sprintf('Additional public key "%s" does not exist or is not readable. Did you correctly set the "lexik_jwt_authentication.additional_public_keys" configuration key?', $key));
}
$rawKey = @file_get_contents($key);
if (false === $rawKey) {
// Try invalidating the realpath cache
clearstatcache(true, $key);
$rawKey = file_get_contents($key);
}
$contents[] = $rawKey;
}
return $contents;
}
private function readKey($type): ?string
{
$isPublic = self::TYPE_PUBLIC === $type;
$key = $isPublic ? $this->publicKey : $this->signingKey;
if (!$key || !is_file($key) || !is_readable($key)) {
if ($isPublic) {
return null;
}
throw new \RuntimeException(sprintf('Signature key "%s" does not exist or is not readable. Did you correctly set the "lexik_jwt_authentication.signature_key" configuration key?', $key));
}
$rawKey = @file_get_contents($key);
if (false === $rawKey) {
// Try invalidating the realpath cache
clearstatcache(true, $key);
$rawKey = file_get_contents($key);
}
return $rawKey;
}
}
@@ -0,0 +1,16 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader;
/**
* @author Robin Chalas <robin.chalas@gmail.com>
*/
interface KeyDumperInterface
{
/**
* Dumps a key to be shared between parties.
*
* @return resource|string
*/
public function dumpKey();
}
@@ -0,0 +1,31 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader;
/**
* Interface for classes that are able to load crypto keys.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
interface KeyLoaderInterface
{
public const TYPE_PUBLIC = 'public';
public const TYPE_PRIVATE = 'private';
/**
* Loads a key from a given type (public or private).
*
* @param resource|string|null $type
*
* @return resource|string|null
*/
public function loadKey($type);
public function getPassphrase(): ?string;
public function getSigningKey(): ?string;
public function getPublicKey(): ?string;
public function getAdditionalPublicKeys(): array;
}
@@ -0,0 +1,52 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader;
/**
* Reads crypto keys.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class RawKeyLoader extends AbstractKeyLoader implements KeyDumperInterface
{
/**
* @param string $type
*
* @return string
*
* @throws \RuntimeException If the key cannot be read
*/
public function loadKey($type)
{
if (!in_array($type, [self::TYPE_PUBLIC, self::TYPE_PRIVATE])) {
throw new \InvalidArgumentException(sprintf('The key type must be "public" or "private", "%s" given.', $type));
}
if (self::TYPE_PUBLIC === $type) {
return $this->dumpKey();
}
return $this->getSigningKey();
}
/**
* {@inheritdoc}
*/
public function dumpKey()
{
if ($publicKey = $this->getPublicKey()) {
return $publicKey;
}
$signingKey = $this->getSigningKey();
// no public key provided, compute it from signing key
try {
$publicKey = openssl_pkey_get_details(openssl_pkey_get_private($signingKey, $this->getPassphrase()))['key'];
} catch (\Throwable $e) {
throw new \RuntimeException('Secret key either does not exist, is not readable or is invalid. Did you correctly set the "lexik_jwt_authentication.secret_key" config option?');
}
return $publicKey;
}
}
@@ -0,0 +1,26 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichment;
use Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichmentInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class ChainEnrichment implements PayloadEnrichmentInterface
{
private $enrichments;
/**
* @param PayloadEnrichmentInterface[] $enrichments
*/
public function __construct(array $enrichments)
{
$this->enrichments = $enrichments;
}
public function enrich(UserInterface $user, array &$payload): void
{
foreach ($this->enrichments as $enrichment) {
$enrichment->enrich($user, $payload);
}
}
}
@@ -0,0 +1,13 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichment;
use Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichmentInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class NullEnrichment implements PayloadEnrichmentInterface
{
public function enrich(UserInterface $user, array &$payload): void
{
}
}
@@ -0,0 +1,14 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichment;
use Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichmentInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class RandomJtiEnrichment implements PayloadEnrichmentInterface
{
public function enrich(UserInterface $user, array &$payload): void
{
$payload['jti'] = bin2hex(random_bytes(16));
}
}
@@ -0,0 +1,10 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services;
use Symfony\Component\Security\Core\User\UserInterface;
interface PayloadEnrichmentInterface
{
public function enrich(UserInterface $user, array &$payload): void;
}
@@ -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;
}
}