The start of something beautiful
This commit is contained in:
Vendored
+66
@@ -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']);
|
||||
}
|
||||
}
|
||||
+23
@@ -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;
|
||||
}
|
||||
+24
@@ -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;
|
||||
}
|
||||
+228
@@ -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();
|
||||
}
|
||||
+90
@@ -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;
|
||||
}
|
||||
}
|
||||
+16
@@ -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();
|
||||
}
|
||||
+31
@@ -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;
|
||||
}
|
||||
}
|
||||
+26
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
+14
@@ -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));
|
||||
}
|
||||
}
|
||||
+10
@@ -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;
|
||||
}
|
||||
+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