welcome back to dyb-tech

This commit is contained in:
Daniel Guzman
2024-05-18 02:28:01 +02:00
parent 9513cdba09
commit 9f30bc98c7
6149 changed files with 668407 additions and 0 deletions
@@ -0,0 +1,131 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\JWSProvider;
@trigger_error(sprintf('The "%s" class is deprecated since version 2.5 and will be removed in 3.0. Use "%s" or create your own "%s" implementation instead.', DefaultJWSProvider::class, LcobucciJWSProvider::class, JWSProviderInterface::class), E_USER_DEPRECATED);
use Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader\KeyLoaderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Signature\CreatedJWS;
use Lexik\Bundle\JWTAuthenticationBundle\Signature\LoadedJWS;
use Namshi\JOSE\JWS;
/**
* JWS Provider, Namshi\JOSE library integration.
* Supports OpenSSL and phpseclib crypto engines.
*
* @final
*
* @author Robin Chalas <robin.chalas@gmail.com>
*
* @deprecated since version 2.5, to be removed in 3.0
*/
class DefaultJWSProvider implements JWSProviderInterface
{
/**
* @var KeyLoaderInterface
*/
private $keyLoader;
/**
* @var string
*/
private $cryptoEngine;
/**
* @var string
*/
private $signatureAlgorithm;
/**
* @var int
*/
private $ttl;
/**
* @var int
*/
private $clockSkew;
/**
* @param string $cryptoEngine
* @param string $signatureAlgorithm
* @param int $ttl
* @param int $clockSkew
*
* @throws \InvalidArgumentException If the given algorithm is not supported
*/
public function __construct(KeyLoaderInterface $keyLoader, $cryptoEngine, $signatureAlgorithm, $ttl, $clockSkew)
{
if (null !== $ttl && !is_numeric($ttl)) {
throw new \InvalidArgumentException(sprintf('The TTL should be a numeric value, got %s instead.', $ttl));
}
if (null !== $clockSkew && !is_numeric($clockSkew)) {
throw new \InvalidArgumentException(sprintf('The clock skew should be a numeric value, got %s instead.', $clockSkew));
}
$cryptoEngine = 'openssl' == $cryptoEngine ? 'OpenSSL' : 'SecLib';
if (!$this->isAlgorithmSupportedForEngine($cryptoEngine, $signatureAlgorithm)) {
throw new \InvalidArgumentException(sprintf('The algorithm "%s" is not supported for %s', $signatureAlgorithm, $cryptoEngine));
}
$this->keyLoader = $keyLoader;
$this->cryptoEngine = $cryptoEngine;
$this->signatureAlgorithm = $signatureAlgorithm;
$this->ttl = $ttl;
$this->clockSkew = $clockSkew;
}
/**
* {@inheritdoc}
*/
public function create(array $payload, array $header = [])
{
$header['alg'] = $this->signatureAlgorithm;
$jws = new JWS($header, $this->cryptoEngine);
$claims = ['iat' => time()];
if (null !== $this->ttl && !isset($payload['exp'])) {
$claims['exp'] = time() + $this->ttl;
}
$jws->setPayload($payload + $claims);
$jws->sign(
$this->keyLoader->loadKey('private'),
$this->keyLoader->getPassphrase()
);
return new CreatedJWS($jws->getTokenString(), $jws->isSigned());
}
/**
* {@inheritdoc}
*/
public function load($token)
{
$jws = JWS::load($token, false, null, $this->cryptoEngine);
return new LoadedJWS(
$jws->getPayload(),
$jws->verify($this->keyLoader->loadKey('public'), $this->signatureAlgorithm),
null !== $this->ttl,
$jws->getHeader(),
$this->clockSkew
);
}
/**
* @param string $cryptoEngine
* @param string $signatureAlgorithm
*
* @return bool
*/
private function isAlgorithmSupportedForEngine($cryptoEngine, $signatureAlgorithm)
{
$signerClass = sprintf('Namshi\\JOSE\\Signer\\%s\\%s', $cryptoEngine, $signatureAlgorithm);
return class_exists($signerClass);
}
}
@@ -0,0 +1,30 @@
<?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.
*
* @return CreatedJWS
*/
public function create(array $payload, array $header = []);
/**
* Loads an existing JWS signature from a given JWT token.
*
* @param string $token
*
* @return LoadedJWS
*/
public function load($token);
}
@@ -0,0 +1,283 @@
<?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;
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\Plain;
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 Lcobucci\JWT\ValidationData;
use Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader\KeyLoaderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader\RawKeyLoader;
use Lexik\Bundle\JWTAuthenticationBundle\Signature\CreatedJWS;
use Lexik\Bundle\JWTAuthenticationBundle\Signature\LoadedJWS;
/**
* @final
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class LcobucciJWSProvider implements JWSProviderInterface
{
/**
* @var KeyLoaderInterface
*/
private $keyLoader;
/**
* @var Clock
*/
private $clock;
/**
* @var Signer
*/
private $signer;
/**
* @var int|null
*/
private $ttl;
/**
* @var int|null
*/
private $clockSkew;
/**
* @var bool
*/
private $allowNoExpiration;
/**
* @throws \InvalidArgumentException If the given crypto engine is not supported
*/
public function __construct(KeyLoaderInterface $keyLoader, string $cryptoEngine, string $signatureAlgorithm, ?int $ttl, ?int $clockSkew, bool $allowNoExpiration = false, Clock $clock = null)
{
if ('openssl' !== $cryptoEngine) {
throw new \InvalidArgumentException(sprintf('The %s provider supports only "openssl" as crypto engine.', self::class));
}
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 = [])
{
if (class_exists(JWTBuilder::class)) {
$jws = new JWTBuilder(new JoseEncoder(), ChainedFormatter::default());
} else {
$jws = new Builder();
}
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);
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']);
}
if (interface_exists(RegisteredClaims::class)) {
$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 $e) {
}
return new CreatedJWS((string) $token, null === $e);
}
/**
* {@inheritdoc}
*/
public function load($token)
{
if (class_exists(JWTParser::class)) {
$jws = (new JWTParser(new JoseEncoder()))->parse((string) $token);
} else {
$jws = (new Parser())->parse((string) $token);
}
$payload = [];
foreach ($jws->claims()->all() as $name => $value) {
if ($value instanceof \DateTimeInterface) {
$value = $value->getTimestamp();
}
$payload[$name] = $value;
}
$jws = new LoadedJWS(
$payload,
$this->verify($jws),
false == $this->allowNoExpiration,
$jws->headers()->all(),
$this->clockSkew
);
return $jws;
}
private function getSignerForAlgorithm($signatureAlgorithm)
{
$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)
{
if (class_exists(InMemory::class)) {
$key = InMemory::plainText($this->keyLoader->loadKey(RawKeyLoader::TYPE_PRIVATE), $this->signer instanceof Hmac ? '' : (string) $this->keyLoader->getPassphrase());
} else {
$key = new Key($this->keyLoader->loadKey(RawKeyLoader::TYPE_PRIVATE), $this->signer instanceof Hmac ? '' : (string) $this->keyLoader->getPassphrase());
}
$token = $jws->getToken($this->signer, $key);
if (!$token instanceof Plain) {
return (string) $token;
}
return $token->toString();
}
private function verify(Token $jwt)
{
if (class_exists(InMemory::class)) {
$key = InMemory::plainText($this->signer instanceof Hmac ? $this->keyLoader->loadKey(RawKeyLoader::TYPE_PRIVATE) : $this->keyLoader->loadKey(RawKeyLoader::TYPE_PUBLIC));
} else {
$key = new Key($this->signer instanceof Hmac ? $this->keyLoader->loadKey(RawKeyLoader::TYPE_PRIVATE) : $this->keyLoader->loadKey(RawKeyLoader::TYPE_PUBLIC));
}
$validator = new Validator();
$classValidator = class_exists(LooseValidAt::class) ? LooseValidAt::class : ValidAt::class;
$isValid = $validator->validate(
$jwt,
new $classValidator($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 $classValidator($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,190 @@
<?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 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 JWTManagerInterface, JWTTokenManagerInterface
{
/**
* @var JWTEncoderInterface
*/
protected $jwtEncoder;
/**
* @var EventDispatcherInterface
*/
protected $dispatcher;
/**
* @var string
*
* @deprecated since v2.15
*/
protected $userIdentityField;
/**
* @var string
*/
protected $userIdClaim;
/**
* @param string|null $userIdClaim
*/
public function __construct(JWTEncoderInterface $encoder, EventDispatcherInterface $dispatcher, $userIdClaim = null)
{
$this->jwtEncoder = $encoder;
$this->dispatcher = $dispatcher;
$this->userIdentityField = 'username';
$this->userIdClaim = $userIdClaim;
}
/**
* @return string The JWT token
*/
public function create(UserInterface $user): string
{
$payload = ['roles' => $user->getRoles()];
$this->addUserIdentityToPayload($user, $payload);
return $this->generateJwtStringAndDispatchEvents($user, $payload);
}
/**
* @return string The JWT token
*/
public function createFromPayload(UserInterface $user, array $payload): string
{
$payload = array_merge(['roles' => $user->getRoles()], $payload);
$this->addUserIdentityToPayload($user, $payload);
return $this->generateJwtStringAndDispatchEvents($user, $payload);
}
/**
* @return string The JWT token
*/
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)
{
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}
*/
public function parse(string $jwtToken): array
{
$payload = $this->jwtEncoder->decode($jwtToken);
$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.
*
* @param array &$payload
*/
protected function addUserIdentityToPayload(UserInterface $user, array &$payload)
{
$accessor = PropertyAccess::createPropertyAccessor();
$identityField = $this->userIdClaim ?: $this->userIdentityField;
if ($user instanceof InMemoryUser && 'username' === $identityField) {
$payload[$identityField] = $accessor->getValue($user, 'userIdentifier');
return;
}
$payload[$identityField] = $accessor->getValue($user, $accessor->isReadable($user, $identityField) ? $identityField : 'user_identifier');
}
/**
* {@inheritdoc}
*/
public function getUserIdentityField(): string
{
if (0 === func_num_args() || func_get_arg(0)) {
trigger_deprecation('lexik/jwt-authentication-bundle', '2.15', 'The "%s()" method is deprecated.', __METHOD__);
}
return $this->userIdentityField;
}
/**
* {@inheritdoc}
*/
public function setUserIdentityField($field)
{
if (1 >= func_num_args() || func_get_arg(1)) {
trigger_deprecation('lexik/jwt-authentication-bundle', '2.15', 'The "%s()" method is deprecated.', __METHOD__);
}
$this->userIdentityField = $field;
}
/**
* @return string
*/
public function getUserIdClaim(): ?string
{
return $this->userIdClaim;
}
}
@@ -0,0 +1,26 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* JWTManagerInterface.
*
* @deprecated since 2.0, removed in 3.0. Use {@link JWTTokenManagerInterface} instead
*
* @author Nicolas Cabot <n.cabot@lexik.fr>
*/
interface JWTManagerInterface
{
/**
* @return string The JWT token
*/
public function create(UserInterface $user);
/**
* @return array|false The JWT token payload or false if an error occurs
*/
public function decode(TokenInterface $token);
}
@@ -0,0 +1,56 @@
<?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>
*
* @method string createFromPayload(UserInterface $user, array $payload = []);
* @method array parse(string $token) Parses a raw JWT token and returns its payload
*/
interface JWTTokenManagerInterface
{
/**
* @return string The JWT token
*/
public function create(UserInterface $user);
/**
* @return array|false The JWT token payload or false if an error occurs
* @throws JWTDecodeFailureException
*/
public function decode(TokenInterface $token);
/**
* Sets the field used as identifier to load an user from a JWT payload.
*
* @param string $field
*
* @deprecated since 2.15, use {@see UserInterface::getUserIdentifier()} instead
*/
public function setUserIdentityField($field);
/**
* Returns the field used as identifier to load an user from a JWT payload.
*
* @return string
*
* @deprecated since 2.15, use {@see UserInterface::getUserIdentifier()} instead
*/
public function getUserIdentityField();
/**
* Returns the claim used as identifier to load an user from a JWT payload.
*
* @return string
*/
public function getUserIdClaim();
}
@@ -0,0 +1,117 @@
<?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 $signingKey;
private $publicKey;
private $passphrase;
private $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()
{
return $this->passphrase;
}
public function getSigningKey()
{
return $this->signingKey && is_file($this->signingKey) ? $this->readKey(self::TYPE_PRIVATE) : $this->signingKey;
}
public function getPublicKey()
{
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 = $key;
if (is_file($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;
}
/**
* @param string $type One of "public" or "private"
*
* @return string The path of the key, an empty string if not a valid path
*
* @throws \InvalidArgumentException If the given type is not valid
* @throws \InvalidArgumentException If the given type is not valid
*/
protected function getKeyPath($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));
}
$path = self::TYPE_PUBLIC === $type ? $this->publicKey : $this->signingKey;
if (!is_file($path) || !is_readable($path)) {
throw new \RuntimeException(sprintf('%s key is not a file or is not readable.', ucfirst($type)));
}
return $path;
}
private function readKey($type)
{
$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,33 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader;
/**
* Interface for classes that are able to load crypto keys.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*
* @method string|null getPublicKey()
* @method string|null getSigningKey()
* @method array getAdditionalPublicKeys()
*/
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);
/**
* @return string|null
*/
public function getPassphrase();
}
@@ -0,0 +1,67 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader;
@trigger_error(sprintf('The "%s\OpenSSLKeyLoader" class is deprecated since version 2.5 and will be removed in 3.0. Use "%s" instead.', __NAMESPACE__, RawKeyLoader::class), E_USER_DEPRECATED);
/**
* Load crypto keys for the OpenSSL crypto engine.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*
* @deprecated since version 2.5, to be removed in 3.0. Use RawKeyLoader instead
*/
class OpenSSLKeyLoader extends AbstractKeyLoader implements KeyDumperInterface
{
/**
* {@inheritdoc}
*
* @throws \RuntimeException If the key cannot be read
* @throws \RuntimeException Either the key or the passphrase is not valid
*/
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));
}
$keyPath = $this->getKeyPath($type);
$rawKey = @file_get_contents($keyPath);
if (false === $rawKey) {
// Try invalidating the realpath cache
clearstatcache(true, $keyPath);
$rawKey = file_get_contents($keyPath);
}
$key = call_user_func_array("openssl_pkey_get_$type", self::TYPE_PRIVATE == $type ? [$rawKey, $this->getPassphrase()] : [$rawKey]);
if (!$key) {
$sslError = '';
while ($msg = trim(openssl_error_string(), " \n\r\t\0\x0B\"")) {
if ('error:' === substr($msg, 0, 6)) {
$msg = substr($msg, 6);
}
$sslError .= "\n $msg";
}
throw new \RuntimeException(sprintf('Failed to load %s key: %s', $type, $sslError));
}
return $key;
}
/**
* {@inheritdoc}
*/
public function dumpKey()
{
$key = openssl_pkey_get_details($this->loadKey('public'));
if (!isset($key['key'])) {
return;
}
return $key['key'];
}
}
@@ -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,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,131 @@
<?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'], $keyEncryptionAlgorithms, $contentEncryptionAlgorithms, [], $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;
}
}