diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index d7e25b59..38b70487 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -9,40 +9,18 @@ jobs: strategy: fail-fast: false matrix: - php: ['7.4', '8.0', '8.1', '8.2'] - symfony: ['4.4.*', '5.4.*', '6.0.*', '6.1.*', '6.2.*', '6.3.*', '6.4.*', '7.0.*'] + php: ['8.1', '8.2'] + symfony: ['5.4.*', '6.4.*', '7.0.*'] composer-flags: ['--prefer-stable'] can-fail: [false] extensions: ['curl, iconv, mbstring, mongodb, pdo, pdo_sqlite, sqlite, zip'] include: - - php: '7.4' - symfony: '4.4.*' + - php: '8.1' + symfony: '5.4.*' composer-flags: '--prefer-stable --prefer-lowest' can-fail: false extensions: 'curl, iconv, mbstring, mongodb, pdo, pdo_sqlite, sqlite, zip' exclude: - - php: '7.4' - symfony: '6.0.*' - - php: '7.4' - symfony: '6.1.*' - - php: '7.4' - symfony: '6.2.*' - - php: '7.4' - symfony: '6.3.*' - - php: '7.4' - symfony: '6.4.*' - - php: '7.4' - symfony: '7.0.*' - - php: '8.0' - symfony: '6.1.*' - - php: '8.0' - symfony: '6.2.*' - - php: '8.0' - symfony: '6.3.*' - - php: '8.0' - symfony: '6.4.*' - - php: '8.0' - symfony: '7.0.*' - php: '8.1' symfony: '7.0.*' @@ -73,10 +51,6 @@ jobs: version: '5.0' topology: server - - name: Remove Guard (Symfony >=6.0) - if: contains(fromJSON('["6.0.*", "6.1.*", "6.2.*", "6.3.*", "6.4.*", "7.0.*"]'), matrix.symfony) - run: composer remove --dev --no-update symfony/security-guard - - name: Install dependencies run: composer update ${{ matrix.composer-flags }} --prefer-dist --no-suggest env: diff --git a/Command/ClearInvalidRefreshTokensCommand.php b/Command/ClearInvalidRefreshTokensCommand.php index bbfb93ea..a5d2afb3 100644 --- a/Command/ClearInvalidRefreshTokensCommand.php +++ b/Command/ClearInvalidRefreshTokensCommand.php @@ -21,13 +21,8 @@ use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand(name: 'gesdinet:jwt:clear', description: 'Clear invalid refresh tokens.')] -class ClearInvalidRefreshTokensCommand extends Command +final class ClearInvalidRefreshTokensCommand extends Command { - /** - * @deprecated - */ - protected static $defaultName = 'gesdinet:jwt:clear'; - private RefreshTokenManagerInterface $refreshTokenManager; public function __construct(RefreshTokenManagerInterface $refreshTokenManager) @@ -39,23 +34,14 @@ public function __construct(RefreshTokenManagerInterface $refreshTokenManager) protected function configure(): void { - $this - ->setDescription('Clear invalid refresh tokens.') - ->addArgument('datetime', InputArgument::OPTIONAL, 'An optional date, all tokens before this date will be removed; the value should be able to be parsed by DateTime.'); + $this->addArgument('datetime', InputArgument::OPTIONAL, 'An optional date, all tokens before this date will be removed; the value should be able to be parsed by DateTime.', 'now'); } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - /** @var string|null $datetime */ - $datetime = $input->getArgument('datetime'); - - if (null === $datetime) { - $datetime = new \DateTime(); - } else { - $datetime = new \DateTime($datetime); - } + $datetime = new \DateTime($input->getArgument('datetime')); $revokedTokens = $this->refreshTokenManager->revokeAllInvalid($datetime); diff --git a/Command/RevokeRefreshTokenCommand.php b/Command/RevokeRefreshTokenCommand.php index 06c5da9b..545888e7 100644 --- a/Command/RevokeRefreshTokenCommand.php +++ b/Command/RevokeRefreshTokenCommand.php @@ -19,14 +19,9 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -#[AsCommand(name: 'gesdinet:jwt:revoke', description: 'Revoke a refresh token')] -class RevokeRefreshTokenCommand extends Command +#[AsCommand(name: 'gesdinet:jwt:revoke', description: 'Revoke a refresh token.')] +final class RevokeRefreshTokenCommand extends Command { - /** - * @deprecated - */ - protected static $defaultName = 'gesdinet:jwt:revoke'; - private RefreshTokenManagerInterface $refreshTokenManager; public function __construct(RefreshTokenManagerInterface $refreshTokenManager) @@ -38,9 +33,7 @@ public function __construct(RefreshTokenManagerInterface $refreshTokenManager) protected function configure(): void { - $this - ->setDescription('Revoke a refresh token') - ->addArgument('refresh_token', InputArgument::REQUIRED, 'The refresh token to revoke'); + $this->addArgument('refresh_token', InputArgument::REQUIRED, 'The refresh token to revoke'); } protected function execute(InputInterface $input, OutputInterface $output): int diff --git a/DependencyInjection/Compiler/AddExtractorsToChainCompilerPass.php b/DependencyInjection/Compiler/AddExtractorsToChainCompilerPass.php index d6c98670..9e5d013d 100644 --- a/DependencyInjection/Compiler/AddExtractorsToChainCompilerPass.php +++ b/DependencyInjection/Compiler/AddExtractorsToChainCompilerPass.php @@ -12,11 +12,11 @@ final class AddExtractorsToChainCompilerPass implements CompilerPassInterface public function process(ContainerBuilder $container): void { - if (!$container->hasDefinition('gesdinet.jwtrefreshtoken.request.extractor.chain')) { + if (!$container->hasDefinition('gesdinet_jwt_refresh_token.request.extractor.chain')) { return; } - $definition = $container->getDefinition('gesdinet.jwtrefreshtoken.request.extractor.chain'); + $definition = $container->getDefinition('gesdinet_jwt_refresh_token.request.extractor.chain'); foreach ($this->findAndSortTaggedServices('gesdinet_jwt_refresh_token.request_extractor', $container) as $extractorService) { $definition->addMethodCall('addExtractor', [$extractorService]); diff --git a/DependencyInjection/Compiler/CustomUserProviderCompilerPass.php b/DependencyInjection/Compiler/CustomUserProviderCompilerPass.php deleted file mode 100644 index a2905a7b..00000000 --- a/DependencyInjection/Compiler/CustomUserProviderCompilerPass.php +++ /dev/null @@ -1,47 +0,0 @@ -internalUse = $internalUse; - } - - public function process(ContainerBuilder $container): void - { - if (false === $this->internalUse) { - trigger_deprecation('gesdinet/jwt-refresh-token-bundle', '1.0', 'The "%s" class is deprecated.', self::class); - } - - /** @var string|null $customUserProvider */ - $customUserProvider = $container->getParameter('gesdinet_jwt_refresh_token.user_provider'); - if (!$customUserProvider) { - return; - } - if (!$container->hasDefinition('gesdinet.jwtrefreshtoken.user_provider')) { - return; - } - - $definition = $container->getDefinition('gesdinet.jwtrefreshtoken.user_provider'); - - $definition->addMethodCall( - 'setCustomUserProvider', - [new Reference($customUserProvider, ContainerInterface::NULL_ON_INVALID_REFERENCE)] - ); - } -} diff --git a/DependencyInjection/Compiler/ObjectManagerCompilerPass.php b/DependencyInjection/Compiler/ObjectManagerCompilerPass.php deleted file mode 100644 index 70f8275f..00000000 --- a/DependencyInjection/Compiler/ObjectManagerCompilerPass.php +++ /dev/null @@ -1,20 +0,0 @@ -getParameter('gesdinet.jwtrefreshtoken.object_manager.id'); - if (!$objectManagerId) { - return; - } - - $container->setAlias('gesdinet.jwtrefreshtoken.object_manager', $objectManagerId); - } -} diff --git a/DependencyInjection/Compiler/UserCheckerCompilerPass.php b/DependencyInjection/Compiler/UserCheckerCompilerPass.php deleted file mode 100644 index e4eaf5cf..00000000 --- a/DependencyInjection/Compiler/UserCheckerCompilerPass.php +++ /dev/null @@ -1,37 +0,0 @@ -internalUse = $internalUse; - } - - public function process(ContainerBuilder $container): void - { - if (false === $this->internalUse) { - trigger_deprecation('gesdinet/jwt-refresh-token-bundle', '1.0', 'The "%s" class is deprecated.', self::class); - } - - /** @var string|null $userCheckerId */ - $userCheckerId = $container->getParameter('gesdinet.jwtrefreshtoken.user_checker.id'); - if (!$userCheckerId) { - return; - } - - $container->setAlias('gesdinet.jwtrefreshtoken.user_checker', $userCheckerId); - } -} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 3c2b2d37..bb1a7539 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -11,12 +11,11 @@ namespace Gesdinet\JWTRefreshTokenBundle\DependencyInjection; -use Gesdinet\JWTRefreshTokenBundle\Entity\RefreshToken; -use Symfony\Component\Config\Definition\BaseNode; +use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenInterface; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; -class Configuration implements ConfigurationInterface +final class Configuration implements ConfigurationInterface { public function getConfigTreeBuilder(): TreeBuilder { @@ -33,54 +32,23 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultFalse() ->info('The default update TTL flag for all authenticators.') ->end() - ->scalarNode('firewall') - ->setDeprecated(...$this->getDeprecationParameters('The "%node%" node is deprecated without replacement.', '1.0')) - ->defaultValue('api') - ->end() - ->scalarNode('user_provider') - ->setDeprecated(...$this->getDeprecationParameters('The "%node%" node is deprecated without replacement.', '1.0')) - ->defaultNull() - ->end() - ->scalarNode('user_identity_field') - ->setDeprecated(...$this->getDeprecationParameters('The "%node%" node is deprecated without replacement.', '1.0')) - ->defaultValue('username') - ->end() ->scalarNode('manager_type') ->defaultValue('orm') ->info('Set the type of object manager to use (default: orm)') ->end() ->scalarNode('refresh_token_class') - ->defaultNull() - ->info(sprintf('Set the refresh token class to use (default: %s)', RefreshToken::class)) + ->isRequired() + ->cannotBeEmpty() + ->info('Set the refresh token class to use') ->validate() - ->ifTrue(static fn ($v): bool => null === $v) - ->then(static function () { - trigger_deprecation( - 'gesdinet/jwt-refresh-token-bundle', - '1.1', - 'Not setting the "refresh_token_class" option is deprecated, as of 2.0 a class must be set.' - ); - }) + ->ifTrue(static fn ($v): bool => null === $v || !\in_array(RefreshTokenInterface::class, class_implements($v), true)) + ->thenInvalid(sprintf('The "refresh_token_class" class must implement "%s".', RefreshTokenInterface::class)) ->end() ->end() ->scalarNode('object_manager') ->defaultNull() ->info('Set the object manager to use (default: doctrine.orm.entity_manager)') ->end() - ->scalarNode('user_checker') - ->setDeprecated(...$this->getDeprecationParameters('The "%node%" node is deprecated without replacement.', '1.0')) - ->defaultValue('security.user_checker') - ->end() - ->scalarNode('refresh_token_entity') - ->setDeprecated(...$this->getDeprecationParameters('The "%node%" node is deprecated, use the "refresh_token_class" node instead.', '0.5')) - ->defaultNull() - ->info(sprintf('Set the refresh token class to use (default: %s)', RefreshToken::class)) - ->end() - ->scalarNode('entity_manager') - ->setDeprecated(...$this->getDeprecationParameters('The "%node%" node is deprecated, use the "object_manager" node instead.', '0.5')) - ->defaultNull() - ->info('Set the entity manager to use') - ->end() ->scalarNode('single_use') ->defaultFalse() ->info('When true, generate a new refresh token on consumption (deleting the old one)') @@ -89,11 +57,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultValue('refresh_token') ->info('The default request parameter name containing the refresh token for all authenticators.') ->end() - ->booleanNode('doctrine_mappings') - ->setDeprecated(...$this->getDeprecationParameters('The "%node%" node is deprecated without replacement.', '1.0')) - ->info('When true, resolving of Doctrine mapping is done automatically to use either ORM or ODM object manager') - ->defaultTrue() - ->end() ->arrayNode('cookie') ->canBeEnabled() ->children() @@ -109,10 +72,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->scalarNode('remove_token_from_body')->defaultTrue()->end() ->end() ->end() - ->scalarNode('logout_firewall') - ->defaultValue('api') - ->info('Name of the firewall that triggers the logout event to hook into (default: api)') - ->end() ->scalarNode('return_expiration') ->defaultFalse() ->info('When true, the response will include the token expiration timestamp') @@ -125,13 +84,4 @@ public function getConfigTreeBuilder(): TreeBuilder return $treeBuilder; } - - private function getDeprecationParameters(string $message, string $version): array - { - if (method_exists(BaseNode::class, 'getDeprecation')) { - return ['gesdinet/jwt-refresh-token-bundle', $version, $message]; - } - - return [$message]; - } } diff --git a/DependencyInjection/GesdinetJWTRefreshTokenExtension.php b/DependencyInjection/GesdinetJWTRefreshTokenExtension.php index 1093b068..2447460f 100644 --- a/DependencyInjection/GesdinetJWTRefreshTokenExtension.php +++ b/DependencyInjection/GesdinetJWTRefreshTokenExtension.php @@ -13,88 +13,46 @@ use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ORM\EntityManager; -use Gesdinet\JWTRefreshTokenBundle\Document\RefreshToken as RefreshTokenDocument; -use Gesdinet\JWTRefreshTokenBundle\Entity\RefreshToken as RefreshTokenEntity; use Gesdinet\JWTRefreshTokenBundle\Request\Extractor\ExtractorInterface; -use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Config\FileLocator; -use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Loader; +use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension; -class GesdinetJWTRefreshTokenExtension extends Extension +final class GesdinetJWTRefreshTokenExtension extends ConfigurableExtension { - public function load(array $configs, ContainerBuilder $container): void + protected function loadInternal(array $mergedConfig, ContainerBuilder $container): void { - $config = $this->processConfiguration($this->getConfiguration($configs, $container), $configs); - $loader = new Loader\PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.php'); $container->registerForAutoconfiguration(ExtractorInterface::class)->addTag('gesdinet_jwt_refresh_token.request_extractor'); - $container->setParameter('gesdinet_jwt_refresh_token.ttl', $config['ttl']); - $container->setParameter('gesdinet_jwt_refresh_token.ttl_update', $config['ttl_update']); - $container->setParameter('gesdinet_jwt_refresh_token.security.firewall', $config['firewall']); - $container->setParameter('gesdinet_jwt_refresh_token.user_provider', $config['user_provider']); - $container->setParameter('gesdinet_jwt_refresh_token.user_identity_field', $config['user_identity_field']); - $container->setParameter('gesdinet_jwt_refresh_token.single_use', $config['single_use']); - $container->setParameter('gesdinet_jwt_refresh_token.token_parameter_name', $config['token_parameter_name']); - $container->setParameter('gesdinet_jwt_refresh_token.doctrine_mappings', $config['doctrine_mappings']); - $container->setParameter('gesdinet_jwt_refresh_token.cookie', $config['cookie'] ?? []); - $container->setParameter('gesdinet_jwt_refresh_token.logout_firewall_context', sprintf( - 'security.firewall.map.context.%s', - $config['logout_firewall'] - )); - $container->setParameter('gesdinet_jwt_refresh_token.return_expiration', $config['return_expiration']); - $container->setParameter('gesdinet_jwt_refresh_token.return_expiration_parameter_name', $config['return_expiration_parameter_name']); - - $refreshTokenClass = RefreshTokenEntity::class; - $objectManager = 'doctrine.orm.entity_manager'; - - // Change the refresh token and object manager to the MongoDB ODM if the configuration explicitly sets it or if the ORM is not installed and the MongoDB ODM is - if ('mongodb' === strtolower($config['manager_type']) || (!class_exists(EntityManager::class) && class_exists(DocumentManager::class))) { - $refreshTokenClass = RefreshTokenDocument::class; + $container->setParameter('gesdinet_jwt_refresh_token.ttl', $mergedConfig['ttl']); + $container->setParameter('gesdinet_jwt_refresh_token.ttl_update', $mergedConfig['ttl_update']); + $container->setParameter('gesdinet_jwt_refresh_token.single_use', $mergedConfig['single_use']); + $container->setParameter('gesdinet_jwt_refresh_token.token_parameter_name', $mergedConfig['token_parameter_name']); + $container->setParameter('gesdinet_jwt_refresh_token.cookie', $mergedConfig['cookie'] ?? []); + $container->setParameter('gesdinet_jwt_refresh_token.return_expiration', $mergedConfig['return_expiration']); + $container->setParameter('gesdinet_jwt_refresh_token.return_expiration_parameter_name', $mergedConfig['return_expiration_parameter_name']); + $container->setParameter('gesdinet_jwt_refresh_token.refresh_token.class', $mergedConfig['refresh_token_class']); + + /* + * Configuration preference: + * - Explicitly configured "object_manager" node + * - Feature detection (ORM then MongoDB ODM) + */ + if (null !== $mergedConfig['object_manager']) { + $objectManager = $mergedConfig['object_manager']; + } elseif (ContainerBuilder::willBeAvailable('doctrine/orm', EntityManager::class, ['doctrine/doctrine-bundle'])) { + $objectManager = 'doctrine.orm.entity_manager'; + } elseif (ContainerBuilder::willBeAvailable('doctrine/mongodb-odm', DocumentManager::class, ['doctrine/mongodb-odm-bundle'])) { $objectManager = 'doctrine_mongodb.odm.document_manager'; + } else { + throw new RuntimeException('The "object_manager" node must be configured when neither "doctrine/orm" or "doctrine/mongodb-odm" are installed.'); } - if (null !== $this->getRefreshTokenClass($config)) { - $refreshTokenClass = $this->getRefreshTokenClass($config); - } - - if (null !== $this->getObjectManager($config)) { - $objectManager = $this->getObjectManager($config); - } - - $container->setParameter('gesdinet.jwtrefreshtoken.refresh_token.class', $refreshTokenClass); - $container->setParameter('gesdinet.jwtrefreshtoken.object_manager.id', $objectManager); - $container->setParameter('gesdinet.jwtrefreshtoken.user_checker.id', $config['user_checker']); - } - - /** - * Get the refresh token class from configuration. - * - * Falls back to deprecated configuration nodes if necessary. - */ - protected function getRefreshTokenClass(array $config): ?string - { - if (isset($config['refresh_token_class'])) { - return $config['refresh_token_class']; - } - - return $config['refresh_token_entity'] ?: null; - } - - /** - * Get object manager from configuration. - * - * Falls back to deprecated configuration nodes if necessary. - */ - protected function getObjectManager(array $config): ?string - { - if (isset($config['object_manager'])) { - return $config['object_manager']; - } - - return $config['entity_manager'] ?: null; + $container->setAlias('gesdinet_jwt_refresh_token.object_manager', $objectManager); } } diff --git a/DependencyInjection/Security/Factory/RefreshTokenAuthenticatorFactory.php b/DependencyInjection/Security/Factory/RefreshTokenAuthenticatorFactory.php index 01c2dbb2..23972b59 100644 --- a/DependencyInjection/Security/Factory/RefreshTokenAuthenticatorFactory.php +++ b/DependencyInjection/Security/Factory/RefreshTokenAuthenticatorFactory.php @@ -17,6 +17,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Security\Http\Event\LogoutEvent; final class RefreshTokenAuthenticatorFactory implements AuthenticatorFactoryInterface { @@ -36,21 +37,15 @@ public function addConfiguration(NodeDefinition $builder): void $builder ->children() ->scalarNode('check_path') - ->defaultNull() - ->validate() - ->ifTrue(static fn ($v): bool => null === $v) - ->then(static function () { - trigger_deprecation( - 'gesdinet/jwt-refresh-token-bundle', - '1.1', - 'Not setting the "check_path" option for the "refresh_jwt" authenticator is deprecated, as of 2.0 the authenticator will only check the request path.' - ); - }) - ->end() + ->defaultValue('/login_check') ->end() ->scalarNode('provider')->end() ->scalarNode('success_handler')->end() ->scalarNode('failure_handler')->end() + ->booleanNode('invalidate_token_on_logout') + ->defaultTrue() + ->info('When enabled, the refresh token will be invalided on logout.') + ->end() /* ->integerNode('ttl') ->defaultNull() @@ -75,18 +70,23 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal // When per-authenticator configuration is supported, this array should be updated to check the $config values before falling back to the bundle parameters $options = [ - 'check_path' => $config['check_path'] ?? null, + 'check_path' => $config['check_path'], 'ttl' => new Parameter('gesdinet_jwt_refresh_token.ttl'), 'ttl_update' => new Parameter('gesdinet_jwt_refresh_token.ttl_update'), 'token_parameter_name' => new Parameter('gesdinet_jwt_refresh_token.token_parameter_name'), ]; - $container->setDefinition($authenticatorId, new ChildDefinition('gesdinet.jwtrefreshtoken.security.refresh_token_authenticator')) + $container->setDefinition($authenticatorId, new ChildDefinition('gesdinet_jwt_refresh_token.security.refresh_token_authenticator')) ->replaceArgument(3, new Reference($userProviderId)) ->replaceArgument(4, new Reference($this->createAuthenticationSuccessHandler($container, $firewallName, $config))) ->replaceArgument(5, new Reference($this->createAuthenticationFailureHandler($container, $firewallName, $config))) ->replaceArgument(6, $options); + if ($config['invalidate_token_on_logout']) { + $container->setDefinition('gesdinet_jwt_refresh_token.security.listener.logout.'.$firewallName, new ChildDefinition('gesdinet_jwt_refresh_token.security.listener.logout')) + ->addTag('kernel.event_listener', ['event' => LogoutEvent::class, 'method' => 'onLogout', 'dispatcher' => 'security.event_dispatcher.'.$firewallName]); + } + return $authenticatorId; } @@ -100,7 +100,7 @@ private function createAuthenticationSuccessHandler(ContainerBuilder $container, ->replaceArgument(1, []) ->replaceArgument(2, $id); } else { - $container->setDefinition($successHandlerId, new ChildDefinition('gesdinet.jwtrefreshtoken.security.authentication.success_handler')) + $container->setDefinition($successHandlerId, new ChildDefinition('gesdinet_jwt_refresh_token.security.authentication.success_handler')) ->addMethodCall('setFirewallName', [$id]); } @@ -116,7 +116,7 @@ private function createAuthenticationFailureHandler(ContainerBuilder $container, ->replaceArgument(0, new Reference($config['failure_handler'])) ->replaceArgument(1, []); } else { - $container->setDefinition($failureHandlerId, new ChildDefinition('gesdinet.jwtrefreshtoken.security.authentication.failure_handler')); + $container->setDefinition($failureHandlerId, new ChildDefinition('gesdinet_jwt_refresh_token.security.authentication.failure_handler')); } return $failureHandlerId; diff --git a/Doctrine/RefreshTokenManager.php b/Doctrine/RefreshTokenManager.php index 3bffaad6..6cfe5b2b 100644 --- a/Doctrine/RefreshTokenManager.php +++ b/Doctrine/RefreshTokenManager.php @@ -12,33 +12,29 @@ namespace Gesdinet\JWTRefreshTokenBundle\Doctrine; use Doctrine\Persistence\ObjectManager; -use Gesdinet\JWTRefreshTokenBundle\Generator\RefreshTokenGeneratorInterface; use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenInterface; use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManagerInterface; -class RefreshTokenManager implements RefreshTokenManagerInterface +final class RefreshTokenManager implements RefreshTokenManagerInterface { - /** - * @var ObjectManager - */ - protected $objectManager; + private ObjectManager $objectManager; /** * @var class-string */ - protected $class; + private string $class; /** * @var RefreshTokenRepositoryInterface */ - protected $repository; + private RefreshTokenRepositoryInterface $repository; /** * @param class-string $class * - * @throws \LogicException if the object repository does not implement `Gesdinet\JWTRefreshTokenBundle\Doctrine\RefreshTokenRepositoryInterface` + * @throws \LogicException if the object repository does not implement {@see RefreshTokenRepositoryInterface} */ - public function __construct(ObjectManager $om, $class) + public function __construct(ObjectManager $om, string $class) { $this->objectManager = $om; @@ -54,48 +50,17 @@ public function __construct(ObjectManager $om, $class) $this->class = $metadata->getName(); } - /** - * Creates an empty RefreshTokenInterface instance. - * - * @return RefreshTokenInterface - * - * @deprecated to be removed in 2.0, use a `Gesdinet\JWTRefreshTokenBundle\Generator\RefreshTokenGeneratorInterface` instead. - */ - public function create() - { - trigger_deprecation('gesdinet/jwt-refresh-token-bundle', '1.0', '%s() is deprecated and will be removed in 2.0, use a "%s" instance to create new %s objects.', __METHOD__, RefreshTokenGeneratorInterface::class, RefreshTokenInterface::class); - - $class = $this->getClass(); - - return new $class(); - } - - /** - * @param string $refreshToken - * - * @return RefreshTokenInterface|null - */ - public function get($refreshToken) + public function get(string $refreshToken): ?RefreshTokenInterface { return $this->repository->findOneBy(['refreshToken' => $refreshToken]); } - /** - * @param string $username - * - * @return RefreshTokenInterface|null - */ - public function getLastFromUsername($username) + public function getLastFromUsername(string $username): ?RefreshTokenInterface { return $this->repository->findOneBy(['username' => $username], ['valid' => 'DESC']); } - /** - * @param bool $andFlush - * - * @return void - */ - public function save(RefreshTokenInterface $refreshToken, $andFlush = true) + public function save(RefreshTokenInterface $refreshToken, bool $andFlush = true): void { $this->objectManager->persist($refreshToken); @@ -104,12 +69,7 @@ public function save(RefreshTokenInterface $refreshToken, $andFlush = true) } } - /** - * @param bool $andFlush - * - * @return void - */ - public function delete(RefreshTokenInterface $refreshToken, $andFlush = true) + public function delete(RefreshTokenInterface $refreshToken, bool $andFlush = true): void { $this->objectManager->remove($refreshToken); @@ -119,12 +79,9 @@ public function delete(RefreshTokenInterface $refreshToken, $andFlush = true) } /** - * @param \DateTimeInterface|null $datetime - * @param bool $andFlush - * * @return RefreshTokenInterface[] */ - public function revokeAllInvalid($datetime = null, $andFlush = true) + public function revokeAllInvalid(?\DateTimeInterface $datetime = null, bool $andFlush = true): array { $invalidTokens = $this->repository->findInvalid($datetime); @@ -140,11 +97,11 @@ public function revokeAllInvalid($datetime = null, $andFlush = true) } /** - * Returns the RefreshToken fully qualified class name. + * Returns the fully qualified class name for a concrete RefreshTokenInterface class. * * @return class-string */ - public function getClass() + public function getClass(): string { return $this->class; } diff --git a/Doctrine/RefreshTokenRepositoryInterface.php b/Doctrine/RefreshTokenRepositoryInterface.php index 649ba921..23ec11bc 100644 --- a/Doctrine/RefreshTokenRepositoryInterface.php +++ b/Doctrine/RefreshTokenRepositoryInterface.php @@ -3,7 +3,6 @@ namespace Gesdinet\JWTRefreshTokenBundle\Doctrine; use Doctrine\Persistence\ObjectRepository; -use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenInterface; /** * @template T of RefreshTokenInterface @@ -13,9 +12,7 @@ interface RefreshTokenRepositoryInterface extends ObjectRepository { /** - * @param \DateTimeInterface|null $datetime - * - * @return T[] + * @return iterable */ - public function findInvalid($datetime = null); + public function findInvalid(?\DateTimeInterface $datetime = null): iterable; } diff --git a/Document/AbstractRefreshToken.php b/Document/AbstractRefreshToken.php deleted file mode 100644 index 2b414878..00000000 --- a/Document/AbstractRefreshToken.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Gesdinet\JWTRefreshTokenBundle\Document; - -use Gesdinet\JWTRefreshTokenBundle\Model\AbstractRefreshToken as BaseAbstractRefreshToken; - -trigger_deprecation('gesdinet/jwt-refresh-token-bundle', '1.0', 'The "%s" class is deprecated, use "%s" instead.', AbstractRefreshToken::class, BaseAbstractRefreshToken::class); - -/** - * @deprecated Extend from `Gesdinet\JWTRefreshTokenBundle\Model\AbstractRefreshToken` instead - */ -abstract class AbstractRefreshToken extends BaseAbstractRefreshToken -{ -} diff --git a/Document/RefreshTokenRepository.php b/Document/RefreshTokenRepository.php index 0afcb4f5..2707413d 100644 --- a/Document/RefreshTokenRepository.php +++ b/Document/RefreshTokenRepository.php @@ -13,17 +13,14 @@ class RefreshTokenRepository extends DocumentRepository implements RefreshTokenRepositoryInterface { /** - * @param \DateTimeInterface|null $datetime - * - * @return RefreshToken[] + * @return iterable */ - public function findInvalid($datetime = null) + public function findInvalid(?\DateTimeInterface $datetime = null): iterable { - $datetime = (null === $datetime) ? new \DateTime() : $datetime; - - $queryBuilder = $this->createQueryBuilder() - ->field('valid')->lt($datetime); - - return $queryBuilder->getQuery()->execute(); + return $this->createQueryBuilder() + ->field('valid') + ->lt($datetime ?? new \DateTime()) + ->getQuery() + ->execute(); } } diff --git a/Entity/AbstractRefreshToken.php b/Entity/AbstractRefreshToken.php deleted file mode 100644 index 06c12e29..00000000 --- a/Entity/AbstractRefreshToken.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Gesdinet\JWTRefreshTokenBundle\Entity; - -use Gesdinet\JWTRefreshTokenBundle\Model\AbstractRefreshToken as BaseAbstractRefreshToken; - -trigger_deprecation('gesdinet/jwt-refresh-token-bundle', '1.0', 'The "%s" class is deprecated, use "%s" instead.', AbstractRefreshToken::class, BaseAbstractRefreshToken::class); - -/** - * @deprecated Extend from `Gesdinet\JWTRefreshTokenBundle\Model\AbstractRefreshToken` instead - */ -abstract class AbstractRefreshToken extends BaseAbstractRefreshToken -{ -} diff --git a/Entity/RefreshTokenRepository.php b/Entity/RefreshTokenRepository.php index 852405b3..d384bc3e 100644 --- a/Entity/RefreshTokenRepository.php +++ b/Entity/RefreshTokenRepository.php @@ -13,17 +13,13 @@ class RefreshTokenRepository extends EntityRepository implements RefreshTokenRepositoryInterface { /** - * @param \DateTimeInterface|null $datetime - * - * @return RefreshToken[] + * @return iterable */ - public function findInvalid($datetime = null) + public function findInvalid(?\DateTimeInterface $datetime = null): iterable { - $datetime = (null === $datetime) ? new \DateTime() : $datetime; - return $this->createQueryBuilder('u') ->where('u.valid < :datetime') - ->setParameter(':datetime', $datetime) + ->setParameter(':datetime', $datetime ?? new \DateTime()) ->getQuery() ->getResult(); } diff --git a/Event/RefreshAuthenticationFailureEvent.php b/Event/RefreshAuthenticationFailureEvent.php index ac046972..8139589f 100644 --- a/Event/RefreshAuthenticationFailureEvent.php +++ b/Event/RefreshAuthenticationFailureEvent.php @@ -15,7 +15,7 @@ use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Contracts\EventDispatcher\Event; -class RefreshAuthenticationFailureEvent extends Event +final class RefreshAuthenticationFailureEvent extends Event { private AuthenticationException $exception; diff --git a/Event/RefreshEvent.php b/Event/RefreshEvent.php index 65337a45..dbf328d7 100644 --- a/Event/RefreshEvent.php +++ b/Event/RefreshEvent.php @@ -15,7 +15,7 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Contracts\EventDispatcher\Event; -class RefreshEvent extends Event +final class RefreshEvent extends Event { private RefreshTokenInterface $refreshToken; @@ -30,24 +30,11 @@ public function __construct(RefreshTokenInterface $refreshToken, TokenInterface $this->firewallName = $firewallName; } - /** - * @return RefreshTokenInterface - */ - public function getRefreshToken() + public function getRefreshToken(): RefreshTokenInterface { return $this->refreshToken; } - /** - * @return TokenInterface - * - * @deprecated use getToken() instead - */ - public function getPreAuthenticatedToken() - { - return $this->getToken(); - } - public function getToken(): TokenInterface { return $this->token; diff --git a/Event/RefreshTokenNotFoundEvent.php b/Event/RefreshTokenNotFoundEvent.php index b3fc07e9..2b53add8 100644 --- a/Event/RefreshTokenNotFoundEvent.php +++ b/Event/RefreshTokenNotFoundEvent.php @@ -15,7 +15,7 @@ use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Contracts\EventDispatcher\Event; -class RefreshTokenNotFoundEvent extends Event +final class RefreshTokenNotFoundEvent extends Event { private AuthenticationException $exception; diff --git a/EventListener/AttachRefreshTokenOnSuccessListener.php b/EventListener/AttachRefreshTokenOnSuccessListener.php index 415022ba..54cba895 100644 --- a/EventListener/AttachRefreshTokenOnSuccessListener.php +++ b/EventListener/AttachRefreshTokenOnSuccessListener.php @@ -21,60 +21,34 @@ use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Kernel; -class AttachRefreshTokenOnSuccessListener +final class AttachRefreshTokenOnSuccessListener { - /** - * @var RefreshTokenManagerInterface - */ - protected $refreshTokenManager; - - /** - * @var int - */ - protected $ttl; - - /** - * @var RequestStack - */ - protected $requestStack; - - /** - * @var string - */ - protected $tokenParameterName; - - /** - * @var bool - */ - protected $singleUse; - - /** - * @var RefreshTokenGeneratorInterface - */ - protected $refreshTokenGenerator; - - /** - * @var ExtractorInterface - */ - protected $extractor; - - protected array $cookieSettings; - - protected bool $returnExpiration; - - protected string $returnExpirationParameterName; - - /** - * @param int $ttl - * @param string $tokenParameterName - * @param bool $singleUse - */ + private RefreshTokenManagerInterface $refreshTokenManager; + + private int $ttl; + + private RequestStack $requestStack; + + private string $tokenParameterName; + + private bool $singleUse; + + private RefreshTokenGeneratorInterface $refreshTokenGenerator; + + private ExtractorInterface $extractor; + + private array $cookieSettings; + + private bool $returnExpiration; + + private string $returnExpirationParameterName; + public function __construct( RefreshTokenManagerInterface $refreshTokenManager, - $ttl, + int $ttl, RequestStack $requestStack, - $tokenParameterName, - $singleUse, + string $tokenParameterName, + bool $singleUse, RefreshTokenGeneratorInterface $refreshTokenGenerator, ExtractorInterface $extractor, array $cookieSettings, diff --git a/EventListener/LogoutEventListener.php b/EventListener/LogoutEventListener.php index 68c574bc..8b6e54cd 100644 --- a/EventListener/LogoutEventListener.php +++ b/EventListener/LogoutEventListener.php @@ -14,22 +14,21 @@ use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManagerInterface; use Gesdinet\JWTRefreshTokenBundle\Request\Extractor\ExtractorInterface; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Http\Event\LogoutEvent; -class LogoutEventListener +final class LogoutEventListener { private RefreshTokenManagerInterface $refreshTokenManager; private ExtractorInterface $refreshTokenExtractor; private string $tokenParameterName; private array $cookieSettings; - private string $logout_firewall_context; public function __construct( RefreshTokenManagerInterface $refreshTokenManager, ExtractorInterface $refreshTokenExtractor, string $tokenParameterName, array $cookieSettings, - string $logout_firewall_context ) { $this->refreshTokenManager = $refreshTokenManager; $this->refreshTokenExtractor = $refreshTokenExtractor; @@ -44,19 +43,14 @@ public function __construct( 'partitioned' => false, 'remove_token_from_body' => true, ], $cookieSettings); - $this->logout_firewall_context = $logout_firewall_context; } public function onLogout(LogoutEvent $event): void { $request = $event->getRequest(); - $current_firewall_context = $request->attributes->get('_firewall_context'); - - if ($current_firewall_context !== $this->logout_firewall_context) { - return; - } $tokenString = $this->refreshTokenExtractor->getRefreshToken($request, $this->tokenParameterName); + if (null === $tokenString) { $event->setResponse( new JsonResponse( @@ -64,35 +58,36 @@ public function onLogout(LogoutEvent $event): void 'code' => 400, 'message' => 'No refresh_token found.', ], - JsonResponse::HTTP_BAD_REQUEST + Response::HTTP_BAD_REQUEST ) ); return; + } + + $refreshToken = $this->refreshTokenManager->get($tokenString); + + if (null === $refreshToken) { + $event->setResponse( + new JsonResponse( + [ + 'code' => 200, + 'message' => 'The supplied refresh_token is already invalid.', + ], + Response::HTTP_OK + ) + ); } else { - $refreshToken = $this->refreshTokenManager->get($tokenString); - if (null === $refreshToken) { - $event->setResponse( - new JsonResponse( - [ - 'code' => 200, - 'message' => 'The supplied refresh_token is already invalid.', - ], - JsonResponse::HTTP_OK - ) - ); - } else { - $this->refreshTokenManager->delete($refreshToken); - $event->setResponse( - new JsonResponse( - [ - 'code' => 200, - 'message' => 'The supplied refresh_token has been invalidated.', - ], - JsonResponse::HTTP_OK - ) - ); - } + $this->refreshTokenManager->delete($refreshToken); + $event->setResponse( + new JsonResponse( + [ + 'code' => 200, + 'message' => 'The supplied refresh_token has been invalidated.', + ], + Response::HTTP_OK + ) + ); } if ($this->cookieSettings['enabled']) { diff --git a/GesdinetJWTRefreshTokenBundle.php b/GesdinetJWTRefreshTokenBundle.php index b9da63a3..9135de2a 100644 --- a/GesdinetJWTRefreshTokenBundle.php +++ b/GesdinetJWTRefreshTokenBundle.php @@ -3,14 +3,10 @@ namespace Gesdinet\JWTRefreshTokenBundle; use Gesdinet\JWTRefreshTokenBundle\DependencyInjection\Compiler\AddExtractorsToChainCompilerPass; -use Gesdinet\JWTRefreshTokenBundle\DependencyInjection\Compiler\CustomUserProviderCompilerPass; -use Gesdinet\JWTRefreshTokenBundle\DependencyInjection\Compiler\ObjectManagerCompilerPass; -use Gesdinet\JWTRefreshTokenBundle\DependencyInjection\Compiler\UserCheckerCompilerPass; use Gesdinet\JWTRefreshTokenBundle\DependencyInjection\Security\Factory\RefreshTokenAuthenticatorFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; -use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface; class GesdinetJWTRefreshTokenBundle extends Bundle { @@ -19,15 +15,9 @@ public function build(ContainerBuilder $container): void parent::build($container); $container->addCompilerPass(new AddExtractorsToChainCompilerPass()); - $container->addCompilerPass(new CustomUserProviderCompilerPass(true)); - $container->addCompilerPass(new ObjectManagerCompilerPass()); - $container->addCompilerPass(new UserCheckerCompilerPass(true)); - // Only register the security authenticator for Symfony 5.4+ - if (interface_exists(RememberMeHandlerInterface::class)) { - /** @var SecurityExtension $extension */ - $extension = $container->getExtension('security'); - $extension->addAuthenticatorFactory(new RefreshTokenAuthenticatorFactory()); - } + /** @var SecurityExtension $extension */ + $extension = $container->getExtension('security'); + $extension->addAuthenticatorFactory(new RefreshTokenAuthenticatorFactory()); } } diff --git a/Http/RefreshAuthenticationFailureResponse.php b/Http/RefreshAuthenticationFailureResponse.php index 58841442..76e05e63 100644 --- a/Http/RefreshAuthenticationFailureResponse.php +++ b/Http/RefreshAuthenticationFailureResponse.php @@ -14,41 +14,7 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; -if (80000 <= \PHP_VERSION_ID && (new \ReflectionMethod(JsonResponse::class, 'setData'))->hasReturnType()) { - eval(' - namespace Gesdinet\JWTRefreshTokenBundle\Http; - - use Symfony\Component\HttpFoundation\JsonResponse; - - /** - * Compatibility layer for Symfony 6.0 and later. - * - * @internal - */ - abstract class CompatRefreshAuthenticationFailureResponse extends JsonResponse - { - public function setData(mixed $data = []): static - { - return parent::setData((array) $data + ["code" => $this->statusCode, "message" => $this->getMessage()]); - } - } - '); -} else { - /** - * Compatibility layer for Symfony 5.4 and earlier. - * - * @internal - */ - abstract class CompatRefreshAuthenticationFailureResponse extends JsonResponse - { - public function setData($data = []) - { - return parent::setData((array) $data + ['code' => $this->statusCode, 'message' => $this->getMessage()]); - } - } -} - -class RefreshAuthenticationFailureResponse extends CompatRefreshAuthenticationFailureResponse +class RefreshAuthenticationFailureResponse extends JsonResponse { private string $message; @@ -59,6 +25,11 @@ public function __construct(string $message = 'Bad credentials', int $statusCode parent::__construct(null, $statusCode); } + public function setData(mixed $data = []): static + { + return parent::setData((array) $data + ['code' => $this->statusCode, 'message' => $this->getMessage()]); + } + public function setMessage(string $message): self { $this->message = $message; diff --git a/Model/AbstractRefreshToken.php b/Model/AbstractRefreshToken.php index 5cfa3d4d..28c1a628 100644 --- a/Model/AbstractRefreshToken.php +++ b/Model/AbstractRefreshToken.php @@ -15,30 +15,18 @@ abstract class AbstractRefreshToken implements RefreshTokenInterface { - /** - * @var int|string|null - */ - protected $id; + protected int|string|null $id = null; - /** - * @var string|null - */ - protected $refreshToken; + protected ?string $refreshToken = null; - /** - * @var string|null - */ - protected $username; + protected ?string $username = null; - /** - * @var \DateTimeInterface|null - */ - protected $valid; + protected ?\DateTimeInterface $valid = null; /** * Creates a new model instance based on the provided details. */ - public static function createForUserWithTtl(string $refreshToken, UserInterface $user, int $ttl): RefreshTokenInterface + public static function createForUserWithTtl(string $refreshToken, UserInterface $user, int $ttl): static { $valid = new \DateTime(); @@ -57,86 +45,53 @@ public static function createForUserWithTtl(string $refreshToken, UserInterface return $model; } - /** - * @return string Refresh Token - */ - public function __toString() + public function __toString(): string { return $this->getRefreshToken() ?: ''; } - /** - * {@inheritdoc} - */ - public function getId() + public function getId(): int|string|null { return $this->id; } - /** - * {@inheritdoc} - */ - public function setRefreshToken($refreshToken = null) + public function setRefreshToken(string $refreshToken): static { - if (null === $refreshToken || '' === $refreshToken) { - trigger_deprecation('gesdinet/jwt-refresh-token-bundle', '1.0', 'Passing an empty token to %s() to automatically generate a token is deprecated.', __METHOD__); - - $refreshToken = bin2hex(random_bytes(64)); - } - $this->refreshToken = $refreshToken; return $this; } - /** - * {@inheritdoc} - */ - public function getRefreshToken() + public function getRefreshToken(): ?string { return $this->refreshToken; } - /** - * {@inheritdoc} - */ - public function setValid($valid) + public function setValid(\DateTimeInterface $valid): static { $this->valid = $valid; return $this; } - /** - * {@inheritdoc} - */ - public function getValid() + public function getValid(): ?\DateTimeInterface { return $this->valid; } - /** - * {@inheritdoc} - */ - public function setUsername($username) + public function setUsername(string $username): static { $this->username = $username; return $this; } - /** - * {@inheritdoc} - */ - public function getUsername() + public function getUsername(): ?string { return $this->username; } - /** - * {@inheritdoc} - */ - public function isValid() + public function isValid(): bool { return null !== $this->valid && $this->valid >= new \DateTime(); } diff --git a/Model/RefreshTokenInterface.php b/Model/RefreshTokenInterface.php index 0b38c476..c10896b7 100644 --- a/Model/RefreshTokenInterface.php +++ b/Model/RefreshTokenInterface.php @@ -18,51 +18,21 @@ interface RefreshTokenInterface extends \Stringable /** * Creates a new model instance based on the provided details. */ - public static function createForUserWithTtl(string $refreshToken, UserInterface $user, int $ttl): RefreshTokenInterface; + public static function createForUserWithTtl(string $refreshToken, UserInterface $user, int $ttl): static; - /** - * @return int|string|null - */ - public function getId(); + public function getId(): int|string|null; - /** - * @param string|null $refreshToken - * - * @return $this - */ - public function setRefreshToken($refreshToken = null); + public function setRefreshToken(string $refreshToken): static; - /** - * @return string|null - */ - public function getRefreshToken(); + public function getRefreshToken(): ?string; - /** - * @param \DateTimeInterface|null $valid - * - * @return $this - */ - public function setValid($valid); + public function setValid(\DateTimeInterface $valid): static; - /** - * @return \DateTimeInterface|null - */ - public function getValid(); + public function getValid(): ?\DateTimeInterface; - /** - * @param string|null $username - * - * @return $this - */ - public function setUsername($username); + public function setUsername(string $username): static; - /** - * @return string|null - */ - public function getUsername(); + public function getUsername(): ?string; - /** - * @return bool - */ - public function isValid(); + public function isValid(): bool; } diff --git a/Model/RefreshTokenManager.php b/Model/RefreshTokenManager.php deleted file mode 100644 index b3cf1e7d..00000000 --- a/Model/RefreshTokenManager.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Gesdinet\JWTRefreshTokenBundle\Model; - -use Gesdinet\JWTRefreshTokenBundle\Generator\RefreshTokenGeneratorInterface; - -trigger_deprecation('gesdinet/jwt-refresh-token-bundle', '1.0', 'The "%s" class is deprecated, implement "%s" directly.', RefreshTokenManager::class, RefreshTokenManagerInterface::class); - -/** - * @deprecated to be removed in 2.0, implement `Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManagerInterface` directly. - */ -abstract class RefreshTokenManager implements RefreshTokenManagerInterface -{ - /** - * Creates an empty RefreshTokenInterface instance. - * - * @return RefreshTokenInterface - * - * @deprecated to be removed in 2.0, use a `Gesdinet\JWTRefreshTokenBundle\Generator\RefreshTokenGeneratorInterface` instead. - */ - public function create() - { - trigger_deprecation('gesdinet/jwt-refresh-token-bundle', '1.0', '%s() is deprecated and will be removed in 2.0, use a "%s" instance to create new %s objects.', __METHOD__, RefreshTokenGeneratorInterface::class, RefreshTokenInterface::class); - - $class = $this->getClass(); - - return new $class(); - } -} diff --git a/Model/RefreshTokenManagerInterface.php b/Model/RefreshTokenManagerInterface.php index 89cfdcb6..6dd8c409 100644 --- a/Model/RefreshTokenManagerInterface.php +++ b/Model/RefreshTokenManagerInterface.php @@ -19,50 +19,23 @@ */ interface RefreshTokenManagerInterface { - /** - * Creates an empty RefreshTokenInterface instance. - * - * @return RefreshTokenInterface - * - * @deprecated to be removed in 2.0, use a `Gesdinet\JWTRefreshTokenBundle\Generator\RefreshTokenGeneratorInterface` instead. - */ - public function create(); - - /** - * @param string $refreshToken - * - * @return RefreshTokenInterface|null - */ - public function get($refreshToken); + public function get(string $refreshToken): ?RefreshTokenInterface; - /** - * @param string $username - * - * @return RefreshTokenInterface|null - */ - public function getLastFromUsername($username); + public function getLastFromUsername(string $username): ?RefreshTokenInterface; - /** - * @return void - */ - public function save(RefreshTokenInterface $refreshToken); + public function save(RefreshTokenInterface $refreshToken): void; - /** - * @return void - */ - public function delete(RefreshTokenInterface $refreshToken); + public function delete(RefreshTokenInterface $refreshToken): void; /** - * @param \DateTimeInterface|null $datetime - * * @return RefreshTokenInterface[] */ - public function revokeAllInvalid($datetime = null); + public function revokeAllInvalid(?\DateTimeInterface $datetime = null): array; /** * Returns the fully qualified class name for a concrete RefreshTokenInterface class. * * @return class-string */ - public function getClass(); + public function getClass(): string; } diff --git a/README.md b/README.md index 4f90b85a..0824564a 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ The purpose of this bundle is manage refresh tokens with JWT (Json Web Tokens) i ## Prerequisites -This bundle requires PHP 7.4 or later and Symfony 4.4, 5.4, or 6.0+. +This bundle requires PHP 8.1 or later and Symfony 5.4, or 6.3+. -For support with older Symfony versions, please use the 0.12 release. +For support with older Symfony versions, please use the 1.x release. **Protip:** Though the bundle doesn't force you to do so, it is highly recommended to use HTTPS. @@ -25,8 +25,6 @@ For support with older Symfony versions, please use the 0.12 release. **You must also install either the Doctrine ORM or MongoDB ODM, these packages are not installed automatically with this bundle. Failing to do so may trigger errors on installation.** -If using Symfony 4.4, you will also need to install the `symfony/security-guard` package, it is only required for the legacy authentication API and is not compatible with Symfony 6.0. - With Doctrine's ORM ```bash @@ -44,11 +42,11 @@ Or, manually edit your project's `composer.json` file to add the required packag ```json { "require": { - "doctrine/doctrine-bundle": "^2.0", - "doctrine/mongodb-odm": "^2.0", - "doctrine/mongodb-odm-bundle": "^4.0", - "doctrine/orm": "^2.7", - "gesdinet/jwt-refresh-token-bundle": "^1.0" + "doctrine/doctrine-bundle": "^2.10", + "doctrine/mongodb-odm": "^2.3", + "doctrine/mongodb-odm-bundle": "^4.5", + "doctrine/orm": "^2.12", + "gesdinet/jwt-refresh-token-bundle": "^2.0" } } ``` @@ -129,7 +127,7 @@ class RefreshToken extends BaseRefreshToken } ``` -### Step 4 (Symfony 5.4+) +### Step 4 #### Define the refresh token route @@ -175,42 +173,6 @@ security: # ... ``` -### Step 4 (Symfony 4.4) - -#### Define the refresh token route - -Open your routing configuration file and add the following route to it: - -```yaml -# config/routes.yaml -api_refresh_token: - path: /api/token/refresh - controller: gesdinet.jwtrefreshtoken::refresh -# ... -``` - -#### Configure the security firewall - -Add the below to your security configuration file: - -```yaml -# config/packages/security.yaml -security: - firewalls: - # put it before all your other firewall API entries - refresh: - pattern: ^/api/token/refresh - stateless: true - anonymous: true - # ... - - access_control: - # ... - - { path: ^/api/token/refresh, roles: IS_AUTHENTICATED_ANONYMOUSLY } - # ... -# ... -``` - ### Step 5: Update your database schema You will need to add the table for the refresh tokens to your application's database. @@ -292,8 +254,6 @@ gesdinet_jwt_refresh_token: ### Set The User Provider -#### Symfony 5.4+ - You can define a user provider to use for the authenticator its configuration. Note, if your application has multiple user providers, you **MUST** configure this value for either the firewall or the provider. @@ -315,39 +275,8 @@ security: By default, when a user provider is not specified, then the user provider for the firewall is used instead. -#### Symfony 4.4 - -*NOTE* This setting is deprecated and is not used with the `refresh_jwt` authenticator - -You can define your own user provider, by default the `gesdinet.jwtrefreshtoken.user_provider` service is used. You can change this value by adding this line to your config: - -```yaml -gesdinet_jwt_refresh_token: - user_provider: user_provider_service_id -``` - -For example, if you are using FOSUserBundle, `user_provider` must be set to `fos_user.user_provider.username_email`. - -For Doctrine ORM UserProvider, `user_provider` must be set to `security.user.provider.concrete.`. - -For example, in your `config/packages/security.yaml` file: -```yaml -security: - # ... - providers: - app_user_provider: - # ... - firewalls: - # ... -# ... -``` - -then your user_provider_service_id is `security.user.provider.concrete.app_user_provider`. - ### Set The User Checker -#### Symfony 5.4+ - You can define a user checker to use for the firewall as part of the firewall configuration: ```yaml @@ -361,19 +290,6 @@ security: refresh_jwt: ~ ``` -#### Symfony 4.4 - -*NOTE* This setting is deprecated and is not used with the `refresh_jwt` authenticator - -You can define your own user checker, by default the `security.user_checker` service is used. You can change this value by adding this line to your config: - -```yaml -gesdinet_jwt_refresh_token: - user_checker: user_checker_service_id -``` - -You will probably want to use a custom user provider along with your user checker to ensure that the checker receives the right type of user. - ### Single Use Tokens You can configure the refresh token so it can only be consumed _once_. If set to `true` and the refresh token is consumed, a new refresh token will be provided. diff --git a/Resources/config/services.php b/Resources/config/services.php index ebfe6943..c9352f12 100644 --- a/Resources/config/services.php +++ b/Resources/config/services.php @@ -1,5 +1,7 @@ services(); - $services->set('gesdinet.jwtrefreshtoken.send_token') + $services->set('gesdinet_jwt_refresh_token.event_listener.attach_refresh_token') ->class(AttachRefreshTokenOnSuccessListener::class) ->args([ - new Reference('gesdinet.jwtrefreshtoken.refresh_token_manager'), - new Parameter('gesdinet_jwt_refresh_token.ttl'), - new Reference('request_stack'), - new Parameter('gesdinet_jwt_refresh_token.token_parameter_name'), - new Parameter('gesdinet_jwt_refresh_token.single_use'), - new Reference('gesdinet.jwtrefreshtoken.refresh_token_generator'), - new Reference('gesdinet.jwtrefreshtoken.request.extractor.chain'), - new Parameter('gesdinet_jwt_refresh_token.cookie'), - new Parameter('gesdinet_jwt_refresh_token.return_expiration'), - new Parameter('gesdinet_jwt_refresh_token.return_expiration_parameter_name'), + service('gesdinet_jwt_refresh_token.refresh_token_manager'), + param('gesdinet_jwt_refresh_token.ttl'), + service('request_stack'), + param('gesdinet_jwt_refresh_token.token_parameter_name'), + param('gesdinet_jwt_refresh_token.single_use'), + service('gesdinet_jwt_refresh_token.refresh_token_generator'), + service('gesdinet_jwt_refresh_token.request.extractor.chain'), + param('gesdinet_jwt_refresh_token.cookie'), + param('gesdinet_jwt_refresh_token.return_expiration'), + param('gesdinet_jwt_refresh_token.return_expiration_parameter_name'), ]) ->tag('kernel.event_listener', [ - 'event' => 'lexik_jwt_authentication.on_authentication_success', + 'event' => Events::AUTHENTICATION_SUCCESS, 'method' => 'attachRefreshToken', ]); - $services->set('gesdinet.jwtrefreshtoken.refresh_token_generator') + $services->set('gesdinet_jwt_refresh_token.refresh_token_generator') ->class(RefreshTokenGenerator::class) ->public() ->args([ - new Reference('gesdinet.jwtrefreshtoken.refresh_token_manager'), + service('gesdinet_jwt_refresh_token.refresh_token_manager'), ]); - $services->alias(RefreshTokenGeneratorInterface::class, 'gesdinet.jwtrefreshtoken.refresh_token_generator'); + $services->alias(RefreshTokenGeneratorInterface::class, 'gesdinet_jwt_refresh_token.refresh_token_generator'); - $services->set('gesdinet.jwtrefreshtoken.refresh_token_manager') + $services->set('gesdinet_jwt_refresh_token.refresh_token_manager') ->class(RefreshTokenManager::class) ->public() ->args([ - new Reference('gesdinet.jwtrefreshtoken.object_manager'), - new Parameter('gesdinet.jwtrefreshtoken.refresh_token.class'), + service('gesdinet_jwt_refresh_token.object_manager'), + param('gesdinet_jwt_refresh_token.refresh_token.class'), ]); - $services->alias(RefreshTokenManagerInterface::class, 'gesdinet.jwtrefreshtoken.refresh_token_manager'); + $services->alias(RefreshTokenManagerInterface::class, 'gesdinet_jwt_refresh_token.refresh_token_manager'); - $services->set('gesdinet.jwtrefreshtoken.request.extractor.chain') + $services->set('gesdinet_jwt_refresh_token.request.extractor.chain') ->class(ChainExtractor::class) ->public(); - $services->alias(ExtractorInterface::class, 'gesdinet.jwtrefreshtoken.request.extractor.chain'); + $services->alias(ExtractorInterface::class, 'gesdinet_jwt_refresh_token.request.extractor.chain'); - $services->set('gesdinet.jwtrefreshtoken.request.extractor.request_body') + $services->set('gesdinet_jwt_refresh_token.request.extractor.request_body') ->class(RequestBodyExtractor::class) ->tag('gesdinet_jwt_refresh_token.request_extractor'); - $services->set('gesdinet.jwtrefreshtoken.request.extractor.request_parameter') + $services->set('gesdinet_jwt_refresh_token.request.extractor.request_parameter') ->class(RequestParameterExtractor::class) ->tag('gesdinet_jwt_refresh_token.request_extractor'); - $services->set('gesdinet.jwtrefreshtoken.request.extractor.request_cookie') + $services->set('gesdinet_jwt_refresh_token.request.extractor.request_cookie') ->class(RequestCookieExtractor::class) ->tag('gesdinet_jwt_refresh_token.request_extractor'); - $services->set('gesdinet.jwtrefreshtoken') - ->deprecate(...$deprecateArgs('1.0')) - ->class(RefreshToken::class) - ->public() - ->args([ - new Reference('gesdinet.jwtrefreshtoken.authenticator'), - new Reference('gesdinet.jwtrefreshtoken.user_provider'), - new Reference('lexik_jwt_authentication.handler.authentication_success'), - new Reference('lexik_jwt_authentication.handler.authentication_failure'), - new Reference('gesdinet.jwtrefreshtoken.refresh_token_manager'), - new Parameter('gesdinet_jwt_refresh_token.ttl'), - new Parameter('gesdinet_jwt_refresh_token.security.firewall'), - new Parameter('gesdinet_jwt_refresh_token.ttl_update'), - new Reference('event_dispatcher'), - ]); - - $services->set('gesdinet.jwtrefreshtoken.user_provider') - ->deprecate(...$deprecateArgs('1.0')) - ->class(RefreshTokenProvider::class) - ->args([ - new Reference('gesdinet.jwtrefreshtoken.refresh_token_manager'), - ]); - - $services->set('gesdinet.jwtrefreshtoken.authenticator') - ->deprecate(...$deprecateArgs('1.0')) - ->class(LegacyRefreshTokenAuthenticator::class) - ->args([ - new Reference('gesdinet.jwtrefreshtoken.user_checker'), - new Parameter('gesdinet_jwt_refresh_token.token_parameter_name'), - new Reference('gesdinet.jwtrefreshtoken.request.extractor.chain'), - ]); - - $services->set('gesdinet.jwtrefreshtoken.security.authentication.failure_handler') + $services->set('gesdinet_jwt_refresh_token.security.authentication.failure_handler') ->class(AuthenticationFailureHandler::class) ->args([ - new Reference('event_dispatcher'), + service('event_dispatcher'), ]); - $services->set('gesdinet.jwtrefreshtoken.security.authentication.success_handler') + $services->set('gesdinet_jwt_refresh_token.security.authentication.success_handler') ->class(AuthenticationSuccessHandler::class) ->args([ - new Reference('lexik_jwt_authentication.handler.authentication_success'), - new Reference('event_dispatcher'), + service('lexik_jwt_authentication.handler.authentication_success'), + service('event_dispatcher'), ]); - $services->set('gesdinet.jwtrefreshtoken.security.refresh_token_authenticator') + $services->set('gesdinet_jwt_refresh_token.security.refresh_token_authenticator') ->abstract() ->class(RefreshTokenAuthenticator::class) ->args([ - new Reference('gesdinet.jwtrefreshtoken.refresh_token_manager'), - new Reference('event_dispatcher'), - new Reference('gesdinet.jwtrefreshtoken.request.extractor.chain'), - $abstractArg('user provider'), - $abstractArg('authentication success handler'), - $abstractArg('authentication failure handler'), - $abstractArg('options'), - new Reference('security.http_utils'), + service('gesdinet_jwt_refresh_token.refresh_token_manager'), + service('event_dispatcher'), + service('gesdinet_jwt_refresh_token.request.extractor.chain'), + abstract_arg('user provider'), + abstract_arg('authentication success handler'), + abstract_arg('authentication failure handler'), + abstract_arg('options'), + service('security.http_utils'), ]); $services->set(ClearInvalidRefreshTokensCommand::class) ->args([ - new Reference('gesdinet.jwtrefreshtoken.refresh_token_manager'), + service('gesdinet_jwt_refresh_token.refresh_token_manager'), ]) ->tag('console.command'); $services->set(RevokeRefreshTokenCommand::class) ->args([ - new Reference('gesdinet.jwtrefreshtoken.refresh_token_manager'), + service('gesdinet_jwt_refresh_token.refresh_token_manager'), ]) ->tag('console.command'); - $services->set(LogoutEventListener::class) + $services->set('gesdinet_jwt_refresh_token.security.listener.logout') + ->abstract() + ->class(LogoutEventListener::class) ->args([ - new Reference('gesdinet.jwtrefreshtoken.refresh_token_manager'), - new Reference('gesdinet.jwtrefreshtoken.request.extractor.chain'), - new Parameter('gesdinet_jwt_refresh_token.token_parameter_name'), - new Parameter('gesdinet_jwt_refresh_token.cookie'), - new Parameter('gesdinet_jwt_refresh_token.logout_firewall_context'), - ]) - ->tag('kernel.event_listener', [ - 'event' => 'Symfony\Component\Security\Http\Event\LogoutEvent', - 'method' => 'onLogout', + service('gesdinet_jwt_refresh_token.refresh_token_manager'), + service('gesdinet_jwt_refresh_token.request.extractor.chain'), + param('gesdinet_jwt_refresh_token.token_parameter_name'), + param('gesdinet_jwt_refresh_token.cookie'), ]); }; diff --git a/Security/Authenticator/RefreshTokenAuthenticator.php b/Security/Authenticator/RefreshTokenAuthenticator.php deleted file mode 100644 index fa9e128e..00000000 --- a/Security/Authenticator/RefreshTokenAuthenticator.php +++ /dev/null @@ -1,166 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Gesdinet\JWTRefreshTokenBundle\Security\Authenticator; - -use Gesdinet\JWTRefreshTokenBundle\Request\Extractor\ExtractorInterface; -use Gesdinet\JWTRefreshTokenBundle\Exception\UnknownRefreshTokenException; -use Gesdinet\JWTRefreshTokenBundle\Exception\UnknownUserFromRefreshTokenException; -use Gesdinet\JWTRefreshTokenBundle\Security\Exception\MissingTokenException; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Exception\AuthenticationException; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; -use Symfony\Component\Security\Core\Exception\UserNotFoundException; -use Symfony\Component\Security\Core\User\UserCheckerInterface; -use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Core\User\UserProviderInterface; -use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; -use Symfony\Component\HttpFoundation\Response; -use Gesdinet\JWTRefreshTokenBundle\Security\Provider\RefreshTokenProvider; - -trigger_deprecation('gesdinet/jwt-refresh-token-bundle', '1.0', 'The "%s" class is deprecated, use the `refresh_jwt` authenticator instead.', RefreshTokenAuthenticator::class); - -/** - * @deprecated use the `refresh_jwt` authenticator instead - */ -class RefreshTokenAuthenticator extends AbstractGuardAuthenticator -{ - private UserCheckerInterface $userChecker; - - /** - * @var string - */ - protected $tokenParameterName; - - /** - * @var ExtractorInterface - */ - protected $extractor; - - /** - * @param string $tokenParameterName - */ - public function __construct(UserCheckerInterface $userChecker, $tokenParameterName, ExtractorInterface $extractor) - { - $this->userChecker = $userChecker; - $this->tokenParameterName = $tokenParameterName; - $this->extractor = $extractor; - } - - /** - * @return bool - */ - public function supports(Request $request) - { - return null !== $this->extractor->getRefreshToken($request, $this->tokenParameterName); - } - - /** - * @return array{token: string|null} - */ - public function getCredentials(Request $request) - { - return [ - 'token' => $this->extractor->getRefreshToken($request, $this->tokenParameterName), - ]; - } - - /** - * @param array{token: string|null} $credentials - * - * @return UserInterface - */ - public function getUser($credentials, UserProviderInterface $userProvider) - { - if (!$userProvider instanceof RefreshTokenProvider) { - throw new \InvalidArgumentException(sprintf('The user provider must be an instance of RefreshTokenProvider (%s was given).', get_class($userProvider))); - } - - $refreshToken = $credentials['token'] ?? null; - - if (null === $refreshToken) { - throw new MissingTokenException('The refresh token could not be read from the request.'); - } - - $username = $userProvider->getUsernameForRefreshToken($refreshToken); - - if (null === $username) { - throw new UnknownRefreshTokenException(sprintf('Refresh token "%s" does not exist.', $refreshToken)); - } - - try { - $user = $userProvider->loadUserByUsername($username); - } catch (UsernameNotFoundException|UserNotFoundException $exception) { - throw new UnknownUserFromRefreshTokenException(sprintf('User with refresh token "%s" does not exist.', $refreshToken), $exception->getCode(), $exception); - } - - $this->userChecker->checkPreAuth($user); - $this->userChecker->checkPostAuth($user); - - return $user; - } - - /** - * @param array{token: string|null} $credentials - * - * @return bool - */ - public function checkCredentials($credentials, UserInterface $user) - { - // check credentials - e.g. make sure the password is valid - // no credential check is needed in this case - - // return true to cause authentication success - return true; - } - - /** - * @param string $providerKey - * - * @return null - */ - public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) - { - // on success, let the request continue - return null; - } - - /** - * @return Response - */ - public function onAuthenticationFailure(Request $request, AuthenticationException $exception) - { - return new Response('Refresh token authentication failed.', 403); - } - - /** - * @return Response - */ - public function start(Request $request, AuthenticationException $authException = null) - { - $data = [ - // you might translate this message - 'message' => 'Authentication Required', - ]; - - return new JsonResponse($data, Response::HTTP_UNAUTHORIZED); - } - - /** - * @return bool - */ - public function supportsRememberMe() - { - return false; - } -} diff --git a/Security/Http/Authentication/AuthenticationFailureHandler.php b/Security/Http/Authentication/AuthenticationFailureHandler.php index 82c34ec7..4287b6fd 100644 --- a/Security/Http/Authentication/AuthenticationFailureHandler.php +++ b/Security/Http/Authentication/AuthenticationFailureHandler.php @@ -19,7 +19,7 @@ use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; -class AuthenticationFailureHandler implements AuthenticationFailureHandlerInterface +final class AuthenticationFailureHandler implements AuthenticationFailureHandlerInterface { private EventDispatcherInterface $eventDispatcher; diff --git a/Security/Http/Authentication/AuthenticationSuccessHandler.php b/Security/Http/Authentication/AuthenticationSuccessHandler.php index db437a33..d6984738 100644 --- a/Security/Http/Authentication/AuthenticationSuccessHandler.php +++ b/Security/Http/Authentication/AuthenticationSuccessHandler.php @@ -19,16 +19,13 @@ use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; -class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface +final class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface { private AuthenticationSuccessHandlerInterface $lexikAuthenticationSuccessHandler; private EventDispatcherInterface $eventDispatcher; - /** - * @var string|null - */ - protected $firewallName; + private ?string $firewallName = null; public function __construct( AuthenticationSuccessHandlerInterface $lexikAuthenticationSuccessHandler, diff --git a/Security/Http/Authenticator/RefreshTokenAuthenticator.php b/Security/Http/Authenticator/RefreshTokenAuthenticator.php index 6102ff71..be09d42a 100644 --- a/Security/Http/Authenticator/RefreshTokenAuthenticator.php +++ b/Security/Http/Authenticator/RefreshTokenAuthenticator.php @@ -38,7 +38,7 @@ use Symfony\Component\Security\Http\HttpUtils; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; -class RefreshTokenAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface +final class RefreshTokenAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface { private RefreshTokenManagerInterface $refreshTokenManager; @@ -64,7 +64,7 @@ public function __construct( AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options, - ?HttpUtils $httpUtils = null + HttpUtils $httpUtils ) { $this->refreshTokenManager = $refreshTokenManager; $this->eventDispatcher = $eventDispatcher; @@ -73,27 +73,17 @@ public function __construct( $this->successHandler = $successHandler; $this->failureHandler = $failureHandler; $this->options = array_merge([ - 'check_path' => null, // @todo in 2.0, change the default to `/login_check` + 'check_path' => '/login_check', 'ttl' => 2592000, 'ttl_update' => false, 'token_parameter_name' => 'refresh_token', ], $options); $this->httpUtils = $httpUtils; - - if (null === $httpUtils) { - trigger_deprecation('gesdinet/jwt-refresh-token-bundle', '1.1', 'Not passing an instance of "%s" to the "%s" constructor is deprecated, it will be required in 2.0.', HttpUtils::class, self::class); - } } public function supports(Request $request): bool { - if (null !== $this->httpUtils && null !== $this->options['check_path']) { - return $this->httpUtils->checkRequestPath($request, $this->options['check_path']); - } - - trigger_deprecation('gesdinet/jwt-refresh-token-bundle', '1.1', 'Checking if the refresh token is in the request in %s() is deprecated, as of 2.0 only the request path will be checked.', __METHOD__); - - return null !== $this->extractor->getRefreshToken($request, $this->options['token_parameter_name']); + return $this->httpUtils->checkRequestPath($request, $this->options['check_path']); } public function authenticate(Request $request): Passport @@ -138,7 +128,7 @@ public function authenticate(Request $request): Passport } /** - * @deprecated to be removed in 2.0, use `createToken()` instead + * @deprecated to be removed when dropping support for Symfony 5.4, use {@see createToken()} instead */ public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface { @@ -148,7 +138,19 @@ public function createAuthenticatedToken(PassportInterface $passport, string $fi trigger_deprecation('gesdinet/jwt-refresh-token-bundle', '1.0', '"%s()" is deprecated, use "%s::createToken()" instead.', __METHOD__, __CLASS__); - return $this->createToken($passport, $firewallName); + /** @var RefreshTokenInterface|null $refreshToken */ + $refreshToken = $passport->getAttribute('refreshToken'); + + if (null === $refreshToken) { + throw new LogicException('Passport does not contain the refresh token.'); + } + + return new PostRefreshTokenAuthenticationToken( + $passport->getUser(), + $firewallName, + $passport->getUser()->getRoles(), + $refreshToken + ); } public function createToken(Passport $passport, string $firewallName): TokenInterface diff --git a/Security/Provider/RefreshTokenProvider.php b/Security/Provider/RefreshTokenProvider.php deleted file mode 100644 index f6a1d106..00000000 --- a/Security/Provider/RefreshTokenProvider.php +++ /dev/null @@ -1,175 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Gesdinet\JWTRefreshTokenBundle\Security\Provider; - -use Symfony\Component\Security\Core\User\InMemoryUser; -use Symfony\Component\Security\Core\User\UserProviderInterface; -use Symfony\Component\Security\Core\User\User; -use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Core\Exception\UnsupportedUserException; -use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManagerInterface; -use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenInterface; - -trigger_deprecation('gesdinet/jwt-refresh-token-bundle', '1.0', 'The "%s" class is deprecated, configure the user provider for the `refresh_jwt` authenticator instead.', RefreshTokenProvider::class); - -if ((new \ReflectionClass(UserProviderInterface::class))->getMethod('supportsClass')->hasReturnType()) { - /** - * Compatibility layer for Symfony 7.0 and later, where {@see UserProviderInterface::supportsClass()} has a return type. - * - * @internal - */ - abstract class CompatRefreshTokenProvider implements UserProviderInterface - { - /** - * @param class-string $class - */ - public function supportsClass(string $class): bool - { - return $this->doSupportsClass($class); - } - - /** - * @param class-string $class - */ - abstract protected function doSupportsClass(string $class): bool; - } -} else { - /** - * Compatibility layer for Symfony 6.4 and earlier, where {@see UserProviderInterface::supportsClass()} does not have a return type. - * - * @internal - */ - abstract class CompatRefreshTokenProvider implements UserProviderInterface - { - /** - * @param class-string $class - * - * @return bool - */ - public function supportsClass($class) - { - return $this->doSupportsClass($class); - } - - /** - * @param class-string $class - */ - abstract protected function doSupportsClass(string $class): bool; - } -} - -/** - * @deprecated configure the user provider for the `refresh_jwt` authenticator instead - */ -class RefreshTokenProvider extends CompatRefreshTokenProvider -{ - /** - * @var RefreshTokenManagerInterface - */ - protected $refreshTokenManager; - - /** - * @var UserProviderInterface - */ - protected $customUserProvider; - - public function __construct(RefreshTokenManagerInterface $refreshTokenManager) - { - $this->refreshTokenManager = $refreshTokenManager; - } - - /** - * @return void - */ - public function setCustomUserProvider(UserProviderInterface $customUserProvider) - { - $this->customUserProvider = $customUserProvider; - } - - /** - * @param string $token - * - * @return string|null - */ - public function getUsernameForRefreshToken($token) - { - $refreshToken = $this->refreshTokenManager->get($token); - - if ($refreshToken instanceof RefreshTokenInterface) { - return $refreshToken->getUsername(); - } - - return null; - } - - /** - * @param string $username - * - * @return UserInterface - * - * @deprecated use loadUserByIdentifier() instead - */ - public function loadUserByUsername($username) - { - return $this->loadUserByIdentifier($username); - } - - public function loadUserByIdentifier(string $identifier): UserInterface - { - if (null !== $this->customUserProvider) { - if (method_exists($this->customUserProvider, 'loadUserByIdentifier')) { - return $this->customUserProvider->loadUserByIdentifier($identifier); - } - - return $this->customUserProvider->loadUserByUsername($identifier); - } - - if (class_exists(InMemoryUser::class)) { - return new InMemoryUser( - $identifier, - null, - ['ROLE_USER'] - ); - } - - return new User( - $identifier, - null, - ['ROLE_USER'] - ); - } - - public function refreshUser(UserInterface $user): UserInterface - { - if (null !== $this->customUserProvider) { - return $this->customUserProvider->refreshUser($user); - } - - throw new UnsupportedUserException(); - } - - /** - * @param class-string $class - */ - protected function doSupportsClass(string $class): bool - { - if (null !== $this->customUserProvider) { - return $this->customUserProvider->supportsClass($class); - } - - if (class_exists(InMemoryUser::class) && InMemoryUser::class === $class) { - return true; - } - - return class_exists(User::class) && User::class === $class; - } -} diff --git a/Service/RefreshToken.php b/Service/RefreshToken.php deleted file mode 100644 index b6b5d974..00000000 --- a/Service/RefreshToken.php +++ /dev/null @@ -1,122 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Gesdinet\JWTRefreshTokenBundle\Service; - -use Gesdinet\JWTRefreshTokenBundle\Event\RefreshEvent; -use Gesdinet\JWTRefreshTokenBundle\Security\Authenticator\RefreshTokenAuthenticator; -use Gesdinet\JWTRefreshTokenBundle\Exception\InvalidRefreshTokenException; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; -use InvalidArgumentException; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Security\Core\Exception\AuthenticationException; -use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManagerInterface; -use Gesdinet\JWTRefreshTokenBundle\Security\Provider\RefreshTokenProvider; -use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; -use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; - -trigger_deprecation('gesdinet/jwt-refresh-token-bundle', '1.0', 'The "%s" class is deprecated, use the `refresh_jwt` authenticator instead.', RefreshToken::class); - -/** - * @deprecated use the `refresh_jwt` authenticator instead - */ -class RefreshToken -{ - private RefreshTokenAuthenticator $authenticator; - - private RefreshTokenProvider $provider; - - private AuthenticationSuccessHandlerInterface $successHandler; - - private AuthenticationFailureHandlerInterface $failureHandler; - - private RefreshTokenManagerInterface $refreshTokenManager; - - private int $ttl; - - private string $providerKey; - - private bool $ttlUpdate; - - private EventDispatcherInterface $eventDispatcher; - - /** - * @param int $ttl - * @param string $providerKey - * @param bool $ttlUpdate - */ - public function __construct( - RefreshTokenAuthenticator $authenticator, - RefreshTokenProvider $provider, - AuthenticationSuccessHandlerInterface $successHandler, - AuthenticationFailureHandlerInterface $failureHandler, - RefreshTokenManagerInterface $refreshTokenManager, - $ttl, - $providerKey, - $ttlUpdate, - EventDispatcherInterface $eventDispatcher - ) { - $this->authenticator = $authenticator; - $this->provider = $provider; - $this->successHandler = $successHandler; - $this->failureHandler = $failureHandler; - $this->refreshTokenManager = $refreshTokenManager; - $this->ttl = $ttl; - $this->providerKey = $providerKey; - $this->ttlUpdate = $ttlUpdate; - $this->eventDispatcher = $eventDispatcher; - } - - /** - * @return Response - * - * @throws InvalidArgumentException - * @throws AuthenticationException - */ - public function refresh(Request $request) - { - $credentials = $this->authenticator->getCredentials($request); - - try { - $user = $this->authenticator->getUser($credentials, $this->provider); - - $postAuthenticationToken = $this->authenticator->createAuthenticatedToken($user, $this->providerKey); - } catch (AuthenticationException $e) { - return $this->failureHandler->onAuthenticationFailure($request, $e); - } - - $refreshToken = $this->refreshTokenManager->get($credentials['token']); - - if (null === $refreshToken || !$refreshToken->isValid()) { - return $this->failureHandler->onAuthenticationFailure( - $request, - new InvalidRefreshTokenException( - sprintf('Refresh token "%s" is invalid.', (string) $refreshToken) - ) - ); - } - - if ($this->ttlUpdate) { - $expirationDate = new \DateTime(); - $expirationDate->modify(sprintf('+%d seconds', $this->ttl)); - $refreshToken->setValid($expirationDate); - - $this->refreshTokenManager->save($refreshToken); - } - - $event = new RefreshEvent($refreshToken, $postAuthenticationToken); - - $this->eventDispatcher->dispatch($event, 'gesdinet.refresh_token'); - - return $this->successHandler->onAuthenticationSuccess($request, $postAuthenticationToken); - } -} diff --git a/Tests/Functional/DependencyInjection/Compiler/AddExtractorsToChainCompilerPassTest.php b/Tests/Functional/DependencyInjection/Compiler/AddExtractorsToChainCompilerPassTest.php index 98a82404..cb141300 100644 --- a/Tests/Functional/DependencyInjection/Compiler/AddExtractorsToChainCompilerPassTest.php +++ b/Tests/Functional/DependencyInjection/Compiler/AddExtractorsToChainCompilerPassTest.php @@ -13,7 +13,7 @@ final class AddExtractorsToChainCompilerPassTest extends AbstractCompilerPassTes { public function test_extractors_are_added_to_the_chain(): void { - $this->registerService('gesdinet.jwtrefreshtoken.request.extractor.chain', ChainExtractor::class); + $this->registerService('gesdinet_jwt_refresh_token.request.extractor.chain', ChainExtractor::class); $this->registerService('test.extractor', ExtractorInterface::class) ->addTag('gesdinet_jwt_refresh_token.request_extractor'); @@ -21,7 +21,7 @@ public function test_extractors_are_added_to_the_chain(): void $this->assertContainerBuilderHasService('test.extractor', ExtractorInterface::class); $this->assertContainerBuilderHasServiceDefinitionWithMethodCall( - 'gesdinet.jwtrefreshtoken.request.extractor.chain', + 'gesdinet_jwt_refresh_token.request.extractor.chain', 'addExtractor', [new Reference('test.extractor')] ); diff --git a/Tests/Functional/DependencyInjection/ConfigurationTest.php b/Tests/Functional/DependencyInjection/ConfigurationTest.php index 621f2f41..7b72ffe3 100644 --- a/Tests/Functional/DependencyInjection/ConfigurationTest.php +++ b/Tests/Functional/DependencyInjection/ConfigurationTest.php @@ -19,7 +19,11 @@ protected function getConfiguration(): ConfigurationInterface public function test_default_configuration_is_valid(): void { - $this->assertConfigurationIsValid([]); + $this->assertConfigurationIsValid([ + [ + 'refresh_token_class' => RefreshToken::class, + ], + ]); } public function test_custom_configuration_is_valid(): void @@ -28,16 +32,11 @@ public function test_custom_configuration_is_valid(): void [ 'ttl' => 123, 'ttl_update' => true, - 'firewall' => 'main', - 'user_provider' => 'my.user_provider', - 'user_identity_field' => 'email', 'manager_type' => 'mongodb', 'refresh_token_class' => RefreshToken::class, 'object_manager' => 'doctrine_mongodb.odm.document_manager', - 'user_checker' => 'my.user_checker', 'single_use' => true, 'token_parameter_name' => 'the_token', - 'doctrine_mappings' => false, 'cookie' => [ 'enabled' => true, 'same_site' => 'strict', @@ -50,4 +49,13 @@ public function test_custom_configuration_is_valid(): void ], ]); } + + public function test_configuration_is_invalid_when_refresh_token_class_does_not_implement_the_required_interface(): void + { + $this->assertConfigurationIsInvalid([ + [ + 'refresh_token_class' => Configuration::class, + ], + ]); + } } diff --git a/Tests/Functional/DependencyInjection/GesdinetJWTRefreshTokenExtensionTest.php b/Tests/Functional/DependencyInjection/GesdinetJWTRefreshTokenExtensionTest.php index c804ffb8..ca9ba173 100644 --- a/Tests/Functional/DependencyInjection/GesdinetJWTRefreshTokenExtensionTest.php +++ b/Tests/Functional/DependencyInjection/GesdinetJWTRefreshTokenExtensionTest.php @@ -18,16 +18,15 @@ protected function getContainerExtensions(): array public function test_container_is_loaded_with_default_configuration(): void { - $this->load(); + $this->load([ + 'refresh_token_class' => RefreshTokenEntity::class, + 'object_manager' => 'doctrine.orm.entity_manager', + ]); $this->assertContainerBuilderHasParameter('gesdinet_jwt_refresh_token.ttl', 2592000); $this->assertContainerBuilderHasParameter('gesdinet_jwt_refresh_token.ttl_update', false); - $this->assertContainerBuilderHasParameter('gesdinet_jwt_refresh_token.security.firewall', 'api'); - $this->assertContainerBuilderHasParameter('gesdinet_jwt_refresh_token.user_provider', null); - $this->assertContainerBuilderHasParameter('gesdinet_jwt_refresh_token.user_identity_field', 'username'); $this->assertContainerBuilderHasParameter('gesdinet_jwt_refresh_token.single_use', false); $this->assertContainerBuilderHasParameter('gesdinet_jwt_refresh_token.token_parameter_name', 'refresh_token'); - $this->assertContainerBuilderHasParameter('gesdinet_jwt_refresh_token.doctrine_mappings', true); $this->assertContainerBuilderHasParameter( 'gesdinet_jwt_refresh_token.cookie', [ @@ -42,9 +41,8 @@ public function test_container_is_loaded_with_default_configuration(): void ], ); - $this->assertContainerBuilderHasParameter('gesdinet.jwtrefreshtoken.refresh_token.class', RefreshTokenEntity::class); - $this->assertContainerBuilderHasParameter('gesdinet.jwtrefreshtoken.object_manager.id', 'doctrine.orm.entity_manager'); - $this->assertContainerBuilderHasParameter('gesdinet.jwtrefreshtoken.user_checker.id', 'security.user_checker'); + $this->assertContainerBuilderHasParameter('gesdinet_jwt_refresh_token.refresh_token.class', RefreshTokenEntity::class); + $this->assertContainerBuilderHasAlias('gesdinet_jwt_refresh_token.object_manager', 'doctrine.orm.entity_manager'); } public function test_container_is_loaded_with_custom_configuration(): void @@ -52,16 +50,11 @@ public function test_container_is_loaded_with_custom_configuration(): void $this->load([ 'ttl' => 123, 'ttl_update' => true, - 'firewall' => 'main', - 'user_provider' => 'my.user_provider', - 'user_identity_field' => 'email', 'manager_type' => 'mongodb', 'refresh_token_class' => RefreshTokenDocument::class, 'object_manager' => 'doctrine_mongodb.odm.document_manager', - 'user_checker' => 'my.user_checker', 'single_use' => true, 'token_parameter_name' => 'the_token', - 'doctrine_mappings' => false, 'cookie' => [ 'enabled' => true, 'same_site' => 'strict', @@ -75,12 +68,8 @@ public function test_container_is_loaded_with_custom_configuration(): void $this->assertContainerBuilderHasParameter('gesdinet_jwt_refresh_token.ttl', 123); $this->assertContainerBuilderHasParameter('gesdinet_jwt_refresh_token.ttl_update', true); - $this->assertContainerBuilderHasParameter('gesdinet_jwt_refresh_token.security.firewall', 'main'); - $this->assertContainerBuilderHasParameter('gesdinet_jwt_refresh_token.user_provider', 'my.user_provider'); - $this->assertContainerBuilderHasParameter('gesdinet_jwt_refresh_token.user_identity_field', 'email'); $this->assertContainerBuilderHasParameter('gesdinet_jwt_refresh_token.single_use', true); $this->assertContainerBuilderHasParameter('gesdinet_jwt_refresh_token.token_parameter_name', 'the_token'); - $this->assertContainerBuilderHasParameter('gesdinet_jwt_refresh_token.doctrine_mappings', false); $this->assertContainerBuilderHasParameter( 'gesdinet_jwt_refresh_token.cookie', [ @@ -95,20 +84,7 @@ public function test_container_is_loaded_with_custom_configuration(): void ], ); - $this->assertContainerBuilderHasParameter('gesdinet.jwtrefreshtoken.refresh_token.class', RefreshTokenDocument::class); - $this->assertContainerBuilderHasParameter('gesdinet.jwtrefreshtoken.object_manager.id', 'doctrine_mongodb.odm.document_manager'); - $this->assertContainerBuilderHasParameter('gesdinet.jwtrefreshtoken.user_checker.id', 'my.user_checker'); - } - - public function test_container_is_loaded_with_deprecated_parameters(): void - { - $this->load([ - 'manager_type' => 'mongodb', - 'refresh_token_entity' => RefreshTokenDocument::class, - 'entity_manager' => 'doctrine_mongodb.odm.document_manager', - ]); - - $this->assertContainerBuilderHasParameter('gesdinet.jwtrefreshtoken.refresh_token.class', RefreshTokenDocument::class); - $this->assertContainerBuilderHasParameter('gesdinet.jwtrefreshtoken.object_manager.id', 'doctrine_mongodb.odm.document_manager'); + $this->assertContainerBuilderHasParameter('gesdinet_jwt_refresh_token.refresh_token.class', RefreshTokenDocument::class); + $this->assertContainerBuilderHasAlias('gesdinet_jwt_refresh_token.object_manager', 'doctrine_mongodb.odm.document_manager'); } } diff --git a/Tests/Functional/DependencyInjection/Security/Factory/RefreshTokenAuthenticatorFactoryTest.php b/Tests/Functional/DependencyInjection/Security/Factory/RefreshTokenAuthenticatorFactoryTest.php index 3051f8bf..a17d5459 100644 --- a/Tests/Functional/DependencyInjection/Security/Factory/RefreshTokenAuthenticatorFactoryTest.php +++ b/Tests/Functional/DependencyInjection/Security/Factory/RefreshTokenAuthenticatorFactoryTest.php @@ -7,7 +7,6 @@ use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface; final class RefreshTokenAuthenticatorFactoryTest extends TestCase { @@ -21,13 +20,6 @@ final class RefreshTokenAuthenticatorFactoryTest extends TestCase */ private $container; - public static function setUpBeforeClass(): void - { - if (!interface_exists(RememberMeHandlerInterface::class)) { - self::markTestSkipped('Only applies to Symfony 5.3+'); - } - } - protected function setUp(): void { $this->factory = new RefreshTokenAuthenticatorFactory(); @@ -39,7 +31,10 @@ public function test_authenticator_service_is_created_with_default_configuration $this->factory->createAuthenticator( $this->container, 'test', - [], + [ + 'check_path' => '/login_check', + 'invalidate_token_on_logout' => true, + ], 'app.user_provider' ); @@ -49,11 +44,11 @@ public function test_authenticator_service_is_created_with_default_configuration /** @var ChildDefinition $successHandler */ $successHandler = $this->container->getDefinition('security.authentication.success_handler.test.refresh_jwt'); - $this->assertSame('gesdinet.jwtrefreshtoken.security.authentication.success_handler', $successHandler->getParent()); + $this->assertSame('gesdinet_jwt_refresh_token.security.authentication.success_handler', $successHandler->getParent()); /** @var ChildDefinition $failureHandler */ $failureHandler = $this->container->getDefinition('security.authentication.failure_handler.test.refresh_jwt'); - $this->assertSame('gesdinet.jwtrefreshtoken.security.authentication.failure_handler', $failureHandler->getParent()); + $this->assertSame('gesdinet_jwt_refresh_token.security.authentication.failure_handler', $failureHandler->getParent()); } public function test_authenticator_service_is_created_with_custom_handlers(): void @@ -62,6 +57,8 @@ public function test_authenticator_service_is_created_with_custom_handlers(): vo $this->container, 'test', [ + 'check_path' => '/login_check', + 'invalidate_token_on_logout' => true, 'success_handler' => 'app.security.authentication.success_handler', 'failure_handler' => 'app.security.authentication.failure_handler', ], diff --git a/Tests/Functional/Fixtures/Document/RefreshToken.php b/Tests/Functional/Fixtures/Document/RefreshToken.php index 955f14c7..dbec05fb 100644 --- a/Tests/Functional/Fixtures/Document/RefreshToken.php +++ b/Tests/Functional/Fixtures/Document/RefreshToken.php @@ -5,9 +5,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Gesdinet\JWTRefreshTokenBundle\Document\RefreshToken as BaseRefreshToken; -/** - * @ODM\Document - */ +#[ODM\Document] class RefreshToken extends BaseRefreshToken { } diff --git a/Tests/Functional/Fixtures/Document/User.php b/Tests/Functional/Fixtures/Document/User.php index 5f2661a0..b3094bc4 100644 --- a/Tests/Functional/Fixtures/Document/User.php +++ b/Tests/Functional/Fixtures/Document/User.php @@ -5,24 +5,16 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Symfony\Component\Security\Core\User\UserInterface; -/** - * @ODM\Document - */ +#[ODM\Document] class User implements UserInterface { - /** - * @ODM\Id - */ + #[ODM\Id] private ?string $id = null; - /** - * @ODM\Field - */ + #[ODM\Field] private string $email; - /** - * @ODM\Field(nullable=true) - */ + #[ODM\Field(nullable: true)] private ?string $password; public function __construct(string $email, ?string $password = null) diff --git a/Tests/Functional/Fixtures/Entity/RefreshToken.php b/Tests/Functional/Fixtures/Entity/RefreshToken.php index 3f7793e8..c911a8c5 100644 --- a/Tests/Functional/Fixtures/Entity/RefreshToken.php +++ b/Tests/Functional/Fixtures/Entity/RefreshToken.php @@ -5,9 +5,7 @@ use Doctrine\ORM\Mapping as ORM; use Gesdinet\JWTRefreshTokenBundle\Entity\RefreshToken as BaseRefreshToken; -/** - * @ORM\Entity() - */ +#[ORM\Entity] class RefreshToken extends BaseRefreshToken { } diff --git a/Tests/Functional/Fixtures/Entity/User.php b/Tests/Functional/Fixtures/Entity/User.php index 02b17ae7..4ebee6a7 100644 --- a/Tests/Functional/Fixtures/Entity/User.php +++ b/Tests/Functional/Fixtures/Entity/User.php @@ -5,30 +5,18 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\UserInterface; -/** - * @ORM\Entity() - * - * @ORM\Table() - */ +#[ORM\Entity] class User implements UserInterface { - /** - * @ORM\Column(type="integer") - * - * @ORM\Id() - * - * @ORM\GeneratedValue(strategy="AUTO") - */ + #[ORM\Id] + #[ORM\Column] + #[ORM\GeneratedValue] private ?int $id = null; - /** - * @ORM\Column(type="string") - */ + #[ORM\Column] private string $email; - /** - * @ORM\Column(type="string", nullable=true) - */ + #[ORM\Column(nullable: true)] private ?string $password; public function __construct(string $email, ?string $password = null) diff --git a/Tests/Functional/ODMTestCase.php b/Tests/Functional/ODMTestCase.php index b23a513a..8555dba0 100644 --- a/Tests/Functional/ODMTestCase.php +++ b/Tests/Functional/ODMTestCase.php @@ -2,9 +2,9 @@ namespace Gesdinet\JWTRefreshTokenBundle\Tests\Functional; -use Doctrine\Common\Cache\Psr6\DoctrineProvider; use Doctrine\ODM\MongoDB\Configuration; use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\Mapping\Driver\AttributeDriver; use Doctrine\ODM\MongoDB\Mapping\Driver\SimplifiedXmlDriver; use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; use MongoDB\Client; @@ -22,13 +22,7 @@ abstract class ODMTestCase extends TestCase protected function setUp(): void { $config = new Configuration(); - - if (method_exists($config, 'setMetadataCache')) { - $config->setMetadataCache(new ArrayAdapter()); - } else { - $config->setMetadataCacheImpl(DoctrineProvider::wrap(new ArrayAdapter())); - } - + $config->setMetadataCache(new ArrayAdapter()); $config->setProxyDir(sys_get_temp_dir().'/JWTRefreshTokenBundle/_files/Proxies'); $config->setProxyNamespace(__NAMESPACE__.'\Proxies'); $config->setHydratorDir(sys_get_temp_dir().'/JWTRefreshTokenBundle/_files/Hydrators'); @@ -39,17 +33,14 @@ protected function setUp(): void $driverChain = new MappingDriverChain(); - $annotationDriver = $config->newDefaultAnnotationDriver([__DIR__.'/Fixtures/Document']); + $attributeDriver = new AttributeDriver([__DIR__.'/Fixtures/Document']); $xmlDriver = new SimplifiedXmlDriver( [(\dirname(__DIR__, 2).'/Resources/config/doctrine') => 'Gesdinet\\JWTRefreshTokenBundle\\Document'], '.mongodb.xml' ); - $driverChain->addDriver( - $annotationDriver, - 'Gesdinet\\JWTRefreshTokenBundle\\Tests\\Functional\\Fixtures\\Document' - ); + $driverChain->addDriver($attributeDriver, 'Gesdinet\\JWTRefreshTokenBundle\\Tests\\Functional\\Fixtures\\Document'); $driverChain->addDriver($xmlDriver, 'Gesdinet\\JWTRefreshTokenBundle\\Document'); $config->setMetadataDriverImpl($driverChain); diff --git a/Tests/Functional/ORMTestCase.php b/Tests/Functional/ORMTestCase.php index e7ce78cc..ced286b8 100644 --- a/Tests/Functional/ORMTestCase.php +++ b/Tests/Functional/ORMTestCase.php @@ -2,9 +2,9 @@ namespace Gesdinet\JWTRefreshTokenBundle\Tests\Functional; -use Doctrine\Common\Cache\Psr6\DoctrineProvider; use Doctrine\ORM\Configuration; use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; use Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver; use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; use PHPUnit\Framework\TestCase; @@ -20,35 +20,19 @@ abstract class ORMTestCase extends TestCase protected function setUp(): void { $config = new Configuration(); - - if (method_exists($config, 'setMetadataCache')) { - $config->setMetadataCache(new ArrayAdapter()); - } else { - $config->setMetadataCacheImpl(DoctrineProvider::wrap(new ArrayAdapter())); - } - - if (method_exists($config, 'setQueryCache')) { - $config->setQueryCache(new ArrayAdapter()); - } else { - $config->setQueryCacheImpl(DoctrineProvider::wrap(new ArrayAdapter())); - } - - if (method_exists($config, 'setResultCache')) { - $config->setResultCache(new ArrayAdapter()); - } else { - $config->setResultCacheImpl(DoctrineProvider::wrap(new ArrayAdapter())); - } - + $config->setMetadataCache(new ArrayAdapter()); + $config->setQueryCache(new ArrayAdapter()); + $config->setResultCache(new ArrayAdapter()); $config->setProxyDir(sys_get_temp_dir().'/JWTRefreshTokenBundle/_files'); $config->setProxyNamespace(__NAMESPACE__.'\Proxies'); $driverChain = new MappingDriverChain(); - $annotationDriver = $config->newDefaultAnnotationDriver([__DIR__.'/Fixtures/Entity'], false); + $attributeDriver = new AttributeDriver([__DIR__.'/Fixtures/Entity']); $xmlDriver = new SimplifiedXmlDriver([(\dirname(__DIR__, 2).'/Resources/config/doctrine') => 'Gesdinet\\JWTRefreshTokenBundle\\Entity']); - $driverChain->addDriver($annotationDriver, 'Gesdinet\\JWTRefreshTokenBundle\\Tests\\Functional\\Fixtures\\Entity'); + $driverChain->addDriver($attributeDriver, 'Gesdinet\\JWTRefreshTokenBundle\\Tests\\Functional\\Fixtures\\Entity'); $driverChain->addDriver($xmlDriver, 'Gesdinet\\JWTRefreshTokenBundle\\Entity'); $config->setMetadataDriverImpl($driverChain); diff --git a/Tests/Services/UserCreator.php b/Tests/Services/UserCreator.php index afd5822b..0da71619 100644 --- a/Tests/Services/UserCreator.php +++ b/Tests/Services/UserCreator.php @@ -3,21 +3,12 @@ namespace Gesdinet\JWTRefreshTokenBundle\Tests\Services; use Symfony\Component\Security\Core\User\InMemoryUser; -use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserInterface; class UserCreator { public static function create(string $identifier = 'username'): UserInterface { - $password = 'password'; - - if (class_exists(InMemoryUser::class)) { - $user = new InMemoryUser($identifier, $password); - } else { - $user = new User($identifier, $password); - } - - return $user; + return new InMemoryUser($identifier, 'password'); } } diff --git a/Tests/Unit/AbstractRefreshTokenTest.php b/Tests/Unit/AbstractRefreshTokenTest.php index aaf8076b..ac78c2ba 100644 --- a/Tests/Unit/AbstractRefreshTokenTest.php +++ b/Tests/Unit/AbstractRefreshTokenTest.php @@ -38,12 +38,6 @@ public function testHasACustomRefreshToken() $this->assertSame('custom-token', $this->refreshToken->getRefreshToken()); } - public function testGeneratesARefreshToken() - { - $this->assertSame($this->refreshToken, $this->refreshToken->setRefreshToken()); - $this->assertIsString($this->refreshToken->getRefreshToken()); - } - public function testHasUsername() { $this->assertSame('username', $this->refreshToken->getUsername()); diff --git a/Tests/Unit/Doctrine/RefreshTokenManagerTest.php b/Tests/Unit/Doctrine/RefreshTokenManagerTest.php index 6012c2d0..87df9ac5 100644 --- a/Tests/Unit/Doctrine/RefreshTokenManagerTest.php +++ b/Tests/Unit/Doctrine/RefreshTokenManagerTest.php @@ -8,7 +8,6 @@ use Gesdinet\JWTRefreshTokenBundle\Entity\RefreshToken; use Gesdinet\JWTRefreshTokenBundle\Entity\RefreshTokenRepository; use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenInterface; -use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManagerInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -57,16 +56,6 @@ protected function setUp(): void ); } - public function testIsARefreshTokenManager() - { - $this->assertInstanceOf(RefreshTokenManagerInterface::class, $this->refreshTokenManager); - } - - public function testCreatesAToken() - { - $this->assertInstanceOf(static::REFRESH_TOKEN_ENTITY_CLASS, $this->refreshTokenManager->create()); - } - public function testRetrievesATokenFromStorage() { $token = 'token'; diff --git a/Tests/Unit/EventListener/AttachRefreshTokenOnSuccessListenerTest.php b/Tests/Unit/EventListener/AttachRefreshTokenOnSuccessListenerTest.php index 7b8e898d..9aba2cae 100644 --- a/Tests/Unit/EventListener/AttachRefreshTokenOnSuccessListenerTest.php +++ b/Tests/Unit/EventListener/AttachRefreshTokenOnSuccessListenerTest.php @@ -317,7 +317,6 @@ private function setSingleUseOnEventListener(bool $singleUse): void { $reflector = new \ReflectionClass(AttachRefreshTokenOnSuccessListener::class); $property = $reflector->getProperty('singleUse'); - $property->setAccessible(true); $property->setValue($this->attachRefreshTokenOnSuccessListener, $singleUse); } } diff --git a/Tests/Unit/Request/Extractor/ChainExtractorTest.php b/Tests/Unit/Request/Extractor/ChainExtractorTest.php index ba2df6fe..6f16725e 100644 --- a/Tests/Unit/Request/Extractor/ChainExtractorTest.php +++ b/Tests/Unit/Request/Extractor/ChainExtractorTest.php @@ -19,11 +19,6 @@ protected function setUp(): void $this->chainExtractor = new ChainExtractor(); } - public function testIsAnExtractor(): void - { - $this->assertInstanceOf(ExtractorInterface::class, $this->chainExtractor); - } - public function testGetsTheTokenFromTheFirstExtractorInTheChain(): void { /** @var ExtractorInterface|MockObject $firstExtractor */ diff --git a/Tests/Unit/Request/Extractor/RequestBodyExtractorTest.php b/Tests/Unit/Request/Extractor/RequestBodyExtractorTest.php index e2d2e3f1..36242e92 100644 --- a/Tests/Unit/Request/Extractor/RequestBodyExtractorTest.php +++ b/Tests/Unit/Request/Extractor/RequestBodyExtractorTest.php @@ -2,7 +2,6 @@ namespace Gesdinet\JWTRefreshTokenBundle\Tests\Unit\Request\Extractor; -use Gesdinet\JWTRefreshTokenBundle\Request\Extractor\ExtractorInterface; use Gesdinet\JWTRefreshTokenBundle\Request\Extractor\RequestBodyExtractor; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -15,15 +14,9 @@ class RequestBodyExtractorTest extends TestCase protected function setUp(): void { - parent::setUp(); $this->requestBodyExtractor = new RequestBodyExtractor(); } - public function testIsAnExtractor(): void - { - $this->assertInstanceOf(ExtractorInterface::class, $this->requestBodyExtractor); - } - public function testGetsTheTokenFromTheRequestBody(): void { $token = 'my-refresh-token'; diff --git a/Tests/Unit/Request/Extractor/RequestCookieExtractorTest.php b/Tests/Unit/Request/Extractor/RequestCookieExtractorTest.php index d47695e3..b70225ec 100644 --- a/Tests/Unit/Request/Extractor/RequestCookieExtractorTest.php +++ b/Tests/Unit/Request/Extractor/RequestCookieExtractorTest.php @@ -2,7 +2,6 @@ namespace Gesdinet\JWTRefreshTokenBundle\Tests\Unit\Request\Extractor; -use Gesdinet\JWTRefreshTokenBundle\Request\Extractor\ExtractorInterface; use Gesdinet\JWTRefreshTokenBundle\Request\Extractor\RequestCookieExtractor; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -17,15 +16,9 @@ class RequestCookieExtractorTest extends TestCase protected function setUp(): void { - parent::setUp(); $this->requestCookieExtractor = new RequestCookieExtractor(); } - public function testIsAnExtractor(): void - { - $this->assertInstanceOf(ExtractorInterface::class, $this->requestCookieExtractor); - } - public function testGetsTheTokenFromTheRequestCookies(): void { $token = 'my-refresh-token'; diff --git a/Tests/Unit/Request/Extractor/RequestParameterExtractorTest.php b/Tests/Unit/Request/Extractor/RequestParameterExtractorTest.php index 0290128c..1f2ee15d 100644 --- a/Tests/Unit/Request/Extractor/RequestParameterExtractorTest.php +++ b/Tests/Unit/Request/Extractor/RequestParameterExtractorTest.php @@ -2,7 +2,6 @@ namespace Gesdinet\JWTRefreshTokenBundle\Tests\Unit\Request\Extractor; -use Gesdinet\JWTRefreshTokenBundle\Request\Extractor\ExtractorInterface; use Gesdinet\JWTRefreshTokenBundle\Request\Extractor\RequestParameterExtractor; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -16,15 +15,9 @@ class RequestParameterExtractorTest extends TestCase protected function setUp(): void { - parent::setUp(); $this->requestParameterExtractor = new RequestParameterExtractor(); } - public function testIsAnExtractor(): void - { - $this->assertInstanceOf(ExtractorInterface::class, $this->requestParameterExtractor); - } - public function testGetsTheTokenFromTheRequestParameters(): void { /** @var Request|MockObject $request */ diff --git a/Tests/Unit/Security/Authenticator/RefreshTokenAuthenticatorTest.php b/Tests/Unit/Security/Authenticator/RefreshTokenAuthenticatorTest.php deleted file mode 100644 index 8fe53886..00000000 --- a/Tests/Unit/Security/Authenticator/RefreshTokenAuthenticatorTest.php +++ /dev/null @@ -1,143 +0,0 @@ -createMock(UserCheckerInterface::class); - - $this->extractor = $this->createMock(ExtractorInterface::class); - - $this->refreshTokenAuthenticator = new RefreshTokenAuthenticator( - $userChecker, - self::PARAMETER_NAME, - $this->extractor - ); - } - - public function testIsAGuardAuthenticator(): void - { - $this->assertInstanceOf(AbstractGuardAuthenticator::class, $this->refreshTokenAuthenticator); - } - - public function testIsAnAuthenticationEntryPoint(): void - { - $this->assertInstanceOf(AuthenticationEntryPointInterface::class, $this->refreshTokenAuthenticator); - } - - public function testReportsTheRequestAsSupportedWhenATokenIsPresent(): void - { - /** @var Request|MockObject $request */ - $request = $this->createMock(Request::class); - $this->createExtractorGetRefreshTokenExpectation($request, 'my-refresh-token'); - - $this->assertTrue($this->refreshTokenAuthenticator->supports($request)); - } - - public function testReportsTheRequestAsNotSupportedWhenATokenIsNotPresent(): void - { - /** @var Request|MockObject $request */ - $request = $this->createMock(Request::class); - $this->createExtractorGetRefreshTokenExpectation($request, null); - - $this->assertFalse($this->refreshTokenAuthenticator->supports($request)); - } - - public function testFetchesTheCredentialsFromTheRequest(): void - { - /** @var Request|MockObject $request */ - $request = $this->createMock(Request::class); - $token = 'my-refresh-token'; - $this->createExtractorGetRefreshTokenExpectation($request, $token); - - $this->assertSame(['token' => $token], $this->refreshTokenAuthenticator->getCredentials($request)); - } - - public function testChecksForValidCredentials(): void - { - /** @var UserInterface|MockObject $user */ - $user = $this->createMock(UserInterface::class); - $this->assertTrue($this->refreshTokenAuthenticator->checkCredentials([], $user)); - } - - public function testHandlesSuccessfulAuthentication(): void - { - /** @var Request|MockObject $request */ - $request = $this->createMock(Request::class); - /** @var TokenInterface|MockObject $token */ - $token = $this->createMock(TokenInterface::class); - - $this->assertNull($this->refreshTokenAuthenticator->onAuthenticationSuccess($request, $token, 'firewall')); - } - - public function testHandlesAnAuthenticationFailure(): void - { - /** @var Request|MockObject $request */ - $request = $this->createMock(Request::class); - - /** @var AuthenticationException|MockObject $exception */ - $exception = $this->createMock(AuthenticationException::class); - - $this->assertInstanceOf( - Response::class, - $this->refreshTokenAuthenticator->onAuthenticationFailure($request, $exception) - ); - } - - public function testStartsAnAuthenticationRequest(): void - { - /** @var Request|MockObject $request */ - $request = $this->createMock(Request::class); - - /** @var AuthenticationException|MockObject $exception */ - $exception = $this->createMock(AuthenticationException::class); - - $this->assertInstanceOf( - Response::class, - $this->refreshTokenAuthenticator->start($request, $exception) - ); - } - - public function testDoesNotSupportRememberMeAuthentication(): void - { - $this->assertFalse($this->refreshTokenAuthenticator->supportsRememberMe()); - } - - private function createExtractorGetRefreshTokenExpectation(Request $request, ?string $token): void - { - $this->extractor - ->expects($this->once()) - ->method('getRefreshToken') - ->with($request) - ->willReturn($token); - } -} diff --git a/Tests/Unit/Security/Http/Authenticator/RefreshTokenAuthenticatorTest.php b/Tests/Unit/Security/Http/Authenticator/RefreshTokenAuthenticatorTest.php index 2939f370..5526cf0f 100644 --- a/Tests/Unit/Security/Http/Authenticator/RefreshTokenAuthenticatorTest.php +++ b/Tests/Unit/Security/Http/Authenticator/RefreshTokenAuthenticatorTest.php @@ -22,12 +22,10 @@ use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; -use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; -use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; use Symfony\Component\Security\Http\HttpUtils; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; @@ -62,10 +60,6 @@ class RefreshTokenAuthenticatorTest extends TestCase protected function setUp(): void { - if (!interface_exists(AuthenticatorInterface::class)) { - self::markTestSkipped('Test requires Symfony 5.4 and later.'); - } - $this->refreshTokenManager = $this->createMock(RefreshTokenManagerInterface::class); $this->extractor = $this->createMock(ExtractorInterface::class); $this->successHandler = $this->createMock(AuthenticationSuccessHandlerInterface::class); @@ -84,16 +78,6 @@ protected function setUp(): void ); } - public function testAnAuthenticator(): void - { - $this->assertInstanceOf(AuthenticatorInterface::class, $this->refreshTokenAuthenticator); - } - - public function testAnAuthenticationEntryPoint(): void - { - $this->assertInstanceOf(AuthenticationEntryPointInterface::class, $this->refreshTokenAuthenticator); - } - public function testReportsTheRequestAsSupportedWhenConfiguredToCheckThePathForPostRequestsOnly(): void { $this->appendOptionsOnRefreshTokenAuthenticator([ @@ -128,30 +112,6 @@ public function testReportsTheRequestAsNotSupportedWhenTheRequestPathDoesNotMatc $this->assertFalse($this->refreshTokenAuthenticator->supports($request)); } - /** - * @group legacy - */ - public function testReportsTheRequestAsSupportedWhenATokenIsPresent(): void - { - /** @var Request|MockObject $request */ - $request = $this->createMock(Request::class); - $token = 'my-refresh-token'; - $this->createExtractorGetRefreshTokenExpectation($request, $token); - - $this->assertTrue($this->refreshTokenAuthenticator->supports($request)); - } - - /** - * @group legacy - */ - public function testReportsTheRequestAsNotSupportedWhenATokenIsNotPresent(): void - { - /** @var Request|MockObject $request */ - $request = $this->createMock(Request::class); - $this->createExtractorGetRefreshTokenExpectation($request, null); - $this->assertFalse($this->refreshTokenAuthenticator->supports($request)); - } - public function testAuthenticatesTheRequestWhenTtlUpdateIsDisabled(): void { /** @var Request|MockObject $request */ @@ -197,7 +157,7 @@ public function testAuthenticatesTheRequestWhenTtlUpdateIsEnabled(): void ->method('save') ->with($this->equalTo($refreshToken)); - $this->assertInstanceOf(Passport::class, $this->refreshTokenAuthenticator->authenticate($request)); + $this->assertInstanceOf(interface_exists(PassportInterface::class) ? PassportInterface::class : Passport::class, $this->refreshTokenAuthenticator->authenticate($request)); } public function testDoesNotAuthenticateTheRequestWhenTheTokenIsNotValid(): void @@ -281,10 +241,6 @@ public function testDoesNotCreateTheAuthenticatedTokenWhenThePassportDoesNotImpl public function testCreatesTheToken(): void { - if (!class_exists(Passport::class)) { - $this->markTestSkipped('Test only applies to Symfony 5.4 and later.'); - } - $username = 'username'; $user = UserCreator::create($username); $passport = $this->createUserPassport($username, $user); @@ -296,10 +252,6 @@ public function testCreatesTheToken(): void public function testDoesNotCreateTheTokenWhenThePassportDoesNotHaveTheRefreshToken(): void { - if (!class_exists(Passport::class)) { - $this->markTestSkipped('Test only applies to Symfony 5.4 and later.'); - } - $username = 'username'; $user = UserCreator::create($username); $passport = $this->createUserPassport($username, $user); diff --git a/Tests/Unit/Security/Provider/RefreshTokenProviderTest.php b/Tests/Unit/Security/Provider/RefreshTokenProviderTest.php deleted file mode 100644 index b285f9ec..00000000 --- a/Tests/Unit/Security/Provider/RefreshTokenProviderTest.php +++ /dev/null @@ -1,151 +0,0 @@ -refreshTokenManager = $this->createMock(RefreshTokenManagerInterface::class); - - $this->refreshTokenProvider = new RefreshTokenProvider($this->refreshTokenManager); - } - - public function testIsAUserProvider(): void - { - $this->assertInstanceOf(UserProviderInterface::class, $this->refreshTokenProvider); - } - - public function testGetsTheUsernameFromATokenWhenTheTokenExistsInStorage(): void - { - /** @var RefreshTokenInterface|MockObject $refreshToken */ - $refreshToken = $this->createMock(RefreshTokenInterface::class); - $token = 'my-refresh-token'; - $username = 'username'; - - $this->createRefreshTokenManagerGetExpectation($token, $refreshToken); - - $refreshToken - ->expects($this->once()) - ->method('getUsername') - ->willReturn($username); - - $this->assertSame($username, $this->refreshTokenProvider->getUsernameForRefreshToken($token)); - } - - public function testReturnsNullWhenTheTokenDoesNotExistInStorage(): void - { - $token = 'my-refresh-token'; - $this->createRefreshTokenManagerGetExpectation($token, null); - - $this->assertNull($this->refreshTokenProvider->getUsernameForRefreshToken($token)); - } - - public function testLoadsAUserByUsername(): void - { - $this->assertInstanceOf( - UserInterface::class, - $this->refreshTokenProvider->loadUserByUsername('testname') - ); - } - - public function testLoadsAUserByUsernameFromACustomUserProvider(): void - { - $userProvider = new InMemoryUserProvider(['testname' => ['password' => 'secure-password']]); - $this->refreshTokenProvider->setCustomUserProvider($userProvider); - - $this->assertInstanceOf( - UserInterface::class, - $this->refreshTokenProvider->loadUserByUsername('testname') - ); - } - - public function testLoadsAUserByIdentifier(): void - { - $this->assertInstanceOf( - UserInterface::class, - $this->refreshTokenProvider->loadUserByIdentifier('testname') - ); - } - - public function testLoadsAUserByIdentifierFromACustomUserProvider(): void - { - $userProvider = new InMemoryUserProvider(['testname' => ['password' => 'secure-password']]); - $this->refreshTokenProvider->setCustomUserProvider($userProvider); - - $this->assertInstanceOf( - UserInterface::class, - $this->refreshTokenProvider->loadUserByIdentifier('testname') - ); - } - - public function testDoesNotSupportRefreshingAUserByDefault(): void - { - /** @var UserInterface|MockObject $user */ - $user = $this->createMock(UserInterface::class); - - $this->expectExceptionObject(new UnsupportedUserException()); - - $this->refreshTokenProvider->refreshUser($user); - } - - public function testRefreshesAUserWhenUsingACustomUserProvider(): void - { - $userProvider = new InMemoryUserProvider(['testname' => ['password' => 'secure-password']]); - - if (method_exists($userProvider, 'loadUserByIdentifier')) { - $user = $userProvider->loadUserByIdentifier('testname'); - } else { - $user = $userProvider->loadUserByUsername('testname'); - } - - $this->refreshTokenProvider->setCustomUserProvider($userProvider); - - $this->assertInstanceOf(UserInterface::class, $this->refreshTokenProvider->refreshUser($user)); - } - - public function testSupportsAUserClass(): void - { - $this->assertTrue($this->refreshTokenProvider->supportsClass( - class_exists(InMemoryUser::class) ? InMemoryUser::class : User::class - )); - } - - public function testSupportsAUserClassWhenUsingACustomProvider(): void - { - $userProvider = new InMemoryUserProvider(['testname' => ['password' => 'secure-password']]); - $this->refreshTokenProvider->setCustomUserProvider($userProvider); - - $this->assertTrue($this->refreshTokenProvider->supportsClass( - class_exists(InMemoryUser::class) ? InMemoryUser::class : User::class - )); - } - - private function createRefreshTokenManagerGetExpectation(string $token, ?RefreshTokenInterface $refreshToken): void - { - $this->refreshTokenManager - ->expects($this->once()) - ->method('get') - ->with($token) - ->willReturn($refreshToken); - } -} diff --git a/Tests/Unit/Service/RefreshTokenTest.php b/Tests/Unit/Service/RefreshTokenTest.php deleted file mode 100644 index ebf8dabd..00000000 --- a/Tests/Unit/Service/RefreshTokenTest.php +++ /dev/null @@ -1,164 +0,0 @@ -authenticator = $this->createMock(RefreshTokenAuthenticator::class); - $this->refreshTokenManager = $this->createMock(RefreshTokenManagerInterface::class); - $this->failureHandler = $this->createMock(AuthenticationFailureHandlerInterface::class); - - $this->refreshToken = new RefreshToken( - $this->authenticator, - $this->createMock(RefreshTokenProvider::class), - $this->createMock(AuthenticationSuccessHandlerInterface::class), - $this->failureHandler, - $this->refreshTokenManager, - 2592000, - 'testkey', - false, - $this->createMock(EventDispatcherInterface::class) - ); - } - - public function testItRefreshesTheToken() - { - $this->createAuthenticatorGetCredentialsExpectation(['token' => '1234']); - $this->createAuthenticatorGetUserExpectation(UserCreator::create('test')); - $this->createAuthenticatorCreateAuthenticatedTokenExpectation( - $this->createMock(PostAuthenticationGuardToken::class) - ); - - $refreshToken = $this->createMock(RefreshTokenInterface::class); - $refreshToken - ->expects($this->once()) - ->method('isValid') - ->willReturn(true); - - $this->refreshTokenManager - ->expects($this->once()) - ->method('get') - ->willReturn($refreshToken); - - $this->refreshToken->refresh($this->createMock(Request::class)); - } - - public function testItRefreshesTokenWithTtlUpdate() - { - $this->setTtlUpdateOnRefreshToken(true); - - $this->createAuthenticatorGetCredentialsExpectation(['token' => '1234']); - $this->createAuthenticatorGetUserExpectation(UserCreator::create('test')); - $this->createAuthenticatorCreateAuthenticatedTokenExpectation( - $this->createMock(PostAuthenticationGuardToken::class) - ); - - $refreshToken = $this->createMock(RefreshTokenInterface::class); - $refreshToken - ->expects($this->once()) - ->method('isValid') - ->willReturn(true); - - $refreshToken - ->expects($this->once()) - ->method('setValid'); - - $this->refreshTokenManager - ->expects($this->once()) - ->method('get') - ->willReturn($refreshToken); - - $this->refreshTokenManager - ->expects($this->once()) - ->method('save') - ->with($refreshToken); - - $this->refreshToken->refresh($this->createMock(Request::class)); - } - - public function testItThrowsAnAuthenticationException() - { - $this->createAuthenticatorGetCredentialsExpectation(['token' => '1234']); - $this->createAuthenticatorGetUserExpectation(UserCreator::create('test')); - $this->createAuthenticatorCreateAuthenticatedTokenExpectation( - $this->createMock(PostAuthenticationGuardToken::class) - ); - - $this->failureHandler - ->expects($this->once()) - ->method('onAuthenticationFailure'); - - $this->refreshToken->refresh($this->createMock(Request::class)); - } - - private function setTtlUpdateOnRefreshToken(bool $ttlUpdate): void - { - $reflector = new \ReflectionClass(RefreshToken::class); - $property = $reflector->getProperty('ttlUpdate'); - $property->setAccessible(true); - $property->setValue($this->refreshToken, $ttlUpdate); - } - - private function createAuthenticatorGetCredentialsExpectation(array $credentials): void - { - $this->authenticator - ->expects($this->atLeastOnce()) - ->method('getCredentials') - ->willReturn($credentials); - } - - private function createAuthenticatorGetUserExpectation(UserInterface $user): void - { - $this->authenticator - ->expects($this->once()) - ->method('getUser') - ->willReturn($user); - } - - private function createAuthenticatorCreateAuthenticatedTokenExpectation( - PostAuthenticationGuardToken $postAuthenticationGuardToken - ): void { - $this->authenticator - ->expects($this->once()) - ->method('createAuthenticatedToken') - ->willReturn($postAuthenticationGuardToken); - } -} diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md new file mode 100644 index 00000000..11da4f6c --- /dev/null +++ b/UPGRADE-2.0.md @@ -0,0 +1,36 @@ +# Upgrade from 1.x to 2.0 + +The below guide will assist in upgrading from the 1.x versions to 2.0. + +## Bundle Requirements + +- Symfony 5.4, 6.4, or 7.0+ +- PHP 8.1 or later + +## General changes + +- The `refresh_token_class` config node is now required and validates that the class implements `Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenInterface` +- The `Gesdinet\JWTRefreshTokenBundle\Security\Http\Authenticator\RefreshTokenAuthenticator::supports()` method now only checks if the request path matches the `check_path` configuration for the authenticator +- Standardized all container IDs to use the `gesdinet_jwt_refresh_token` prefix +- Made several classes final +- Added parameter and return typehints + +## Removed Features + +- Removed classes supporting authentication for Symfony 5.3 and earlier +- Removed the `AbstractRefreshToken` classes from the `Gesdinet\JWTRefreshTokenBundle\Document` and `Gesdinet\JWTRefreshTokenBundle\Entity` namespaces, use the `RefreshToken` class from the same namespace instead +- Removed `Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManagerInterface::create()` and its implementations, a `Gesdinet\JWTRefreshTokenBundle\Generator\RefreshTokenGeneratorInterface` implementation should be used instead +- Removed `Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManager`, implement `Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManagerInterface` directly instead +- Removed automatic token generation from `Gesdinet\JWTRefreshTokenBundle\Model\AbstractRefreshToken::setRefreshToken()`, a token is now required +- Removed `Gesdinet\JWTRefreshTokenBundle\DependencyInjection\Compiler\ObjectManagerCompilerPass` and inlined its logic to the container extension +- Removed the `gesdinet.jwtrefreshtoken.object_manager.id` container parameter +- Removed deprecated configuration nodes: + - `firewall` - No replacement + - `user_provider` - No direct replacement, the user provider should be set on the security firewall configuration instead + - `user_identity_field` - No replacement + - `user_checker` - No direct replacement, the user checker should be set on the security firewall configuration instead + - `refresh_token_entity` - Use the `refresh_token_class` node instead + - `entity_manager` - Use the `object_manager` node instead + - `doctrine_mappings` - No replacement + - `manager_type` - Set the `object_manager` when needed + - `logout_firewall` - Set the `invalidate_token_on_logout` config on the `refresh_jwt` authenticator instead diff --git a/composer.json b/composer.json index 141d9b03..8b11ae30 100644 --- a/composer.json +++ b/composer.json @@ -13,36 +13,33 @@ "MIT" ], "require" : { - "php": ">=7.4", - "doctrine/persistence": "^1.3.3|^2.0|^3.0", - "lexik/jwt-authentication-bundle": "^2.0|^3.0", - "symfony/config": "^4.4|^5.4|^6.0|^7.0", - "symfony/console": "^4.4|^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^4.4|^5.4|^6.0|^7.0", + "php": ">=8.1", + "doctrine/persistence": "^2.5|^3.0", + "lexik/jwt-authentication-bundle": "^2.15|^3.0", + "symfony/config": "^5.4|^6.4|^7.0", + "symfony/console": "^5.4|^6.4|^7.0", + "symfony/dependency-injection": "^5.4|^6.4|^7.0", "symfony/deprecation-contracts": "^2.1|^3.0", - "symfony/event-dispatcher": "^4.4|^5.4|^6.0|^7.0", - "symfony/http-foundation": "^4.4|^5.4|^6.0|^7.0", - "symfony/http-kernel": "^4.4|^5.4|^6.0|^7.0", - "symfony/polyfill-php80": "^1.15", - "symfony/property-access": "^4.4|^5.4|^6.0|^7.0", - "symfony/security-bundle": "^4.4|^5.4|^6.0|^7.0", - "symfony/security-core": "^4.4|^5.4|^6.0|^7.0", - "symfony/security-http": "^4.4|^5.4|^6.0|^7.0" + "symfony/event-dispatcher": "^5.4|^6.4|^7.0", + "symfony/http-foundation": "^5.4|^6.4|^7.0", + "symfony/http-kernel": "^5.4|^6.4|^7.0", + "symfony/property-access": "^5.4|^6.4|^7.0", + "symfony/security-bundle": "^5.4|^6.4|^7.0", + "symfony/security-core": "^5.4|^6.4|^7.0", + "symfony/security-http": "^5.4|^6.4|^7.0" }, "require-dev": { "doctrine/annotations": "^1.13|^2.0", - "doctrine/cache": "^1.11|^2.0", - "doctrine/mongodb-odm": "^2.2", - "doctrine/orm": "^2.7", - "matthiasnoback/symfony-config-test": "^4.2|^5.0", - "matthiasnoback/symfony-dependency-injection-test": "^4.2|^5.0", + "doctrine/mongodb-odm": "^2.3", + "doctrine/orm": "^2.12", + "matthiasnoback/symfony-config-test": "^5.1", + "matthiasnoback/symfony-dependency-injection-test": "^5.1", "phpunit/phpunit": "^9.5", - "symfony/cache": "^4.4|^5.4|^6.0|^7.0", - "symfony/security-guard": "^4.4|^5.4" + "symfony/cache": "^5.4|^6.4|^7.0" }, "conflict": { - "doctrine/mongodb-odm": "<2.2", - "doctrine/orm": "<2.7" + "doctrine/mongodb-odm": "<2.3", + "doctrine/orm": "<2.12" }, "config": { "allow-plugins": { @@ -65,7 +62,7 @@ }, "extra" : { "branch-alias" : { - "dev-master" : "1.x-dev" + "dev-master" : "2.x-dev" } }, "minimum-stability": "dev",