Skip to content

Commit 7cd40b0

Browse files
wouterjfabpot
authored andcommitted
Lazily load the user during the check passport event
1 parent 48f5333 commit 7cd40b0

File tree

11 files changed

+222
-5
lines changed

11 files changed

+222
-5
lines changed

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

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 <[email protected]>
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 ['[email protected]', true];
50+
yield ['[email protected]', false];
51+
}
52+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Badge\UserBadge;
22+
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
23+
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
24+
25+
class ApiAuthenticator extends AbstractAuthenticator
26+
{
27+
public function supports(Request $request): ?bool
28+
{
29+
return $request->headers->has('X-USER-EMAIL');
30+
}
31+
32+
public function authenticate(Request $request): PassportInterface
33+
{
34+
$email = $request->headers->get('X-USER-EMAIL');
35+
if (false === strpos($email, '@')) {
36+
throw new BadCredentialsException('Email is not a valid email address.');
37+
}
38+
39+
return new SelfValidatingPassport(new UserBadge($email));
40+
}
41+
42+
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
43+
{
44+
return null;
45+
}
46+
47+
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
48+
{
49+
return new JsonResponse([
50+
'error' => $exception->getMessageKey(),
51+
], JsonResponse::HTTP_FORBIDDEN);
52+
}
53+
}
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 <[email protected]>
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+
}

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 <[email protected]>
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+
'[email protected]': { password: test, roles: [ROLE_USER] }
30+
in_memory2:
31+
memory:
32+
users:
33+
'[email protected]': { 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

0 commit comments

Comments
 (0)