diff --git a/.gitignore b/.gitignore
index 04f7b493..2d4ac93a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ build
vendor
composer.lock
.phpunit.result.cache
+.idea
diff --git a/DependencyInjection/Security/Factory/JWTAuthenticatorFactory.php b/DependencyInjection/Security/Factory/JWTAuthenticatorFactory.php
new file mode 100644
index 00000000..cb77685a
--- /dev/null
+++ b/DependencyInjection/Security/Factory/JWTAuthenticatorFactory.php
@@ -0,0 +1,60 @@
+
+ */
+class JWTAuthenticatorFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
+ {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPosition()
+ {
+ return 'pre_auth';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getKey()
+ {
+ return 'jwt';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addConfiguration(NodeDefinition $node)
+ {
+ // no-op - no config here for now
+ }
+
+ public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId)
+ {
+ $authenticatorId = 'security.authenticator.jwt.'.$firewallName;
+ $container
+ ->setDefinition($authenticatorId, new ChildDefinition('lexik_jwt_authentication.security.jwt_authenticator'))
+ ->replaceArgument(3, new Reference($userProviderId));
+
+ return $authenticatorId;
+ }
+}
diff --git a/DependencyInjection/Security/Factory/JWTFactory.php b/DependencyInjection/Security/Factory/JWTFactory.php
index c1714895..8e926d7d 100644
--- a/DependencyInjection/Security/Factory/JWTFactory.php
+++ b/DependencyInjection/Security/Factory/JWTFactory.php
@@ -19,6 +19,13 @@
*/
class JWTFactory implements SecurityFactoryInterface
{
+ public function __construct($triggerDeprecation = true)
+ {
+ if ($triggerDeprecation) {
+ trigger_deprecation('lexik/jwt-authentication-bundle', '2.0', 'Class "%s" is deprecated, use "%s" instead.', self::class, JWTAuthenticatorFactory::class);
+ }
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/LexikJWTAuthenticationBundle.php b/LexikJWTAuthenticationBundle.php
index 323ea6df..e8348d66 100644
--- a/LexikJWTAuthenticationBundle.php
+++ b/LexikJWTAuthenticationBundle.php
@@ -3,8 +3,10 @@
namespace Lexik\Bundle\JWTAuthenticationBundle;
use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Compiler\WireGenerateTokenCommandPass;
+use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Security\Factory\JWTAuthenticatorFactory;
use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Security\Factory\JWTFactory;
use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Security\Factory\JWTUserFactory;
+use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface;
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
use Symfony\Component\Console\Application;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -30,7 +32,10 @@ public function build(ContainerBuilder $container)
$extension = $container->getExtension('security');
$extension->addUserProviderFactory(new JWTUserFactory());
- $extension->addSecurityListenerFactory(new JWTFactory()); // BC 1.x, to be removed in 3.0
+ $extension->addSecurityListenerFactory(new JWTFactory(false)); // BC 1.x, to be removed in 3.0
+ if (interface_exists(AuthenticatorFactoryInterface::class)) {
+ $extension->addSecurityListenerFactory(new JWTAuthenticatorFactory());
+ }
}
/**
diff --git a/Resources/config/token_authenticator.xml b/Resources/config/token_authenticator.xml
index df9b90b3..9b03a35d 100644
--- a/Resources/config/token_authenticator.xml
+++ b/Resources/config/token_authenticator.xml
@@ -15,5 +15,13 @@
+
+
+
+
+
+
+
+
diff --git a/Security/Authenticator/JWTAuthenticator.php b/Security/Authenticator/JWTAuthenticator.php
new file mode 100644
index 00000000..514a7a36
--- /dev/null
+++ b/Security/Authenticator/JWTAuthenticator.php
@@ -0,0 +1,222 @@
+tokenExtractor = $tokenExtractor;
+ $this->jwtManager = $jwtManager;
+ $this->eventDispatcher = $eventDispatcher;
+ $this->userProvider = $userProvider;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function start(Request $request, AuthenticationException $authException = null): JWTAuthenticationFailureResponse
+ {
+ $exception = new MissingTokenException('JWT Token not found', 0, $authException);
+ $event = new JWTNotFoundEvent($exception, new JWTAuthenticationFailureResponse($exception->getMessageKey()));
+
+ $this->eventDispatcher->dispatch($event, Events::JWT_NOT_FOUND);
+
+ return $event->getResponse();
+ }
+
+ public function supports(Request $request): ?bool
+ {
+ return false !== $this->getTokenExtractor()->extract($request);
+ }
+
+ public function authenticate(Request $request): PassportInterface
+ {
+ $token = $this->getTokenExtractor()->extract($request);
+
+ try {
+ if (!$payload = $this->jwtManager->decodeFromJsonWebToken($token)) {
+ throw new InvalidTokenException('Invalid JWT Token');
+ }
+ } catch (JWTDecodeFailureException $e) {
+ if (JWTDecodeFailureException::EXPIRED_TOKEN === $e->getReason()) {
+ throw new ExpiredTokenException();
+ }
+
+ throw new InvalidTokenException('Invalid JWT Token', 0, $e);
+ }
+
+ $idClaim = $this->jwtManager->getUserIdClaim();
+ if (!isset($payload[$idClaim])) {
+ throw new InvalidPayloadException($idClaim);
+ }
+
+ $passport = new SelfValidatingPassport(
+ new UserBadge($payload[$idClaim],
+ function ($userIdentifier) use($payload) {
+ return $this->loadUser($payload, $userIdentifier);
+ })
+ );
+
+ $passport->setAttribute('payload', $payload);
+
+ return $passport;
+ }
+
+ public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
+ {
+ return null;
+ }
+
+ public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
+ {
+ $errorMessage = strtr($exception->getMessageKey(), $exception->getMessageData());
+ $response = new JWTAuthenticationFailureResponse($errorMessage);
+
+ if ($exception instanceof ExpiredTokenException) {
+ $event = new JWTExpiredEvent($exception, $response);
+ $eventName = Events::JWT_EXPIRED;
+ } else {
+ $event = new JWTInvalidEvent($exception, $response);
+ $eventName = Events::JWT_INVALID;
+ }
+
+ $this->eventDispatcher->dispatch($event, $eventName);
+
+ return $event->getResponse();
+ }
+
+ /**
+ * Gets the token extractor to be used for retrieving a JWT token in the
+ * current request.
+ *
+ * Override this method for adding/removing extractors to the chain one or
+ * returning a different {@link TokenExtractorInterface} implementation.
+ */
+ protected function getTokenExtractor(): TokenExtractorInterface
+ {
+ return $this->tokenExtractor;
+ }
+
+ /**
+ * Loads the user to authenticate.
+ *
+ * @param array $payload The token payload
+ * @param string $identity The key from which to retrieve the user "identifier"
+ */
+ protected function loadUser(array $payload, string $identity): UserInterface
+ {
+ if ($this->userProvider instanceof PayloadAwareUserProviderInterface) {
+ if (method_exists($this->userProvider, 'loadUserByIdentifierAndPayload')) {
+ return $this->userProvider->loadUserByIdentifierAndPayload($identity, $payload);
+ } else {
+ return $this->userProvider->loadUserByUsernameAndPayload($identity, $payload);
+ }
+ }
+
+ if ($this->userProvider instanceof ChainUserProvider) {
+ foreach ($this->userProvider->getProviders() as $provider) {
+ try {
+ if ($provider instanceof PayloadAwareUserProviderInterface) {
+ if (method_exists(PayloadAwareUserProviderInterface::class, 'loadUserByIdentifierAndPayload')) {
+ return $this->userProvider->loadUserByIdentifierAndPayload($identity, $payload);
+ } else {
+ return $this->userProvider->loadUserByUsernameAndPayload($identity, $payload);
+ }
+ }
+
+ return $provider->loadUserByIdentifier($identity);
+ // More generic call to catch both UsernameNotFoundException for SF<5.3 and new UserNotFoundException
+ } catch (AuthenticationException $e) {
+ // try next one
+ }
+ }
+
+ if(!class_exists(UsernameNotFoundException::class)) {
+ $ex = new UsernameNotFoundException(sprintf('There is no user with username "%s".', $identity));
+ $ex->setUsername($identity);
+ } else {
+ $ex = new UserNotFoundException(sprintf('There is no user with identifier "%s".', $identity));
+ $ex->setUserIdentifier($identity);
+ }
+
+ throw $ex;
+ }
+
+ if (method_exists($this->userProvider, 'loadUserByIdentifier')) {
+ return $this->userProvider->loadUserByIdentifier($identity);
+ } else {
+ return $this->userProvider->loadUserByUsername($identity);
+ }
+ }
+
+ public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
+ {
+ $token = parent::createAuthenticatedToken($passport, $firewallName);
+
+ if (!$passport instanceof SelfValidatingPassport) {
+ throw new \LogicException(sprintf('Expected "%s" but got "%s".', SelfValidatingPassport::class, get_debug_type($passport)));
+ }
+
+ $this->eventDispatcher->dispatch(new JWTAuthenticatedEvent($passport->getAttribute('payload'), $token), Events::JWT_AUTHENTICATED);
+
+ return $token;
+ }
+}
diff --git a/Security/Guard/JWTTokenAuthenticator.php b/Security/Guard/JWTTokenAuthenticator.php
index f6860675..2e2a077d 100644
--- a/Security/Guard/JWTTokenAuthenticator.php
+++ b/Security/Guard/JWTTokenAuthenticator.php
@@ -30,6 +30,7 @@
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
+use Symfony\Component\Security\Guard\AuthenticatorInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
@@ -40,7 +41,7 @@
* @author Nicolas Cabot
* @author Robin Chalas
*/
-class JWTTokenAuthenticator extends AbstractGuardAuthenticator
+class JWTTokenAuthenticator implements AuthenticatorInterface
{
/**
* @var JWTTokenManagerInterface
diff --git a/Security/User/JWTUser.php b/Security/User/JWTUser.php
index 32e85db1..f2102449 100644
--- a/Security/User/JWTUser.php
+++ b/Security/User/JWTUser.php
@@ -11,15 +11,15 @@
*/
class JWTUser implements JWTUserInterface
{
- private $username;
+ private $userIdentifier;
private $roles;
/**
* @final
*/
- public function __construct(string $username, array $roles = [])
+ public function __construct(string $userIdentifier, array $roles = [])
{
- $this->username = $username;
+ $this->userIdentifier = $userIdentifier;
$this->roles = $roles;
}
@@ -38,15 +38,20 @@ public static function createFromPayload($username, array $payload)
/**
* {@inheritdoc}
*/
- public function getUsername()
+ public function getUsername(): string
{
- return $this->username;
+ return $this->getUserIdentifier();
+ }
+
+ public function getUserIdentifier(): string
+ {
+ return $this->userIdentifier;
}
/**
* {@inheritdoc}
*/
- public function getRoles()
+ public function getRoles(): array
{
return $this->roles;
}
@@ -54,7 +59,7 @@ public function getRoles()
/**
* {@inheritdoc}
*/
- public function getPassword()
+ public function getPassword(): ?string
{
return null;
}
@@ -62,7 +67,7 @@ public function getPassword()
/**
* {@inheritdoc}
*/
- public function getSalt()
+ public function getSalt(): ?string
{
return null;
}
diff --git a/Security/User/JWTUserProvider.php b/Security/User/JWTUserProvider.php
index 4a97c268..78f1ea44 100644
--- a/Security/User/JWTUserProvider.php
+++ b/Security/User/JWTUserProvider.php
@@ -27,18 +27,26 @@ public function __construct($class)
* {@inheritdoc}
*
* @param array $payload The JWT payload from which to create an instance
- *
- * @return JWTUserInterface
*/
public function loadUserByUsername($username, array $payload = [])
{
return $this->loadUserByUsernameAndPayload($username, $payload);
}
+ /**
+ * {@inheritdoc}
+ *
+ * @param array $payload The JWT payload from which to create an instance
+ */
+ public function loadUserByIdentifier(string $identifier, array $payload = []): UserInterface
+ {
+ return $this->loadUserByIdentifierAndPayload($identifier, $payload);
+ }
+
/**
* {@inheritdoc}
*/
- public function loadUserByUsernameAndPayload($username, array $payload)
+ public function loadUserByUsernameAndPayload(string $username, array $payload): UserInterface
{
$class = $this->class;
@@ -49,6 +57,17 @@ public function loadUserByUsernameAndPayload($username, array $payload)
return $this->cache[$username] = $class::createFromPayload($username, $payload);
}
+ public function loadUserByIdentifierAndPayload(string $userIdentifier, array $payload): UserInterface
+ {
+ $class = $this->class;
+
+ if (isset($this->cache[$userIdentifier])) {
+ return $this->cache[$userIdentifier];
+ }
+
+ return $this->cache[$userIdentifier] = $class::createFromPayload($userIdentifier, $payload);
+ }
+
/**
* {@inheritdoc}
*/
@@ -57,7 +76,7 @@ public function supportsClass($class)
return $class === $this->class || (new \ReflectionClass($class))->implementsInterface(JWTUserInterface::class);
}
- public function refreshUser(UserInterface $user)
+ public function refreshUser(UserInterface $user): UserInterface
{
return $user; // noop
}
diff --git a/Security/User/PayloadAwareUserProviderInterface.php b/Security/User/PayloadAwareUserProviderInterface.php
index 4627fb27..dbf29b43 100644
--- a/Security/User/PayloadAwareUserProviderInterface.php
+++ b/Security/User/PayloadAwareUserProviderInterface.php
@@ -2,20 +2,22 @@
namespace Lexik\Bundle\JWTAuthenticationBundle\Security\User;
+use Lexik\Bundle\JWTAuthenticationBundle\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
+/**
+ * @method UserInterface loadUserByIdentifierAndPayload(string $identifier) Loads a user from an identifier and JWT token payload.
+ */
interface PayloadAwareUserProviderInterface extends UserProviderInterface
{
/**
* Load a user by its username, including the JWT token payload.
*
- * @param string $username
+ * @throws UsernameNotFoundException|UserNotFoundException if the user is not found
*
- * @throws UsernameNotFoundException if the user is not found
- *
- * @return UserInterface
+ * @deprecated since 2.12, implement loadUserByIdentifierAndPayload() instead.
*/
- public function loadUserByUsernameAndPayload($username, array $payload);
+ public function loadUserByUsernameAndPayload(string $username, array $payload)/*: UserInterface*/;
}
diff --git a/Services/JWTManager.php b/Services/JWTManager.php
index a755ff4a..66e7d425 100644
--- a/Services/JWTManager.php
+++ b/Services/JWTManager.php
@@ -8,6 +8,7 @@
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\UserInterface;
@@ -55,7 +56,7 @@ public function __construct(JWTEncoderInterface $encoder, EventDispatcherInterfa
/**
* @return string The JWT token
*/
- public function create(UserInterface $user)
+ public function create(UserInterface $user): string
{
$payload = ['roles' => $user->getRoles()];
$this->addUserIdentityToPayload($user, $payload);
@@ -66,7 +67,7 @@ public function create(UserInterface $user)
/**
* @return string The JWT token
*/
- public function createFromPayload(UserInterface $user, array $payload)
+ public function createFromPayload(UserInterface $user, array $payload): string
{
$payload = array_merge(['roles' => $user->getRoles()], $payload);
$this->addUserIdentityToPayload($user, $payload);
@@ -77,7 +78,7 @@ public function createFromPayload(UserInterface $user, array $payload)
/**
* @return string The JWT token
*/
- private function generateJwtStringAndDispatchEvents(UserInterface $user, array $payload)
+ private function generateJwtStringAndDispatchEvents(UserInterface $user, array $payload): string
{
$jwtCreatedEvent = new JWTCreatedEvent($payload, $user);
$this->dispatcher->dispatch($jwtCreatedEvent, Events::JWT_CREATED);
@@ -97,6 +98,7 @@ private function generateJwtStringAndDispatchEvents(UserInterface $user, array $
/**
* {@inheritdoc}
+ * @throws JWTDecodeFailureException
*/
public function decode(TokenInterface $token)
{
@@ -114,6 +116,26 @@ public function decode(TokenInterface $token)
return $event->getPayload();
}
+ /**
+ * @inheritDoc
+ * @throws JWTDecodeFailureException
+ */
+ public function decodeFromJsonWebToken(string $jwtToken)
+ {
+ if (!($payload = $this->jwtEncoder->decode($jwtToken))) {
+ return false;
+ }
+
+ $event = new JWTDecodedEvent($payload);
+ $this->dispatcher->dispatch($event, Events::JWT_DECODED);
+
+ if (!$event->isValid()) {
+ return false;
+ }
+
+ return $event->getPayload();
+ }
+
/**
* Add user identity to payload, username by default.
* Override this if you need to identify it by another property.
@@ -129,7 +151,7 @@ protected function addUserIdentityToPayload(UserInterface $user, array &$payload
/**
* {@inheritdoc}
*/
- public function getUserIdentityField()
+ public function getUserIdentityField(): string
{
return $this->userIdentityField;
}
@@ -137,15 +159,15 @@ public function getUserIdentityField()
/**
* {@inheritdoc}
*/
- public function setUserIdentityField($userIdentityField)
+ public function setUserIdentityField($field)
{
- $this->userIdentityField = $userIdentityField;
+ $this->userIdentityField = $field;
}
/**
* @return string
*/
- public function getUserIdClaim()
+ public function getUserIdClaim(): ?string
{
return $this->userIdClaim;
}
diff --git a/Services/JWTTokenManagerInterface.php b/Services/JWTTokenManagerInterface.php
index 1c35227b..1a6e0c55 100644
--- a/Services/JWTTokenManagerInterface.php
+++ b/Services/JWTTokenManagerInterface.php
@@ -2,6 +2,7 @@
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;
@@ -23,9 +24,16 @@ 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);
+ /**
+ * @return array|false The JWT token payload or false if an error occurs
+ * @throws JWTDecodeFailureException
+ */
+ public function decodeFromJsonWebToken(string $jwtToken);
+
/**
* Sets the field used as identifier to load an user from a JWT payload.
*
diff --git a/Tests/Functional/Command/GenerateTokenCommandTest.php b/Tests/Functional/Command/GenerateTokenCommandTest.php
index 4fe5eb8a..8b11f3ac 100644
--- a/Tests/Functional/Command/GenerateTokenCommandTest.php
+++ b/Tests/Functional/Command/GenerateTokenCommandTest.php
@@ -5,6 +5,7 @@
use Lexik\Bundle\JWTAuthenticationBundle\Tests\Functional\TestCase;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
+use Symfony\Component\Security\Core\User\InMemoryUser;
use Symfony\Component\Security\Core\User\User;
class GenerateTokenCommandTest extends TestCase
@@ -13,7 +14,7 @@ public function testRun()
{
$tester = new CommandTester((new Application($this->bootKernel(['test_case' => 'GenerateTokenCommand'])))->get('lexik:jwt:generate-token'));
- $this->assertSame(0, $tester->execute(['username' => 'lexik', '--user-class' => User::class]));
+ $this->assertSame(0, $tester->execute(['username' => 'lexik', '--user-class' => class_exists(InMemoryUser::class) ? InMemoryUser::class : User::class]));
}
public function testRunWithoutSpecifiedProviderAndMoreThanOneConfigured()
diff --git a/Tests/Functional/app/AppKernel.php b/Tests/Functional/app/AppKernel.php
index caaa1280..b02c4057 100644
--- a/Tests/Functional/app/AppKernel.php
+++ b/Tests/Functional/app/AppKernel.php
@@ -7,6 +7,7 @@
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel;
+use Symfony\Component\Security\Core\Exception\UserNotFoundException;
/**
* AppKernel.
@@ -70,18 +71,37 @@ public function getLogDir()
*/
public function registerContainerConfiguration(LoaderInterface $loader)
{
- if (\method_exists(TreeBuilder::class, 'getRootNode')) {
- // Symfony 4.2+
- $loader->load(__DIR__.'/config/config_router_utf8.yml');
+ $loader->load(__DIR__.'/config/config_router_utf8.yml');
+
+ // 5.3+ session config
+ if (class_exists(UserNotFoundException::class)) {
+ $sessionConfig = [
+ 'storage_factory_id' => 'session.storage.factory.mock_file',
+ ];
+ } else {
+ $sessionConfig = [
+ 'handler_id' => null,
+ 'cookie_secure' => 'auto',
+ 'cookie_samesite' => 'lax',
+ 'storage_id' => 'session.storage.mock_file',
+ ];
}
+ $loader->load(function (ContainerBuilder $container) use ($sessionConfig) {
+ $container->prependExtensionConfig('framework', [
+ 'router' => [
+ 'resource' => '%kernel.root_dir%/config/routing.yml',
+ 'utf8' => true,
+ ],
+ 'session' => $sessionConfig
+ ]);
+ });
+
if ($this->testCase && file_exists(__DIR__.'/config/'.$this->testCase.'/config.yml')) {
$loader->load(__DIR__.'/config/'.$this->testCase.'/config.yml');
-
- return;
}
- $loader->load(__DIR__.sprintf('/config/security_%s.yml', $this->userProvider));
+ $loader->load(__DIR__.sprintf('/config/security_%s.yml', $this->userProvider . (class_exists(UserNotFoundException::class) ? '' : '_legacy')));
if ($this->signatureAlgorithm && file_exists($file = __DIR__.sprintf('/config/config_%s_%s.yml', $this->encoder, strtolower($this->signatureAlgorithm)))) {
$loader->load($file);
diff --git a/Tests/Functional/app/config/GenerateTokenCommand/config.yml b/Tests/Functional/app/config/GenerateTokenCommand/config.yml
index 458212a8..cabc1940 100644
--- a/Tests/Functional/app/config/GenerateTokenCommand/config.yml
+++ b/Tests/Functional/app/config/GenerateTokenCommand/config.yml
@@ -1,6 +1,5 @@
imports:
- { resource: '../base_config.yml' }
- - { resource: '../security_in_memory.yml' }
lexik_jwt_authentication:
secret_key: testing
diff --git a/Tests/Functional/app/config/base_config.yml b/Tests/Functional/app/config/base_config.yml
index ba9ff6c0..baa06876 100644
--- a/Tests/Functional/app/config/base_config.yml
+++ b/Tests/Functional/app/config/base_config.yml
@@ -1,10 +1,6 @@
framework:
secret: test
- router:
- resource: '%kernel.root_dir%/config/routing.yml'
test: ~
- session:
- storage_id: session.storage.mock_file
services:
lexik_jwt_authentication.test.jwt_event_subscriber:
@@ -19,7 +15,6 @@ services:
arguments: ['@lexik_jwt_authentication.jws_provider.lcobucci']
public: true
-
Lexik\Bundle\JWTAuthenticationBundle\Tests\Functional\Bundle\Controller\TestController:
arguments: ['@security.token_storage']
public: true
diff --git a/Tests/Functional/app/config/base_security.yml b/Tests/Functional/app/config/base_security.yml
index 3c893d56..0d6fdd05 100644
--- a/Tests/Functional/app/config/base_security.yml
+++ b/Tests/Functional/app/config/base_security.yml
@@ -1,7 +1,4 @@
security:
- encoders:
- Symfony\Component\Security\Core\User\User: plaintext
-
providers:
in_memory:
memory:
diff --git a/Tests/Functional/app/config/security_in_memory.yml b/Tests/Functional/app/config/security_in_memory.yml
index 79acc319..c3bb6422 100644
--- a/Tests/Functional/app/config/security_in_memory.yml
+++ b/Tests/Functional/app/config/security_in_memory.yml
@@ -2,11 +2,15 @@ imports:
- { resource: base_security.yml }
security:
+ enable_authenticator_manager: true
+
+ password_hashers:
+ Symfony\Component\Security\Core\User\UserInterface: plaintext
+
firewalls:
login:
pattern: ^/login
stateless: true
- anonymous: true
provider: in_memory
form_login:
check_path: /login_check
@@ -19,6 +23,4 @@ security:
stateless: true
anonymous: false
provider: in_memory
- guard:
- authenticators:
- - lexik_jwt_authentication.jwt_token_authenticator
+ jwt: ~
diff --git a/Tests/Functional/app/config/security_in_memory_legacy.yml b/Tests/Functional/app/config/security_in_memory_legacy.yml
new file mode 100644
index 00000000..55e5f0cc
--- /dev/null
+++ b/Tests/Functional/app/config/security_in_memory_legacy.yml
@@ -0,0 +1,27 @@
+imports:
+ - { resource: base_security.yml }
+
+security:
+ encoders:
+ Symfony\Component\Security\Core\User\UserInterface: plaintext
+
+ firewalls:
+ login:
+ pattern: ^/login
+ stateless: true
+ anonymous: true
+ provider: in_memory
+ form_login:
+ check_path: /login_check
+ require_previous_session: false
+ success_handler: lexik_jwt_authentication.handler.authentication_success
+ failure_handler: lexik_jwt_authentication.handler.authentication_failure
+
+ api:
+ pattern: ^/api
+ stateless: true
+ anonymous: false
+ provider: in_memory
+ guard:
+ authenticators:
+ - lexik_jwt_authentication.jwt_token_authenticator
diff --git a/Tests/Functional/app/config/security_lexik_jwt.yml b/Tests/Functional/app/config/security_lexik_jwt.yml
index c7a09718..7bc50160 100644
--- a/Tests/Functional/app/config/security_lexik_jwt.yml
+++ b/Tests/Functional/app/config/security_lexik_jwt.yml
@@ -2,11 +2,15 @@ imports:
- { resource: base_security.yml }
security:
+ enable_authenticator_manager: true
+
+ password_hashers:
+ Symfony\Component\Security\Core\User\UserInterface: plaintext
+
firewalls:
login:
pattern: ^/login
stateless: true
- anonymous: true
provider: in_memory
form_login:
check_path: /login_check
@@ -17,8 +21,5 @@ security:
api:
pattern: ^/api
stateless: true
- anonymous: false
provider: jwt
- guard:
- authenticators:
- - lexik_jwt_authentication.jwt_token_authenticator
+ jwt: ~
diff --git a/Tests/Functional/app/config/security_lexik_jwt_legacy.yml b/Tests/Functional/app/config/security_lexik_jwt_legacy.yml
new file mode 100644
index 00000000..5aaf731e
--- /dev/null
+++ b/Tests/Functional/app/config/security_lexik_jwt_legacy.yml
@@ -0,0 +1,27 @@
+imports:
+ - { resource: base_security.yml }
+
+security:
+ encoders:
+ Symfony\Component\Security\Core\User\UserInterface: plaintext
+
+ firewalls:
+ login:
+ pattern: ^/login
+ stateless: true
+ anonymous: true
+ provider: in_memory
+ form_login:
+ check_path: /login_check
+ require_previous_session: false
+ success_handler: lexik_jwt_authentication.handler.authentication_success
+ failure_handler: lexik_jwt_authentication.handler.authentication_failure
+
+ api:
+ pattern: ^/api
+ stateless: true
+ anonymous: false
+ provider: jwt
+ guard:
+ authenticators:
+ - lexik_jwt_authentication.jwt_token_authenticator
diff --git a/Tests/Security/Authenticator/JWTAuthenticatorTest.php b/Tests/Security/Authenticator/JWTAuthenticatorTest.php
new file mode 100644
index 00000000..009847ac
--- /dev/null
+++ b/Tests/Security/Authenticator/JWTAuthenticatorTest.php
@@ -0,0 +1,276 @@
+= 7.2 */
+class JWTAuthenticatorTest extends TestCase
+{
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ if (!class_exists(UserNotFoundException::class)) {
+ $this->markTestSkipped('This test suite only concerns the new Symfony 5.3 authentication system.');
+ }
+ }
+
+ public function testAuthenticate() {
+ $userIdClaim = 'sub';
+ $payload = [$userIdClaim => 'lexik'];
+ $rawToken = 'token';
+ $userRoles = ['ROLE_USER'];
+
+ $userStub = new AdvancedUserStub('lexik', 'password', 'user@gmail.com', $userRoles);
+
+ $jwtManager = $this->getJWTManagerMock(null, $userIdClaim);
+ $jwtManager
+ ->method('decodeFromJsonWebToken')
+ ->willReturn(['sub' => 'lexik']);
+
+ $userProvider = $this->getUserProviderMock();
+ $userProvider
+ ->method('loadUserByIdentifierAndPayload')
+ ->with($payload['sub'], $payload)
+ ->willReturn($userStub);
+
+ $authenticator = new JWTAuthenticator(
+ $jwtManager,
+ $this->getEventDispatcherMock(),
+ $this->getTokenExtractorMock($rawToken),
+ $userProvider
+ );
+
+ $this->assertSame($userStub, ($authenticator->authenticate($this->getRequestMock()))->getUser());
+ }
+
+ public function testAuthenticateWithExpiredTokenThrowsException() {
+ $jwtManager = $this->getJWTManagerMock();
+ $jwtManager->method('decodeFromJsonWebToken')
+ ->will($this->throwException(new JWTDecodeFailureException(JWTDecodeFailureException::EXPIRED_TOKEN, 'Expired JWT Token')));
+
+ $this->expectException(ExpiredTokenException::class);
+
+ $authenticator = new JWTAuthenticator(
+ $jwtManager,
+ $this->getEventDispatcherMock(),
+ $this->getTokenExtractorMock('token'),
+ $this->getUserProviderMock()
+ );
+
+ $authenticator->authenticate($this->getRequestMock());
+ }
+
+ public function testAuthenticateWithInvalidTokenThrowsException() {
+ $jwtManager = $this->getJWTManagerMock();
+ $jwtManager->method('decodeFromJsonWebToken')
+ ->willThrowException(new JWTDecodeFailureException(
+ JWTDecodeFailureException::INVALID_TOKEN,
+ 'Invalid JWT Token')
+ );
+ $authenticator = new JWTAuthenticator(
+ $jwtManager,
+ $this->getEventDispatcherMock(),
+ $this->getTokenExtractorMock('token'),
+ $this->getUserProviderMock()
+ );
+
+ $this->expectException(InvalidTokenException::class);
+
+ $authenticator->authenticate($this->getRequestMock());
+ }
+
+ public function testAuthenticateWithUndecodableTokenThrowsException() {
+ $jwtManager = $this->getJWTManagerMock();
+ $jwtManager->method('decodeFromJsonWebToken')
+ ->willReturn(null);
+ $authenticator = new JWTAuthenticator(
+ $jwtManager,
+ $this->getEventDispatcherMock(),
+ $this->getTokenExtractorMock('token'),
+ $this->getUserProviderMock()
+ );
+
+ $this->expectException(InvalidTokenException::class);
+
+ $authenticator->authenticate($this->getRequestMock());
+ }
+
+ public function testAuthenticationWithInvalidPayloadThrowsException() {
+ $jwtManager = $this->getJWTManagerMock();
+ $jwtManager->method('decodeFromJsonWebToken')
+ ->willReturn(['foo' => 'bar']);
+ $jwtManager->method('getUserIdClaim')
+ ->willReturn('identifier');
+ $authenticator = new JWTAuthenticator(
+ $jwtManager,
+ $this->getEventDispatcherMock(),
+ $this->getTokenExtractorMock('token'),
+ $this->getUserProviderMock()
+ );
+
+ $this->expectException(InvalidPayloadException::class);
+
+ $authenticator->authenticate($this->getRequestMock());
+ }
+
+ public function testAuthenticateWithInvalidUserThrowsException() {
+ $jwtManager = $this->getJWTManagerMock();
+ $jwtManager->method('decodeFromJsonWebToken')
+ ->willReturn(['identifier' => 'bar']);
+ $jwtManager->method('getUserIdClaim')
+ ->willReturn('identifier');
+
+ $userProvider = $this->getUserProviderMock();
+ $userProvider->method('loadUserByIdentifierAndPayload')
+ ->willThrowException(new UserNotFoundException());
+
+ $authenticator = new JWTAuthenticator(
+ $jwtManager,
+ $this->getEventDispatcherMock(),
+ $this->getTokenExtractorMock('token'),
+ $userProvider
+ );
+
+ $this->expectException(UserNotFoundException::class);
+
+ $authenticator->authenticate($this->getRequestMock())->getUser();
+ }
+
+ public function testOnAuthenticationFailureWithInvalidToken() {
+ $authException = new InvalidTokenException();
+ $expectedResponse = new JWTAuthenticationFailureResponse('Invalid JWT Token');
+
+ $dispatcher = $this->getEventDispatcherMock();
+ $this->expectEvent(Events::JWT_INVALID, new JWTInvalidEvent($authException, $expectedResponse), $dispatcher);
+
+ $authenticator = new JWTAuthenticator(
+ $this->getJWTManagerMock(),
+ $dispatcher,
+ $this->getTokenExtractorMock(),
+ $this->getUserProviderMock()
+ );
+
+ $response = $authenticator->onAuthenticationFailure($this->getRequestMock(), $authException);
+
+ $this->assertEquals($expectedResponse, $response);
+ $this->assertSame($expectedResponse->getMessage(), $response->getMessage());
+ }
+
+ public function testStart()
+ {
+ $authException = new MissingTokenException('JWT Token not found');
+ $failureResponse = new JWTAuthenticationFailureResponse($authException->getMessageKey());
+
+ $dispatcher = $this->getEventDispatcherMock();
+ $this->expectEvent(Events::JWT_NOT_FOUND, new JWTNotFoundEvent($authException, $failureResponse), $dispatcher);
+
+ $authenticator = new JWTAuthenticator(
+ $this->getJWTManagerMock(),
+ $dispatcher,
+ $this->getTokenExtractorMock(),
+ $this->getUserProviderMock()
+ );
+
+ $response = $authenticator->start($this->getRequestMock());
+
+ $this->assertEquals($failureResponse, $response);
+ $this->assertSame($failureResponse->getMessage(), $response->getMessage());
+ }
+
+ private function getJWTManagerMock($userIdentityField = null, $userIdClaim = null)
+ {
+ $jwtManager = $this->getMockBuilder(JWTTokenManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ if (null !== $userIdentityField) {
+ $jwtManager
+ ->expects($this->once())
+ ->method('getUserIdentityField')
+ ->willReturn($userIdentityField);
+ }
+ if (null !== $userIdClaim) {
+ $jwtManager
+ ->expects($this->once())
+ ->method('getUserIdClaim')
+ ->willReturn($userIdClaim);
+ }
+
+ return $jwtManager;
+ }
+
+ private function getEventDispatcherMock()
+ {
+ return $this->getMockBuilder(EventDispatcherInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ }
+
+ private function getTokenExtractorMock($returnValue = null)
+ {
+ $extractor = $this->getMockBuilder(TokenExtractorInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ if (null !== $returnValue) {
+ $extractor
+ ->expects($this->once())
+ ->method('extract')
+ ->willReturn($returnValue);
+ }
+
+ return $extractor;
+ }
+
+ private function getRequestMock()
+ {
+ return $this->getMockBuilder(Request::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ }
+
+ private function getUserProviderMock()
+ {
+ return $this->getMockBuilder(DummyUserProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ }
+
+ private function expectEvent($eventName, $event, $dispatcher)
+ {
+ $dispatcher->expects($this->once())->method('dispatch')->with($event, $eventName);
+ }
+}
+
+abstract class DummyUserProvider implements UserProviderInterface, PayloadAwareUserProviderInterface
+{
+ public function loadUserByIdentifier(string $identifier): UserInterface
+ {
+ }
+
+ public function loadUserByIdentifierAndPayload(string $identifier): UserInterface
+ {
+ }
+}
diff --git a/Tests/Security/Guard/JWTTokenAuthenticatorTest.php b/Tests/Security/Guard/JWTTokenAuthenticatorTest.php
index 9b44347d..a766ced9 100644
--- a/Tests/Security/Guard/JWTTokenAuthenticatorTest.php
+++ b/Tests/Security/Guard/JWTTokenAuthenticatorTest.php
@@ -24,9 +24,14 @@
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\UserNotFoundException as SecurityUserNotFoundException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
+use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+/**
+ *
+ * @group legacy
+ */
class JWTTokenAuthenticatorTest extends TestCase
{
public function testGetCredentials()
@@ -113,7 +118,7 @@ public function testGetUser()
$userProvider = $this->getUserProviderMock();
$userProvider
->expects($this->once())
- ->method('loadUserByUsername')
+ ->method('loadUserByIdentifier')
->with($payload[$userIdClaim])
->willReturn($userStub);
@@ -177,7 +182,7 @@ public function testGetUserWithInvalidUserThrowsException()
$userProvider = $this->getUserProviderMock();
$userProvider
->expects($this->once())
- ->method('loadUserByUsername')
+ ->method('loadUserByIdentifier')
->with($payload[$userIdClaim])
->will($this->throwException($exception));
@@ -231,7 +236,7 @@ public function testCreateAuthenticatedToken()
$userProvider = $this->getUserProviderMock();
$userProvider
->expects($this->once())
- ->method('loadUserByUsername')
+ ->method('loadUserByIdentifier')
->with($payload['sub'])
->willReturn($userStub);
@@ -382,9 +387,7 @@ private function getRequestMock()
private function getUserProviderMock()
{
- return $this->getMockBuilder(UserProviderInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ return $this->getMockBuilder(DummyUserProvider::class)->getMock();
}
/**
@@ -402,3 +405,10 @@ private function expectEvent($eventName, $event, $dispatcher)
$dispatcher->expects($this->once())->method('dispatch')->with($event, $eventName);
}
}
+
+abstract class DummyUserProvider implements UserProviderInterface
+{
+ public function loadUserByIdentifier(string $identifier): UserInterface
+ {
+ }
+}
diff --git a/Tests/Stubs/User.php b/Tests/Stubs/User.php
index eaa2f49e..33222521 100644
--- a/Tests/Stubs/User.php
+++ b/Tests/Stubs/User.php
@@ -22,18 +22,18 @@
*/
final class User implements UserInterface
{
- private $username;
+ private $userIdentifier;
private $password;
private $roles;
private $email;
- public function __construct($username, $password, $email = '', array $roles = [])
+ public function __construct($userIdentifier, $password, $email = '', array $roles = [])
{
- if (empty($username)) {
+ if (empty($userIdentifier)) {
throw new \InvalidArgumentException('The username cannot be empty.');
}
- $this->username = $username;
+ $this->userIdentifier = $userIdentifier;
$this->password = $password;
$this->roles = $roles;
$this->email = $email;
@@ -67,15 +67,12 @@ public function getSalt()
*/
public function getUsername()
{
- return $this->username;
+ return $this->getUserIdentifier();
}
- /**
- * {@inheritdoc}
- */
public function getUserIdentifier(): string
{
- return $this->username;
+ return $this->userIdentifier;
}
/**
diff --git a/composer.json b/composer.json
index ff42db90..a5e6bf23 100644
--- a/composer.json
+++ b/composer.json
@@ -42,7 +42,8 @@
"ext-openssl": "*",
"lcobucci/jwt": "^3.4|^4.0",
"symfony/framework-bundle": "^4.4|^5.1",
- "symfony/security-bundle": "^4.4|^5.1"
+ "symfony/security-bundle": "^4.4|^5.1",
+ "symfony/deprecation-contracts": "^2.4"
},
"require-dev": {
"symfony/browser-kit": "^4.4|^5.1",