From 907ef311bf789f5641dfc718a5a59dcd9fd5ace4 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sat, 15 Aug 2020 14:02:24 +0200 Subject: [PATCH] Lazily load the user during the check passport event --- .../DependencyInjection/SecurityExtension.php | 9 +- .../config/security_authenticator.php | 14 ++++ .../Tests/Functional/AuthenticatorTest.php | 52 ++++++++++++ .../AuthenticatorBundle/ApiAuthenticator.php | 53 ++++++++++++ .../AuthenticatorBundle/ProfileController.php | 24 ++++++ .../Tests/Functional/CsrfFormLoginTest.php | 4 - .../Functional/app/Authenticator/bundles.php | 15 ++++ .../Functional/app/Authenticator/config.yml | 33 ++++++++ .../Authenticator/firewall_user_provider.yml | 10 +++ .../Authenticator/implicit_user_provider.yml | 9 ++ .../Functional/app/Authenticator/routing.yml | 4 + .../CheckLdapCredentialsListenerTest.php | 13 ++- .../GuardBridgeAuthenticator.php | 29 +++++-- .../GuardBridgeAuthenticatorTest.php | 13 ++- .../Authentication/AuthenticatorManager.php | 3 +- .../AbstractPreAuthenticatedAuthenticator.php | 8 +- .../Authenticator/FormLoginAuthenticator.php | 14 ++-- .../Authenticator/HttpBasicAuthenticator.php | 13 +-- .../Authenticator/JsonLoginAuthenticator.php | 13 +-- .../Passport/Badge/UserBadge.php | 83 +++++++++++++++++++ .../Http/Authenticator/Passport/Passport.php | 22 ++++- .../Passport/SelfValidatingPassport.php | 14 +++- .../Authenticator/RememberMeAuthenticator.php | 3 +- .../EventListener/CsrfProtectionListener.php | 2 +- .../EventListener/UserProviderListener.php | 48 +++++++++++ .../AuthenticatorManagerTest.php | 11 +-- .../JsonLoginAuthenticatorTest.php | 5 -- .../Authenticator/X509AuthenticatorTest.php | 24 +++--- .../CheckCredentialsListenerTest.php | 9 +- .../CsrfProtectionListenerTest.php | 3 +- .../PasswordMigratingListenerTest.php | 7 +- .../EventListener/RememberMeListenerTest.php | 5 +- .../SessionStrategyListenerTest.php | 3 +- .../EventListener/UserCheckerListenerTest.php | 7 +- .../UserProviderListenerTest.php | 79 ++++++++++++++++++ 35 files changed, 570 insertions(+), 88 deletions(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticatorTest.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ApiAuthenticator.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ProfileController.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/bundles.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/config.yml create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/firewall_user_provider.yml create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/implicit_user_provider.yml create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/routing.yml create mode 100644 src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/UserBadge.php create mode 100644 src/Symfony/Component/Security/Http/EventListener/UserProviderListener.php create mode 100644 src/Symfony/Component/Security/Http/Tests/EventListener/UserProviderListenerTest.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 14a87082c8..889f75f819 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -41,6 +41,7 @@ use Symfony\Component\Security\Core\User\ChainUserProvider; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Controller\UserValueResolver; use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; +use Symfony\Component\Security\Http\Event\CheckPassportEvent; use Twig\Extension\AbstractExtension; /** @@ -342,6 +343,12 @@ class SecurityExtension extends Extension implements PrependExtensionInterface throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall['provider'])); } $defaultProvider = $providerIds[$normalizedName]; + + if ($this->authenticatorManagerEnabled) { + $container->setDefinition('security.listener.'.$id.'.user_provider', new ChildDefinition('security.listener.user_provider.abstract')) + ->addTag('kernel.event_listener', ['event' => CheckPassportEvent::class, 'priority' => 2048, 'method' => 'checkPassport']) + ->replaceArgument(0, new Reference($defaultProvider)); + } } elseif (1 === \count($providerIds)) { $defaultProvider = reset($providerIds); } @@ -632,7 +639,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface return $userProvider; } - if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey) { + if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey || 'custom_authenticators' === $factoryKey) { return 'security.user_providers'; } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php index 0274a50765..4a7453136b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php @@ -23,11 +23,13 @@ use Symfony\Component\Security\Http\Authenticator\JsonLoginAuthenticator; use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticator; use Symfony\Component\Security\Http\Authenticator\RemoteUserAuthenticator; use Symfony\Component\Security\Http\Authenticator\X509Authenticator; +use Symfony\Component\Security\Http\Event\CheckPassportEvent; use Symfony\Component\Security\Http\EventListener\CheckCredentialsListener; use Symfony\Component\Security\Http\EventListener\PasswordMigratingListener; use Symfony\Component\Security\Http\EventListener\RememberMeListener; use Symfony\Component\Security\Http\EventListener\SessionStrategyListener; use Symfony\Component\Security\Http\EventListener\UserCheckerListener; +use Symfony\Component\Security\Http\EventListener\UserProviderListener; use Symfony\Component\Security\Http\Firewall\AuthenticatorManagerListener; return static function (ContainerConfigurator $container) { @@ -73,6 +75,18 @@ return static function (ContainerConfigurator $container) { ]) ->tag('kernel.event_subscriber') + ->set('security.listener.user_provider', UserProviderListener::class) + ->args([ + service('security.user_providers'), + ]) + ->tag('kernel.event_listener', ['event' => CheckPassportEvent::class, 'priority' => 1024, 'method' => 'checkPassport']) + + ->set('security.listener.user_provider.abstract', UserProviderListener::class) + ->abstract() + ->args([ + abstract_arg('user provider'), + ]) + ->set('security.listener.password_migrating', PasswordMigratingListener::class) ->args([ service('security.encoder_factory'), diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticatorTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticatorTest.php new file mode 100644 index 0000000000..201e446e04 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticatorTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional; + +class AuthenticatorTest extends AbstractWebTestCase +{ + /** + * @dataProvider provideEmails + */ + public function testGlobalUserProvider($email) + { + $client = $this->createClient(['test_case' => 'Authenticator', 'root_config' => 'implicit_user_provider.yml']); + + $client->request('GET', '/profile', [], [], [ + 'HTTP_X-USER-EMAIL' => $email, + ]); + $this->assertJsonStringEqualsJsonString('{"email":"'.$email.'"}', $client->getResponse()->getContent()); + } + + /** + * @dataProvider provideEmails + */ + public function testFirewallUserProvider($email, $withinFirewall) + { + $client = $this->createClient(['test_case' => 'Authenticator', 'root_config' => 'firewall_user_provider.yml']); + + $client->request('GET', '/profile', [], [], [ + 'HTTP_X-USER-EMAIL' => $email, + ]); + + if ($withinFirewall) { + $this->assertJsonStringEqualsJsonString('{"email":"'.$email.'"}', $client->getResponse()->getContent()); + } else { + $this->assertJsonStringEqualsJsonString('{"error":"Username could not be found."}', $client->getResponse()->getContent()); + } + } + + public function provideEmails() + { + yield ['jane@example.org', true]; + yield ['john@example.org', false]; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ApiAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ApiAuthenticator.php new file mode 100644 index 0000000000..6bff3145c9 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ApiAuthenticator.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle; + +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; + +class ApiAuthenticator extends AbstractAuthenticator +{ + public function supports(Request $request): ?bool + { + return $request->headers->has('X-USER-EMAIL'); + } + + public function authenticate(Request $request): PassportInterface + { + $email = $request->headers->get('X-USER-EMAIL'); + if (false === strpos($email, '@')) { + throw new BadCredentialsException('Email is not a valid email address.'); + } + + return new SelfValidatingPassport(new UserBadge($email)); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + return null; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + return new JsonResponse([ + 'error' => $exception->getMessageKey(), + ], JsonResponse::HTTP_FORBIDDEN); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ProfileController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ProfileController.php new file mode 100644 index 0000000000..3e23d86e37 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ProfileController.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle; + +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + +class ProfileController extends AbstractController +{ + public function __invoke() + { + $this->denyAccessUnlessGranted('ROLE_USER'); + + return $this->json(['email' => $this->getUser()->getUsername()]); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php index f252314b0c..0302c8a1e3 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php @@ -51,10 +51,6 @@ class CsrfFormLoginTest extends AbstractWebTestCase $client = $this->createClient($options); $form = $client->request('GET', '/login')->selectButton('login')->form(); - if ($options['enable_authenticator_manager'] ?? false) { - $form['user_login[username]'] = 'johannes'; - $form['user_login[password]'] = 'test'; - } $form['user_login[_token]'] = ''; $client->submit($form); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/bundles.php new file mode 100644 index 0000000000..d1e9eb7e0d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/bundles.php @@ -0,0 +1,15 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + new Symfony\Bundle\SecurityBundle\SecurityBundle(), +]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/config.yml new file mode 100644 index 0000000000..5e55d065ff --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/config.yml @@ -0,0 +1,33 @@ +framework: + secret: test + router: { resource: "%kernel.project_dir%/%kernel.test_case%/routing.yml", utf8: true } + test: ~ + default_locale: en + profiler: false + session: + storage_id: session.storage.mock_file + +services: + logger: { class: Psr\Log\NullLogger } + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ProfileController: + public: true + calls: + - ['setContainer', ['@Psr\Container\ContainerInterface']] + tags: [container.service_subscriber] + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator: ~ + +security: + enable_authenticator_manager: true + + encoders: + Symfony\Component\Security\Core\User\User: plaintext + + providers: + in_memory: + memory: + users: + 'jane@example.org': { password: test, roles: [ROLE_USER] } + in_memory2: + memory: + users: + 'john@example.org': { password: test, roles: [ROLE_USER] } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/firewall_user_provider.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/firewall_user_provider.yml new file mode 100644 index 0000000000..59e5e5b536 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/firewall_user_provider.yml @@ -0,0 +1,10 @@ +imports: +- { resource: ./config.yml } + +security: + firewalls: + api: + pattern: / + provider: in_memory + custom_authenticator: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/implicit_user_provider.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/implicit_user_provider.yml new file mode 100644 index 0000000000..ce62733725 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/implicit_user_provider.yml @@ -0,0 +1,9 @@ +imports: +- { resource: ./config.yml } + +security: + firewalls: + api: + pattern: / + custom_authenticator: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/routing.yml new file mode 100644 index 0000000000..2fd12cf560 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/routing.yml @@ -0,0 +1,4 @@ +profile: + path: /profile + defaults: + _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ProfileController diff --git a/src/Symfony/Component/Ldap/Tests/Security/CheckLdapCredentialsListenerTest.php b/src/Symfony/Component/Ldap/Tests/Security/CheckLdapCredentialsListenerTest.php index abc964eb85..5dbb1bf22a 100644 --- a/src/Symfony/Component/Ldap/Tests/Security/CheckLdapCredentialsListenerTest.php +++ b/src/Symfony/Component/Ldap/Tests/Security/CheckLdapCredentialsListenerTest.php @@ -27,6 +27,7 @@ use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; @@ -64,14 +65,13 @@ class CheckLdapCredentialsListenerTest extends TestCase $this->markTestSkipped('This test requires symfony/security-http:^5.1'); } - $user = new User('Wouter', null, ['ROLE_USER']); // no LdapBadge - yield [new TestAuthenticator(), new Passport($user, new PasswordCredentials('s3cret'))]; + yield [new TestAuthenticator(), new Passport(new UserBadge('test'), new PasswordCredentials('s3cret'))]; // ldap already resolved $badge = new LdapBadge('app.ldap'); $badge->markResolved(); - yield [new TestAuthenticator(), new Passport($user, new PasswordCredentials('s3cret'), [$badge])]; + yield [new TestAuthenticator(), new Passport(new UserBadge('test'), new PasswordCredentials('s3cret'), [$badge])]; } public function testPasswordCredentialsAlreadyResolvedThrowsException() @@ -81,8 +81,7 @@ class CheckLdapCredentialsListenerTest extends TestCase $badge = new PasswordCredentials('s3cret'); $badge->markResolved(); - $user = new User('Wouter', null, ['ROLE_USER']); - $passport = new Passport($user, $badge, [new LdapBadge('app.ldap')]); + $passport = new Passport(new UserBadge('test'), $badge, [new LdapBadge('app.ldap')]); $listener = $this->createListener(); $listener->onCheckPassport(new CheckPassportEvent(new TestAuthenticator(), $passport)); @@ -116,7 +115,7 @@ class CheckLdapCredentialsListenerTest extends TestCase } // no password credentials - yield [new SelfValidatingPassport(new User('Wouter', null, ['ROLE_USER']), [new LdapBadge('app.ldap')])]; + yield [new SelfValidatingPassport(new UserBadge('test'), [new LdapBadge('app.ldap')])]; // no user passport $passport = $this->createMock(PassportInterface::class); @@ -181,7 +180,7 @@ class CheckLdapCredentialsListenerTest extends TestCase { return new CheckPassportEvent( new TestAuthenticator(), - new Passport(new User('Wouter', null, ['ROLE_USER']), new PasswordCredentials($password), [$ldapBadge ?? new LdapBadge('app.ldap')]) + new Passport(new UserBadge('Wouter', function () { return new User('Wouter', null, ['ROLE_USER']); }), new PasswordCredentials($password), [$ldapBadge ?? new LdapBadge('app.ldap')]) ); } diff --git a/src/Symfony/Component/Security/Guard/Authenticator/GuardBridgeAuthenticator.php b/src/Symfony/Component/Security/Guard/Authenticator/GuardBridgeAuthenticator.php index e07e8746a8..6d51428c99 100644 --- a/src/Symfony/Component/Security/Guard/Authenticator/GuardBridgeAuthenticator.php +++ b/src/Symfony/Component/Security/Guard/Authenticator/GuardBridgeAuthenticator.php @@ -24,6 +24,7 @@ use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; @@ -62,14 +63,11 @@ class GuardBridgeAuthenticator implements InteractiveAuthenticatorInterface } // get the user from the GuardAuthenticator - $user = $this->guard->getUser($credentials, $this->userProvider); - - if (null === $user) { - throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', get_debug_type($this->guard))); - } - - if (!$user instanceof UserInterface) { - throw new \UnexpectedValueException(sprintf('The "%s::getUser()" method must return a UserInterface. You returned "%s".', get_debug_type($this->guard), get_debug_type($user))); + if (class_exists(UserBadge::class)) { + $user = new UserBadge('guard_authenticator_'.md5(serialize($credentials)), function () use ($credentials) { return $this->getUser($credentials); }); + } else { + // BC with symfony/security-http:5.1 + $user = $this->getUser($credentials); } $passport = new Passport($user, new CustomCredentials([$this->guard, 'checkCredentials'], $credentials)); @@ -84,6 +82,21 @@ class GuardBridgeAuthenticator implements InteractiveAuthenticatorInterface return $passport; } + private function getUser($credentials): UserInterface + { + $user = $this->guard->getUser($credentials, $this->userProvider); + + if (null === $user) { + throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', get_debug_type($this->guard))); + } + + if (!$user instanceof UserInterface) { + throw new \UnexpectedValueException(sprintf('The "%s::getUser()" method must return a UserInterface. You returned "%s".', get_debug_type($this->guard), get_debug_type($user))); + } + + return $user; + } + public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface { if (!$passport instanceof UserPassportInterface) { diff --git a/src/Symfony/Component/Security/Guard/Tests/Authenticator/GuardBridgeAuthenticatorTest.php b/src/Symfony/Component/Security/Guard/Tests/Authenticator/GuardBridgeAuthenticatorTest.php index f6f5c5e544..36ca5626a9 100644 --- a/src/Symfony/Component/Security/Guard/Tests/Authenticator/GuardBridgeAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Guard/Tests/Authenticator/GuardBridgeAuthenticatorTest.php @@ -22,6 +22,7 @@ use Symfony\Component\Security\Guard\Authenticator\GuardBridgeAuthenticator; use Symfony\Component\Security\Guard\AuthenticatorInterface; use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; @@ -83,6 +84,7 @@ class GuardBridgeAuthenticatorTest extends TestCase ->willReturn($user); $passport = $this->authenticator->authenticate($request); + $this->assertEquals($user, $passport->getUser()); $this->assertTrue($passport->hasBadge(CustomCredentials::class)); $this->guardAuthenticator->expects($this->once()) @@ -110,7 +112,8 @@ class GuardBridgeAuthenticatorTest extends TestCase ->with($credentials, $this->userProvider) ->willReturn(null); - $this->authenticator->authenticate($request); + $passport = $this->authenticator->authenticate($request); + $passport->getUser(); } /** @@ -126,12 +129,6 @@ class GuardBridgeAuthenticatorTest extends TestCase ->with($request) ->willReturn($credentials); - $user = new User('test', null, ['ROLE_USER']); - $this->guardAuthenticator->expects($this->once()) - ->method('getUser') - ->with($credentials, $this->userProvider) - ->willReturn($user); - $this->guardAuthenticator->expects($this->once()) ->method('supportsRememberMe') ->willReturn($rememberMeSupported); @@ -156,7 +153,7 @@ class GuardBridgeAuthenticatorTest extends TestCase ->with($user, 'main') ->willReturn($token); - $this->assertSame($token, $this->authenticator->createAuthenticatedToken(new SelfValidatingPassport($user), 'main')); + $this->assertSame($token, $this->authenticator->createAuthenticatedToken(new SelfValidatingPassport(new UserBadge('test', function () use ($user) { return $user; })), 'main')); } public function testHandleSuccess() diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php index 7b255f937c..1d299c0f1c 100644 --- a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php @@ -24,6 +24,7 @@ use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\Passport\AnonymousPassport; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\AuthenticationTokenCreatedEvent; @@ -69,7 +70,7 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request, array $badges = []): ?Response { // create an authenticated token for the User - $token = $authenticator->createAuthenticatedToken($passport = new SelfValidatingPassport($user, $badges), $this->firewallName); + $token = $authenticator->createAuthenticatedToken($passport = new SelfValidatingPassport(new UserBadge($user->getUsername(), function () use ($user) { return $user; }), $badges), $this->firewallName); // announce the authenticated token $token = $this->eventDispatcher->dispatch(new AuthenticationTokenCreatedEvent($token))->getAuthenticatedToken(); diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php index e3d656c231..e957c99048 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php @@ -21,6 +21,7 @@ use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PreAuthenticatedUserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; @@ -86,10 +87,9 @@ abstract class AbstractPreAuthenticatedAuthenticator implements InteractiveAuthe public function authenticate(Request $request): PassportInterface { - $username = $request->attributes->get('_pre_authenticated_username'); - $user = $this->userProvider->loadUserByUsername($username); - - return new SelfValidatingPassport($user, [new PreAuthenticatedUserBadge()]); + return new SelfValidatingPassport(new UserBadge($request->attributes->get('_pre_authenticated_username'), function ($username) { + return $this->userProvider->loadUserByUsername($username); + }), [new PreAuthenticatedUserBadge()]); } public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface diff --git a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php index 201eab349d..53ac77362f 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php @@ -28,6 +28,7 @@ use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerI use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; @@ -80,12 +81,15 @@ class FormLoginAuthenticator extends AbstractLoginFormAuthenticator public function authenticate(Request $request): PassportInterface { $credentials = $this->getCredentials($request); - $user = $this->userProvider->loadUserByUsername($credentials['username']); - if (!$user instanceof UserInterface) { - throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); - } - $passport = new Passport($user, new PasswordCredentials($credentials['password']), [new RememberMeBadge()]); + $passport = new Passport(new UserBadge($credentials['username'], function ($username) { + $user = $this->userProvider->loadUserByUsername($username); + if (!$user instanceof UserInterface) { + throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); + } + + return $user; + }), new PasswordCredentials($credentials['password']), [new RememberMeBadge()]); if ($this->options['enable_csrf']) { $passport->addBadge(new CsrfTokenBadge($this->options['csrf_token_id'], $credentials['csrf_token'])); } diff --git a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php index 7a70ddc9f3..d320723c0c 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php @@ -22,6 +22,7 @@ use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; @@ -66,12 +67,14 @@ class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEn $username = $request->headers->get('PHP_AUTH_USER'); $password = $request->headers->get('PHP_AUTH_PW', ''); - $user = $this->userProvider->loadUserByUsername($username); - if (!$user instanceof UserInterface) { - throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); - } + $passport = new Passport(new UserBadge($username, function ($username) { + $user = $this->userProvider->loadUserByUsername($username); + if (!$user instanceof UserInterface) { + throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); + } - $passport = new Passport($user, new PasswordCredentials($password)); + return $user; + }), new PasswordCredentials($password)); if ($this->userProvider instanceof PasswordUpgraderInterface) { $passport->addBadge(new PasswordUpgradeBadge($password, $this->userProvider)); } diff --git a/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php index b277082a84..e7ab8a0148 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php @@ -30,6 +30,7 @@ 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\Passport\Badge\PasswordUpgradeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; @@ -87,12 +88,14 @@ class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface throw $e; } - $user = $this->userProvider->loadUserByUsername($credentials['username']); - if (!$user instanceof UserInterface) { - throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); - } + $passport = new Passport(new UserBadge($credentials['username'], function ($username) { + $user = $this->userProvider->loadUserByUsername($username); + if (!$user instanceof UserInterface) { + throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); + } - $passport = new Passport($user, new PasswordCredentials($credentials['password'])); + return $user; + }), new PasswordCredentials($credentials['password'])); if ($this->userProvider instanceof PasswordUpgraderInterface) { $passport->addBadge(new PasswordUpgradeBadge($credentials['password'], $this->userProvider)); } diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/UserBadge.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/UserBadge.php new file mode 100644 index 0000000000..c8235a872a --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/UserBadge.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge; + +use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\EventListener\UserProviderListener; + +/** + * Represents the user in the authentication process. + * + * It uses an identifier (e.g. email, or username) and + * "user loader" to load the related User object. + * + * @author Wouter de Jong + * + * @experimental in 5.2 + */ +class UserBadge implements BadgeInterface +{ + private $userIdentifier; + private $userLoader; + private $user; + + /** + * Initializes the user badge. + * + * You must provide a $userIdentifier. This is a unique string representing the + * user for this authentication (e.g. the email if authentication is done using + * email + password; or a string combining email+company if authentication is done + * based on email *and* company name). This string can be used for e.g. login throttling. + * + * Optionally, you may pass a user loader. This callable receives the $userIdentifier + * as argument and must return a UserInterface object (otherwise a UsernameNotFoundException + * is thrown). If this is not set, the default user provider will be used with + * $userIdentifier as username. + */ + public function __construct(string $userIdentifier, ?callable $userLoader = null) + { + $this->userIdentifier = $userIdentifier; + $this->userLoader = $userLoader; + } + + public function getUser(): UserInterface + { + if (null === $this->user) { + if (null === $this->userLoader) { + throw new \LogicException(sprintf('No user loader is configured, did you forget to register the "%s" listener?', UserProviderListener::class)); + } + + $this->user = ($this->userLoader)($this->userIdentifier); + if (!$this->user instanceof UserInterface) { + throw new UsernameNotFoundException(); + } + } + + return $this->user; + } + + public function getUserLoader(): ?callable + { + return $this->userLoader; + } + + public function setUserLoader(callable $userLoader): void + { + $this->userLoader = $userLoader; + } + + public function isResolved(): bool + { + return true; + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Passport.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Passport.php index 1e3752d0f2..6128582599 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/Passport/Passport.php +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Passport.php @@ -13,6 +13,7 @@ namespace Symfony\Component\Security\Http\Authenticator\Passport; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CredentialsInterface; /** @@ -31,13 +32,22 @@ class Passport implements UserPassportInterface private $attributes = []; /** + * @param UserBadge $userBadge * @param CredentialsInterface $credentials the credentials to check for this authentication, use * SelfValidatingPassport if no credentials should be checked * @param BadgeInterface[] $badges */ - public function __construct(UserInterface $user, CredentialsInterface $credentials, array $badges = []) + public function __construct($userBadge, CredentialsInterface $credentials, array $badges = []) { - $this->user = $user; + if ($userBadge instanceof UserInterface) { + trigger_deprecation('symfony/security-http', '5.2', 'The 1st argument of "%s" must be an instance of "%s", support for "%s" will be removed in symfony/security-http 5.3.', __CLASS__, UserBadge::class, UserInterface::class); + + $this->user = $userBadge; + } elseif ($userBadge instanceof UserBadge) { + $this->addBadge($userBadge); + } else { + throw new \TypeError(sprintf('Argument 1 of "%s" must be an instance of "%s", "%s" given.', __METHOD__, UserBadge::class, get_debug_type($userBadge))); + } $this->addBadge($credentials); foreach ($badges as $badge) { @@ -47,6 +57,14 @@ class Passport implements UserPassportInterface public function getUser(): UserInterface { + if (null === $this->user) { + if (!$this->hasBadge(UserBadge::class)) { + throw new \LogicException('Cannot get the Security user, no username or UserBadge configured for this passport.'); + } + + $this->user = $this->getBadge(UserBadge::class)->getUser(); + } + return $this->user; } diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/SelfValidatingPassport.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/SelfValidatingPassport.php index 597351a85f..9b95baaf88 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/Passport/SelfValidatingPassport.php +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/SelfValidatingPassport.php @@ -13,6 +13,7 @@ namespace Symfony\Component\Security\Http\Authenticator\Passport; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; /** * An implementation used when there are no credentials to be checked (e.g. @@ -25,11 +26,20 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; class SelfValidatingPassport extends Passport { /** + * @param UserBadge $userBadge * @param BadgeInterface[] $badges */ - public function __construct(UserInterface $user, array $badges = []) + public function __construct($userBadge, array $badges = []) { - $this->user = $user; + if ($userBadge instanceof UserInterface) { + trigger_deprecation('symfony/security-http', '5.2', 'The 1st argument of "%s" must be an instance of "%s", support for "%s" will be removed in symfony/security-http 5.3.', __CLASS__, UserBadge::class, UserInterface::class); + + $this->user = $userBadge; + } elseif ($userBadge instanceof UserBadge) { + $this->addBadge($userBadge); + } else { + throw new \TypeError(sprintf('Argument 1 of "%s" must be an instance of "%s", "%s" given.', __METHOD__, UserBadge::class, get_debug_type($userBadge))); + } foreach ($badges as $badge) { $this->addBadge($badge); diff --git a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php index 61ad2aa2ee..967d59f751 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php @@ -17,6 +17,7 @@ use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; @@ -74,7 +75,7 @@ class RememberMeAuthenticator implements InteractiveAuthenticatorInterface throw new \LogicException('No remember me token is set.'); } - return new SelfValidatingPassport($token->getUser()); + return new SelfValidatingPassport(new UserBadge($token->getUsername(), [$token, 'getUser'])); } public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface diff --git a/src/Symfony/Component/Security/Http/EventListener/CsrfProtectionListener.php b/src/Symfony/Component/Security/Http/EventListener/CsrfProtectionListener.php index ae22fa57e1..55f9e6bfcf 100644 --- a/src/Symfony/Component/Security/Http/EventListener/CsrfProtectionListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/CsrfProtectionListener.php @@ -57,6 +57,6 @@ class CsrfProtectionListener implements EventSubscriberInterface public static function getSubscribedEvents(): array { - return [CheckPassportEvent::class => ['checkPassport', 128]]; + return [CheckPassportEvent::class => ['checkPassport', 512]]; } } diff --git a/src/Symfony/Component/Security/Http/EventListener/UserProviderListener.php b/src/Symfony/Component/Security/Http/EventListener/UserProviderListener.php new file mode 100644 index 0000000000..cb0d8fcdae --- /dev/null +++ b/src/Symfony/Component/Security/Http/EventListener/UserProviderListener.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\EventListener; + +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; +use Symfony\Component\Security\Http\Event\CheckPassportEvent; + +/** + * @author Wouter de Jong + * + * @final + * @experimental in 5.2 + */ +class UserProviderListener +{ + private $userProvider; + + public function __construct(UserProviderInterface $userProvider) + { + $this->userProvider = $userProvider; + } + + public function checkPassport(CheckPassportEvent $event): void + { + $passport = $event->getPassport(); + if (!$passport->hasBadge(UserBadge::class)) { + return; + } + + /** @var UserBadge $badge */ + $badge = $passport->getBadge(UserBadge::class); + if (null !== $badge->getUserLoader()) { + return; + } + + $badge->setUserLoader([$this->userProvider, 'loadUserByUsername']); + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php index 736b7f0ccf..5a99f2c46f 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php @@ -21,6 +21,7 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Http\Authentication\AuthenticatorManager; use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; @@ -93,7 +94,7 @@ class AuthenticatorManagerTest extends TestCase $authenticators[($matchingAuthenticatorIndex + 1) % 2]->expects($this->never())->method('authenticate'); - $matchingAuthenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport($this->user)); + $matchingAuthenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport(new UserBadge('wouter', function () { return $this->user; }))); $listenerCalled = false; $this->eventDispatcher->addListener(CheckPassportEvent::class, function (CheckPassportEvent $event) use (&$listenerCalled, $matchingAuthenticator) { @@ -121,7 +122,7 @@ class AuthenticatorManagerTest extends TestCase $authenticator = $this->createAuthenticator(); $this->request->attributes->set('_security_authenticators', [$authenticator]); - $authenticator->expects($this->any())->method('authenticate')->willReturn(new Passport($this->user, new PasswordCredentials('pass'))); + $authenticator->expects($this->any())->method('authenticate')->willReturn(new Passport(new UserBadge('wouter', function () { return $this->user; }), new PasswordCredentials('pass'))); $authenticator->expects($this->once()) ->method('onAuthenticationFailure') @@ -139,7 +140,7 @@ class AuthenticatorManagerTest extends TestCase $authenticator = $this->createAuthenticator(); $this->request->attributes->set('_security_authenticators', [$authenticator]); - $authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport($this->user)); + $authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport(new UserBadge('wouter', function () { return $this->user; }))); $authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); @@ -160,7 +161,7 @@ class AuthenticatorManagerTest extends TestCase $authenticator = $this->createAuthenticator(); $this->request->attributes->set('_security_authenticators', [$authenticator]); - $authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport($this->user)); + $authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport(new UserBadge('wouter', function () { return $this->user; }))); $authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); @@ -216,7 +217,7 @@ class AuthenticatorManagerTest extends TestCase $authenticator->expects($this->any())->method('isInteractive')->willReturn(true); $this->request->attributes->set('_security_authenticators', [$authenticator]); - $authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport($this->user)); + $authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport(new UserBadge('wouter', function () { return $this->user; }))); $authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); $this->tokenStorage->expects($this->once())->method('setToken')->with($this->token); diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php index 0f1967600a..1229607db8 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php @@ -16,7 +16,6 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Security; -use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\JsonLoginAuthenticator; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; @@ -72,8 +71,6 @@ class JsonLoginAuthenticatorTest extends TestCase { $this->setUpAuthenticator(); - $this->userProvider->expects($this->once())->method('loadUserByUsername')->with('dunglas')->willReturn(new User('dunglas', 'pa$$')); - $request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": "dunglas", "password": "foo"}'); $passport = $this->authenticator->authenticate($request); $this->assertEquals('foo', $passport->getBadge(PasswordCredentials::class)->getPassword()); @@ -86,8 +83,6 @@ class JsonLoginAuthenticatorTest extends TestCase 'password_path' => 'authentication.password', ]); - $this->userProvider->expects($this->once())->method('loadUserByUsername')->with('dunglas')->willReturn(new User('dunglas', 'pa$$')); - $request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"authentication": {"username": "dunglas", "password": "foo"}}'); $passport = $this->authenticator->authenticate($request); $this->assertEquals('foo', $passport->getBadge(PasswordCredentials::class)->getPassword()); diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php index 2490f9d042..9f620efd2c 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php @@ -32,11 +32,11 @@ class X509AuthenticatorTest extends TestCase /** * @dataProvider provideServerVars */ - public function testAuthentication($user, $credentials) + public function testAuthentication($username, $credentials) { $serverVars = []; - if ('' !== $user) { - $serverVars['SSL_CLIENT_S_DN_Email'] = $user; + if ('' !== $username) { + $serverVars['SSL_CLIENT_S_DN_Email'] = $username; } if ('' !== $credentials) { $serverVars['SSL_CLIENT_S_DN'] = $credentials; @@ -45,12 +45,13 @@ class X509AuthenticatorTest extends TestCase $request = $this->createRequest($serverVars); $this->assertTrue($this->authenticator->supports($request)); - $this->userProvider->expects($this->once()) + $this->userProvider->expects($this->any()) ->method('loadUserByUsername') - ->with($user) - ->willReturn(new User($user, null)); + ->with($username) + ->willReturn(new User($username, null)); - $this->authenticator->authenticate($request); + $passport = $this->authenticator->authenticate($request); + $this->assertEquals($username, $passport->getUser()->getUsername()); } public static function provideServerVars() @@ -73,7 +74,8 @@ class X509AuthenticatorTest extends TestCase ->with($emailAddress) ->willReturn(new User($emailAddress, null)); - $this->authenticator->authenticate($request); + $passport = $this->authenticator->authenticate($request); + $this->assertEquals($emailAddress, $passport->getUser()->getUsername()); } public static function provideServerVarsNoUser() @@ -108,7 +110,8 @@ class X509AuthenticatorTest extends TestCase ->with('TheUser') ->willReturn(new User('TheUser', null)); - $authenticator->authenticate($request); + $passport = $this->authenticator->authenticate($request); + $this->assertEquals('TheUser', $passport->getUser()->getUsername()); } public function testAuthenticationCustomCredentialsKey() @@ -125,7 +128,8 @@ class X509AuthenticatorTest extends TestCase ->with('cert@example.com') ->willReturn(new User('cert@example.com', null)); - $authenticator->authenticate($request); + $passport = $authenticator->authenticate($request); + $this->assertEquals('cert@example.com', $passport->getUser()->getUsername()); } private function createRequest(array $server) diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/CheckCredentialsListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/CheckCredentialsListenerTest.php index ee63715796..5dc411bef0 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/CheckCredentialsListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/CheckCredentialsListenerTest.php @@ -17,6 +17,7 @@ use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; @@ -53,7 +54,7 @@ class CheckCredentialsListenerTest extends TestCase } $credentials = new PasswordCredentials($password); - $this->listener->checkPassport($this->createEvent(new Passport($this->user, $credentials))); + $this->listener->checkPassport($this->createEvent(new Passport(new UserBadge('wouter', function () { return $this->user; }), $credentials))); if (true === $result) { $this->assertTrue($credentials->isResolved()); @@ -73,7 +74,7 @@ class CheckCredentialsListenerTest extends TestCase $this->encoderFactory->expects($this->never())->method('getEncoder'); - $event = $this->createEvent(new Passport($this->user, new PasswordCredentials(''))); + $event = $this->createEvent(new Passport(new UserBadge('wouter', function () { return $this->user; }), new PasswordCredentials(''))); $this->listener->checkPassport($event); } @@ -91,7 +92,7 @@ class CheckCredentialsListenerTest extends TestCase $credentials = new CustomCredentials(function () use ($result) { return $result; }, ['password' => 'foo']); - $this->listener->checkPassport($this->createEvent(new Passport($this->user, $credentials))); + $this->listener->checkPassport($this->createEvent(new Passport(new UserBadge('wouter', function () { return $this->user; }), $credentials))); if (true === $result) { $this->assertTrue($credentials->isResolved()); @@ -108,7 +109,7 @@ class CheckCredentialsListenerTest extends TestCase { $this->encoderFactory->expects($this->never())->method('getEncoder'); - $event = $this->createEvent(new SelfValidatingPassport($this->user)); + $event = $this->createEvent(new SelfValidatingPassport(new UserBadge('wouter', function () { return $this->user; }))); $this->listener->checkPassport($event); } diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php index 17c80ac250..b5c8c14cbf 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php @@ -18,6 +18,7 @@ use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\CheckPassportEvent; use Symfony\Component\Security\Http\EventListener\CsrfProtectionListener; @@ -75,7 +76,7 @@ class CsrfProtectionListenerTest extends TestCase private function createPassport(?CsrfTokenBadge $badge) { - $passport = new SelfValidatingPassport(new User('wouter', 'pass')); + $passport = new SelfValidatingPassport(new UserBadge('wouter', function ($username) { return new User($username, 'pass'); })); if ($badge) { $passport->addBadge($badge); } diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php index 5b08721e46..bf90aa3be6 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php @@ -20,6 +20,7 @@ use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; @@ -51,10 +52,10 @@ class PasswordMigratingListenerTest extends TestCase public function provideUnsupportedEvents() { // no password upgrade badge - yield [$this->createEvent(new SelfValidatingPassport($this->createMock(UserInterface::class)))]; + yield [$this->createEvent(new SelfValidatingPassport(new UserBadge('test', function () { return $this->createMock(UserInterface::class); })))]; // blank password - yield [$this->createEvent(new SelfValidatingPassport($this->createMock(UserInterface::class), [new PasswordUpgradeBadge('', $this->createPasswordUpgrader())]))]; + yield [$this->createEvent(new SelfValidatingPassport(new UserBadge('test', function () { return $this->createMock(UserInterface::class); }), [new PasswordUpgradeBadge('', $this->createPasswordUpgrader())]))]; // no user yield [$this->createEvent($this->createMock(PassportInterface::class))]; @@ -76,7 +77,7 @@ class PasswordMigratingListenerTest extends TestCase ->with($this->user, 'new-encoded-password') ; - $event = $this->createEvent(new SelfValidatingPassport($this->user, [new PasswordUpgradeBadge('pa$$word', $passwordUpgrader)])); + $event = $this->createEvent(new SelfValidatingPassport(new UserBadge('test', function () { return $this->user; }), [new PasswordUpgradeBadge('pa$$word', $passwordUpgrader)])); $this->listener->onLoginSuccess($event); } diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php index f451c89d96..4714a8a171 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\LoginFailureEvent; @@ -47,7 +48,7 @@ class RememberMeListenerTest extends TestCase { $this->rememberMeServices->expects($this->never())->method('loginSuccess'); - $event = $this->createLoginSuccessfulEvent('main_firewall', $this->response, new SelfValidatingPassport(new User('wouter', null))); + $event = $this->createLoginSuccessfulEvent('main_firewall', $this->response, new SelfValidatingPassport(new UserBadge('wouter', function ($username) { return new User($username, null); }))); $this->listener->onSuccessfulLogin($event); } @@ -78,7 +79,7 @@ class RememberMeListenerTest extends TestCase private function createLoginSuccessfulEvent($firewallName, $response, PassportInterface $passport = null) { if (null === $passport) { - $passport = new SelfValidatingPassport(new User('test', null), [new RememberMeBadge()]); + $passport = new SelfValidatingPassport(new UserBadge('test', function ($username) { return new User($username, null); }), [new RememberMeBadge()]); } return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $passport, $this->token, $this->request, $response, $firewallName); diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/SessionStrategyListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/SessionStrategyListenerTest.php index 80b74e1f49..ac4b5d95af 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/SessionStrategyListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/SessionStrategyListenerTest.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; use Symfony\Component\Security\Http\EventListener\SessionStrategyListener; @@ -62,7 +63,7 @@ class SessionStrategyListenerTest extends TestCase private function createEvent($firewallName) { - return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), new SelfValidatingPassport(new User('test', null)), $this->token, $this->request, null, $firewallName); + return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), new SelfValidatingPassport(new UserBadge('test', function ($username) { return new User($username, null); })), $this->token, $this->request, null, $firewallName); } private function configurePreviousSession() diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php index af359a94f5..5422abfe5d 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php @@ -18,6 +18,7 @@ use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PreAuthenticatedUserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\CheckPassportEvent; @@ -55,7 +56,7 @@ class UserCheckerListenerTest extends TestCase { $this->userChecker->expects($this->never())->method('checkPreAuth'); - $this->listener->preCheckCredentials($this->createCheckPassportEvent(new SelfValidatingPassport($this->user, [new PreAuthenticatedUserBadge()]))); + $this->listener->preCheckCredentials($this->createCheckPassportEvent(new SelfValidatingPassport(new UserBadge('test', function () { return $this->user; }), [new PreAuthenticatedUserBadge()]))); } public function testPostAuthValidCredentials() @@ -75,7 +76,7 @@ class UserCheckerListenerTest extends TestCase private function createCheckPassportEvent($passport = null) { if (null === $passport) { - $passport = new SelfValidatingPassport($this->user); + $passport = new SelfValidatingPassport(new UserBadge('test', function () { return $this->user; })); } return new CheckPassportEvent($this->createMock(AuthenticatorInterface::class), $passport); @@ -84,7 +85,7 @@ class UserCheckerListenerTest extends TestCase private function createLoginSuccessEvent($passport = null) { if (null === $passport) { - $passport = new SelfValidatingPassport($this->user); + $passport = new SelfValidatingPassport(new UserBadge('test', function () { return $this->user; })); } return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $passport, $this->createMock(TokenInterface::class), new Request(), null, 'main'); diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/UserProviderListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/UserProviderListenerTest.php new file mode 100644 index 0000000000..b43aebde96 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/UserProviderListenerTest.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\User\User; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\AnonymousPassport; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; +use Symfony\Component\Security\Http\Event\CheckPassportEvent; +use Symfony\Component\Security\Http\EventListener\UserProviderListener; + +class UserProviderListenerTest extends TestCase +{ + private $userProvider; + private $listener; + + protected function setUp(): void + { + $this->userProvider = $this->createMock(UserProviderInterface::class); + $this->listener = new UserProviderListener($this->userProvider); + } + + public function testSetUserProvider() + { + $passport = new SelfValidatingPassport(new UserBadge('wouter')); + + $this->listener->checkPassport(new CheckPassportEvent($this->createMock(AuthenticatorInterface::class), $passport)); + + $badge = $passport->getBadge(UserBadge::class); + $this->assertEquals([$this->userProvider, 'loadUserByUsername'], $badge->getUserLoader()); + + $user = new User('wouter', null); + $this->userProvider->expects($this->once())->method('loadUserByUsername')->with('wouter')->willReturn($user); + $this->assertSame($user, $passport->getUser()); + } + + /** + * @dataProvider provideCompletePassports + */ + public function testNotOverrideUserLoader($passport) + { + $badgeBefore = $passport->hasBadge(UserBadge::class) ? $passport->getBadge(UserBadge::class) : null; + $this->listener->checkPassport(new CheckPassportEvent($this->createMock(AuthenticatorInterface::class), $passport)); + + $this->assertEquals($passport->hasBadge(UserBadge::class) ? $passport->getBadge(UserBadge::class) : null, $badgeBefore); + } + + public function provideCompletePassports() + { + yield [new AnonymousPassport()]; + yield [new SelfValidatingPassport(new UserBadge('wouter', function () {}))]; + } + + /** + * @group legacy + */ + public function testLegacyUserPassport() + { + $passport = new SelfValidatingPassport($user = $this->createMock(UserInterface::class)); + $this->listener->checkPassport(new CheckPassportEvent($this->createMock(AuthenticatorInterface::class), $passport)); + + $this->assertFalse($passport->hasBadge(UserBadge::class)); + $this->assertSame($user, $passport->getUser()); + } +}