8000 Lazily load the user during the check passport event · symfony/symfony@4099e8f · GitHub
[go: up one dir, main page]

Skip to content

Commit 4099e8f

Browse files
committed
Lazily load the user during the check passport event
1 parent d6ccc4f commit 4099e8f

35 files changed

+555
-90
lines changed

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
use Symfony\Component\Security\Core\User\UserProviderInterface;
4242
use Symfony\Component\Security\Http\Controller\UserValueResolver;
4343
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
44+
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
4445
use Twig\Extension\AbstractExtension;
4546

4647
/**
@@ -342,6 +343,12 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
342343
throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall['provider']));
343344
}
344345
$defaultProvider = $providerIds[$normalizedName];
346+
347+
if ($this->authenticatorManagerEnabled) {
348+
$container->setDefinition('security.listener.'.$id.'.user_provider', new ChildDefinition('security.listener.user_provider.abstract'))
349+
->addTag('kernel.event_listener', ['event' => CheckPassportEvent::class, 'priority' => 2048, 'method' => 'checkPassport'])
350+
->replaceArgument(0, new Reference($defaultProvider));
351+
}
345352
} elseif (1 === \count($providerIds)) {
346353
$defaultProvider = reset($providerIds);
347354
}
@@ -632,7 +639,7 @@ private function getUserProvider(ContainerBuilder $container, string $id, array
632639
return $userProvider;
633640
}
634641

635-
if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey) {
642+
if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey || 'custom_authenticators' === $factoryKey) {
636643
return 'security.user_providers';
637644
}
638645

src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@
2323
use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticator;
2424
use Symfony\Component\Security\Http\Authenticator\RemoteUserAuthenticator;
2525
use Symfony\Component\Security\Http\Authenticator\X509Authenticator;
26+
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
2627
use Symfony\Component\Security\Http\EventListener\CheckCredentialsListener;
2728
use Symfony\Component\Security\Http\EventListener\PasswordMigratingListener;
2829
use Symfony\Component\Security\Http\EventListener\RememberMeListener;
2930
use Symfony\Component\Security\Http\EventListener\SessionStrategyListener;
3031
use Symfony\Component\Security\Http\EventListener\UserCheckerListener;
32+
use Symfony\Component\Security\Http\EventListener\UserProviderListener;
3133
use Symfony\Component\Security\Http\Firewall\AuthenticatorManagerListener;
3234

3335
return static function (ContainerConfigurator $container) {
@@ -73,6 +75,18 @@
7375
])
7476
->tag('kernel.event_subscriber')
7577

78+
->set('security.listener.user_provider', UserProviderListener::class)
79+
->args([
80+
service('security.user_providers'),
81+
])
82+
->tag('kernel.event_listener', ['event' => CheckPassportEvent::class, 'priority' => 1024, 'method' => 'checkPassport'])
83+
84+
->set('security.listener.user_provider.abstract', UserProviderListener::class)
85+
->abstract()
86+
->args([
87+
abstract_arg('user provider'),
88+
])
89+
7690
->set('security.listener.password_migrating', PasswordMigratingListener::class)
7791
->args([
7892
service('security.encoder_factory'),
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
13+
14+
class AuthenticatorTest extends AbstractWebTestCase
15+
{
16+
/**
17+
* @dataProvider provideEmails
18+
*/
19+
public function testGlobalUserProvider($email)
20+
{
21+
$client = $this->createClient(['test_case' => 'Authenticator', 'root_config' => 'implicit_user_provider.yml']);
22+
23+
$client->request('GET', '/profile', [], [], [
24+
'HTTP_X-USER-EMAIL' => $email,
25+
]);
26+
$this->assertJsonStringEqualsJsonString('{"email":"'.$email.'"}', $client->getResponse()->getContent());
27+
}
28+
29+
/**
30+
* @dataProvider provideEmails
31+
*/
32+
public function testFirewallUserProvider($email, $withinFirewall)
33+
{
34+
$client = $this->createClient(['test_case' => 'Authenticator', 'root_config' => 'firewall_user_provider.yml']);
35+
36+
$client->request('GET', '/profile', [], [], [
37+
'HTTP_X-USER-EMAIL' => $email,
38+
]);
39+
40+
if ($withinFirewall) {
41+
$this->assertJsonStringEqualsJsonString('{"email":"'.$email.'"}', $client->getResponse()->getContent());
42+
} else {
43+
$this->assertJsonStringEqualsJsonString('{"error":"Username could not be found."}', $client->getResponse()->getContent());
44+
}
45+
}
46+
47+
public function provideEmails()
48+
{
49+
yield ['jane@example.org', true];
50+
yield ['john@example.org', false];
51+
}
52+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle;
13+
14+
use Symfony\Component\HttpFoundation\JsonResponse;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\Response;
17+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
18+
use Symfony\Component\Security\Core\Exception\AuthenticationException;
19+
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
20+
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
21+
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
22+
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
23+
24+
class ApiAuthenticator extends AbstractAuthenticator
25+
{
26+
public function supports(Request $request): ?bool
27+
{
28+
return $request->headers->has('X-USER-EMAIL');
29+
}
30+
31+
public function authenticate(Request $request): PassportInterface
32+
{
33+
$email = $request->headers->get('X-USER-EMAIL');
34+
if (false === strpos($email, '@')) {
35+
throw new BadCredentialsException('Email is not a valid email address.');
36+
}
37+
38+
return new SelfValidatingPassport($email);
39+
}
40+
41+
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
42+
{
43+
return null;
44+
}
45+
46+
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
47+
{
48+
return new JsonResponse([
49+
'error' => $exception->getMessageKey(),
50+
], JsonResponse::HTTP_FORBIDDEN);
51+
}
52+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle;
13+
14+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
15+
16+
class ProfileController extends AbstractController
17+
{
18+
public function __invoke()
19+
{
20+
$this->denyAccessUnlessGranted('ROLE_USER');
21+
22+
return $this->json(['email' => $this->getUser()->getUsername()]);
23+
}
24+
}

src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,6 @@ public function testFormLoginWithInvalidCsrfToken($options)
5151
$client = $this->createClient($options);
5252

5353
$form = $client->request('GET', '/login')->selectButton('login')->form();
54-
if ($options['enable_authenticator_manager'] ?? false) {
55-
$form['user_login[username]'] = 'johannes';
56-
$form['user_login[password]'] = 'test';
57-
}
5854
$form['user_login[_token]'] = '';
5955
$client->submit($form);
6056

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
return [
13+
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
14+
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
15+
];
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
framework:
2+
secret: test
3+
router: { resource: "%kernel.project_dir%/%kernel.test_case%/routing.yml", utf8: true }
4+
test: ~
5+
default_locale: en
6+
profiler: false
7+
session:
8+
storage_id: session.storage.mock_file
9+
10+
services:
11+
logger: { class: Psr\Log\NullLogger }
12+
Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ProfileController:
13+
public: true
14+
calls:
15+
- ['setContainer', ['@Psr\Container\ContainerInterface']]
16+
tags: [container.service_subscriber]
17+
Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator: ~
18+
19+
security:
20+
enable_authenticator_manager: true
21+
22+
encoders:
23+
Symfony\Component\Security\Core\User\User: plaintext
24+
25+
providers:
26+
in_memory:
27+
memory:
28+
users:
29+
'jane@example.org': { password: test, roles: [ROLE_USER] }
30+
in_memory2:
31+
memory:
32+
users:
33+
'john@example.org': { password: test, roles: [ROLE_USER] }
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
imports:
2+
- { resource: ./config.yml }
3+
4+
security:
5+
firewalls:
6+
api:
7+
pattern: /
8+
provider: in_memory
9+
custom_authenticator: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator
10+
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
imports:
2+
- { resource: ./config.yml }
3+
4+
security:
5+
firewalls:
6+
api:
7+
pattern: /
8+
custom_authenticator: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator
9+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
profile:
2+
path: /profile
3+
defaults:
4+
_controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ProfileController

src/Symfony/Component/Ldap/Tests/Security/CheckLdapCredentialsListenerTest.php

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
2828
use Symfony\Component\Security\Core\User\User;
2929
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
30+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
3031
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
3132
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
3233
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
@@ -64,14 +65,13 @@ public function provideShouldNotCheckPassport()
6465
$this->markTestSkipped('This test requires symfony/security-http:^5.1');
6566
}
6667

