From 5464c5732ce090bacd33a106ed457ddbca47fc5a Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sat, 10 Dec 2022 17:47:26 +0100 Subject: [PATCH] [SecurityBundle] Improve support for authenticators that don't need a user provider Ref https://github.com/symfony/symfony/pull/48285 --- .../Bundle/SecurityBundle/CHANGELOG.md | 1 + .../Security/Factory/AccessTokenFactory.php | 6 ++-- ...StatelessAuthenticatorFactoryInterface.php | 28 +++++++++++++++++++ .../DependencyInjection/SecurityExtension.php | 28 ++++++++++++++----- .../Tests/Functional/AccessTokenTest.php | 12 ++++++++ .../Security/Handler/AccessTokenHandler.php | 6 ++-- .../config_self_contained_token.yml | 26 +++++++++++++++++ 7 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/StatelessAuthenticatorFactoryInterface.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AccessToken/config_self_contained_token.yml diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index 9deb248e1365f..ecf3804fa5253 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Deprecate enabling bundle and not configuring it * Add `_stateless` attribute to the request when firewall is stateless + * Add `StatelessAuthenticatorFactoryInterface` for authenticators targeting `stateless` firewalls only and that don't require a user provider 6.2 --- diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AccessTokenFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AccessTokenFactory.php index 2fbf3b2f8b567..a3ed0e3ee0839 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AccessTokenFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AccessTokenFactory.php @@ -23,7 +23,7 @@ * * @internal */ -final class AccessTokenFactory extends AbstractFactory +final class AccessTokenFactory extends AbstractFactory implements StatelessAuthenticatorFactoryInterface { private const PRIORITY = -40; @@ -67,7 +67,7 @@ public function getKey(): string return 'access_token'; } - public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, ?string $userProviderId): string { $successHandler = isset($config['success_handler']) ? new Reference($this->createAuthenticationSuccessHandler($container, $firewallName, $config)) : null; $failureHandler = isset($config['failure_handler']) ? new Reference($this->createAuthenticationFailureHandler($container, $firewallName, $config)) : null; @@ -78,7 +78,7 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.access_token')) ->replaceArgument(0, new Reference($config['token_handler'])) ->replaceArgument(1, new Reference($extractorId)) - ->replaceArgument(2, new Reference($userProviderId)) + ->replaceArgument(2, $userProviderId ? new Reference($userProviderId) : null) ->replaceArgument(3, $successHandler) ->replaceArgument(4, $failureHandler) ->replaceArgument(5, $config['realm']) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/StatelessAuthenticatorFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/StatelessAuthenticatorFactoryInterface.php new file mode 100644 index 0000000000000..4d536019b36e7 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/StatelessAuthenticatorFactoryInterface.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; + +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Stateless authenticators are authenticators that can work without a user provider. + * + * This situation can only occur in stateless firewalls, as statefull firewalls + * need the user provider to refresh the user in each subsequent request. A + * stateless authenticator can be used on both stateless and statefull authenticators. + * + * @author Wouter de Jong + */ +interface StatelessAuthenticatorFactoryInterface extends AuthenticatorFactoryInterface +{ + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, ?string $userProviderId): string|array; +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 6b91a65d14ae7..9aa0293651b37 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -14,6 +14,7 @@ use Symfony\Bridge\Twig\Extension\LogoutUrlExtension; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FirewallListenerFactoryInterface; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\StatelessAuthenticatorFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -615,6 +616,10 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri throw new InvalidConfigurationException(sprintf('Authenticator factory "%s" ("%s") must implement "%s".', get_debug_type($factory), $key, AuthenticatorFactoryInterface::class)); } + if (null === $userProvider && !$factory instanceof StatelessAuthenticatorFactoryInterface) { + $userProvider = $this->createMissingUserProvider($container, $id, $key); + } + $authenticators = $factory->createAuthenticator($container, $id, $firewall[$key], $userProvider); if (\is_array($authenticators)) { foreach ($authenticators as $authenticator) { @@ -641,7 +646,7 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri return [$listeners, $defaultEntryPoint]; } - private function getUserProvider(ContainerBuilder $container, string $id, array $firewall, string $factoryKey, ?string $defaultProvider, array $providerIds, ?string $contextListenerId): string + private function getUserProvider(ContainerBuilder $container, string $id, array $firewall, string $factoryKey, ?string $defaultProvider, array $providerIds, ?string $contextListenerId): ?string { if (isset($firewall[$factoryKey]['provider'])) { if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall[$factoryKey]['provider'])])) { @@ -660,13 +665,11 @@ private function getUserProvider(ContainerBuilder $container, string $id, array } if (!$providerIds) { - $userProvider = sprintf('security.user.provider.missing.%s', $factoryKey); - $container->setDefinition( - $userProvider, - (new ChildDefinition('security.user.provider.missing'))->replaceArgument(0, $id) - ); + if ($firewall['stateless'] ?? false) { + return null; + } - return $userProvider; + return $this->createMissingUserProvider($container, $id, $factoryKey); } if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey || 'custom_authenticators' === $factoryKey) { @@ -680,6 +683,17 @@ private function getUserProvider(ContainerBuilder $container, string $id, array throw new InvalidConfigurationException(sprintf('Not configuring explicitly the provider for the "%s" authenticator on "%s" firewall is ambiguous as there is more than one registered provider.', $factoryKey, $id)); } + private function createMissingUserProvider(ContainerBuilder $container, string $id, string $factoryKey): string + { + $userProvider = sprintf('security.user.provider.missing.%s', $factoryKey); + $container->setDefinition( + $userProvider, + (new ChildDefinition('security.user.provider.missing'))->replaceArgument(0, $id) + ); + + return $userProvider; + } + private function createHashers(array $hashers, ContainerBuilder $container) { $hasherMap = []; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AccessTokenTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AccessTokenTest.php index 01b205737dd59..d07b5ed3e8eb7 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AccessTokenTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AccessTokenTest.php @@ -315,4 +315,16 @@ public function customQueryAccessTokenFailure(): iterable { yield ['/foo?protection_token=INVALID_ACCESS_TOKEN']; } + + public function testSelfContainedTokens() + { + $client = $this->createClient(['test_case' => 'AccessToken', 'root_config' => 'config_self_contained_token.yml']); + $client->catchExceptions(false); + $client->request('GET', '/foo', [], [], ['HTTP_AUTHORIZATION' => 'Bearer SELF_CONTAINED_ACCESS_TOKEN']); + $response = $client->getResponse(); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame(['message' => 'Welcome @dunglas!'], json_decode($response->getContent(), true)); + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AccessTokenBundle/Security/Handler/AccessTokenHandler.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AccessTokenBundle/Security/Handler/AccessTokenHandler.php index 4f94cc6936a05..8e6c7b6dd44d0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AccessTokenBundle/Security/Handler/AccessTokenHandler.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AccessTokenBundle/Security/Handler/AccessTokenHandler.php @@ -12,19 +12,17 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AccessTokenBundle\Security\Handler; use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; class AccessTokenHandler implements AccessTokenHandlerInterface { - public function __construct() - { - } - public function getUserBadgeFrom(string $accessToken): UserBadge { return match ($accessToken) { 'VALID_ACCESS_TOKEN' => new UserBadge('dunglas'), + 'SELF_CONTAINED_ACCESS_TOKEN' => new UserBadge('dunglas', fn () => new InMemoryUser('dunglas', null, ['ROLE_USER'])), default => throw new BadCredentialsException('Invalid credentials.'), }; } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AccessToken/config_self_contained_token.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AccessToken/config_self_contained_token.yml new file mode 100644 index 0000000000000..8143698fdec1a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AccessToken/config_self_contained_token.yml @@ -0,0 +1,26 @@ +imports: + - { resource: ./../config/framework.yml } + +framework: + http_method_override: false + serializer: ~ + +security: + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext + + firewalls: + main: + pattern: ^/ + stateless: true + access_token: + token_handler: access_token.access_token_handler + token_extractors: 'header' + realm: 'My API' + + access_control: + - { path: ^/foo, roles: ROLE_USER } + +services: + access_token.access_token_handler: + class: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AccessTokenBundle\Security\Handler\AccessTokenHandler