67-
$user = new User('Wouter', null, ['ROLE_USER']);
6868
// no LdapBadge
69-
yield [new TestAuthenticator(), new Passport($user, new PasswordCredentials('s3cret'))];
69+
yield [new TestAuthenticator(), new Passport('wouter', new PasswordCredentials('s3cret'))];
7070

7171
// ldap already resolved
7272
$badge = new LdapBadge('app.ldap');
7373
$badge->markResolved();
74-
yield [new TestAuthenticator(), new Passport($user, new PasswordCredentials('s3cret'), [$badge])];
74+
yield [new TestAuthenticator(), new Passport('wouter', new PasswordCredentials('s3cret'), [$badge])];
7575
}
7676

7777
public function testPasswordCredentialsAlreadyResolvedThrowsException()
@@ -81,8 +81,7 @@ public function testPasswordCredentialsAlreadyResolvedThrowsException()
8181

8282
$badge = new PasswordCredentials('s3cret');
8383
$badge->markResolved();
84-
$user = new User('Wouter', null, ['ROLE_USER']);
85-
$passport = new Passport($user, $badge, [new LdapBadge('app.ldap')]);
84+
$passport = new Passport('wouter', $badge, [new LdapBadge('app.ldap')]);
8685

8786
$listener = $this->createListener();
8887
$listener->onCheckPassport(new CheckPassportEvent(new TestAuthenticator(), $passport));
@@ -116,7 +115,7 @@ public function provideWrongPassportData()
116115
}
117116

118117
// no password credentials
119-
yield [new SelfValidatingPassport(new User('Wouter', null, ['ROLE_USER']), [new LdapBadge('app.ldap')])];
118+
yield [new SelfValidatingPassport('wouter', [new LdapBadge('app.ldap')])];
120119

121120
// no user passport
122121
$passport = $this->createMock(PassportInterface::class);
@@ -181,7 +180,7 @@ private function createEvent($password = 's3cr3t', $ldapBadge = null)
181180
{
182181
return new CheckPassportEvent(
183182
new TestAuthenticator(),
184-
new Passport(new User('Wouter', null, ['ROLE_USER']), new PasswordCredentials($password), [$ldapBadge ?? new LdapBadge('app.ldap')])
183+
new Passport(new UserBadge('Wouter', function () { return new User('Wouter', null, ['ROLE_USER']); }), new PasswordCredentials($password), [$ldapBadge ?? new LdapBadge('app.ldap')])
185184
);
186185
}
187186

src/Symfony/Component/Security/Guard/Authenticator/GuardBridgeAuthenticator.php

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
2525
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge;
2626
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
27+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
2728
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials;
2829
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
2930
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
@@ -62,14 +63,11 @@ public function authenticate(Request $request): PassportInterface
6263
}
6364

6465
// get the user from the GuardAuthenticator
65-
$user = $this->guard->getUser($credentials, $this->userProvider);
66-
67-
if (null === $user) {
68-
throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', get_debug_type($this->guard)));
69-
}
70-
71-
if (!$user instanceof UserInterface) {
72-
throw new \UnexpectedValueException(sprintf('The "%s::getUser()" method must return a UserInterface. You returned "%s".', get_debug_type($this->guard), get_debug_type($user)));
66+
if (class_exists(UserBadge::class)) {
67+
$user = new UserBadge(null, function () use ($credentials) { return $this->getUser($credentials); });
68+
} else {
69+
// BC with symfony/security-http:5.1
70+
$user = $this->getUser($credentials);
7371
}
7472

7573
$passport = new Passport($user, new CustomCredentials([$this->guard, 'checkCredentials'], $credentials));
@@ -84,6 +82,21 @@ public function authenticate(Request $request): PassportInterface
8482
return $passport;
8583
}
8684

85+
private function getUser($credentials): UserInterface
86+
{
87+
$user = $this->guard->getUser($credentials, $this->userProvider);
88+
89+
if (null === $user) {
90+
throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', get_debug_type($this->guard)));
91+
}
92+
93+
if (!$user instanceof UserInterface) {
94+
throw new \UnexpectedValueException(sprintf('The "%s::getUser()" method must return a UserInterface. You returned "%s".', get_debug_type($this->guard), get_debug_type($user)));
95+
}
96+
97+
return $user;
98+
}
99+
87100
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
88101
{
89102
if (!$passport instanceof UserPassportInterface) {

0 commit comments

Comments
 (0)
0