8000 [Security] Add a method in the security helper to ease programmatic l… · symfony/symfony@aabf498 · GitHub
[go: up one dir, main page]

Skip to content

Commit aabf498

Browse files
committed
[Security] Add a method in the security helper to ease programmatic login (#40662)
1 parent 27b1654 commit aabf498

File tree

4 files changed

+126
-12
lines changed

4 files changed

+126
-12
lines changed

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
4343
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
4444
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
45+
use Symfony\Component\Security\Core\Security;
4546
use Symfony\Component\Security\Core\User\ChainUserProvider;
4647
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
4748
use Symfony\Component\Security\Core\User\UserProviderInterface;
@@ -293,16 +294,19 @@ private function createFirewalls(array $config, ContainerBuilder $container)
293294

294295
// load firewall map
295296
$mapDef = $container->getDefinition('security.firewall.map');
296-
$map = $authenticationProviders = $contextRefs = [];
297+
$map = $authenticationProviders = $contextRefs = $authenticators = [];
297298
foreach ($firewalls as $name => $firewall) {
298299
if (isset($firewall['user_checker']) && 'security.user_checker' !== $firewall['user_checker']) {
299300
$customUserChecker = true;
300301
}
301302

302303
$configId = 'security.firewall.map.config.'.$name;
303304

304-
[$matcher, $listeners, $exceptionListener, $logoutListener] = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $configId);
305+
[$matcher, $listeners, $exceptionListener, $logoutListener, $firewallAuthenticators] = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $configId);
305306

307+
$authenticators[$name] = array_map(function ($serviceId) {
308+
return new Reference($serviceId);
309+
}, $firewallAuthenticators);
306310
$contextId = 'security.firewall.map.context.'.$name;
307311
$isLazy = !$firewall['stateless'] && (!empty($firewall['anonymous']['lazy']) || $firewall['lazy']);
308312
$context = new ChildDefinition($isLazy ? 'security.firewall.lazy_context' : 'security.firewall.context');
@@ -317,6 +321,10 @@ private function createFirewalls(array $config, ContainerBuilder $container)
317321
$contextRefs[$contextId] = new Reference($contextId);
318322
$map[$contextId] = $matcher;
319323
}
324+
$container
325+
->getDefinition('security.helper')
326+
->replaceArgument(0, $authenticators)
327+
;
320328

321329
$container->setAlias('security.firewall.context_locator', (string) ServiceLocatorTagPass::register($container, $contextRefs));
322330

@@ -362,7 +370,7 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
362370

363371
// Security disabled?
364372
if (false === $firewall['security']) {
365-
return [$matcher, [], null, null];
373+
return [$matcher, [], null, null, []];
366374
}
367375

368376
$config->replaceArgument(4, $firewall['stateless']);
@@ -565,7 +573,7 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
565573
$config->replaceArgument(10, $listenerKeys);
566574
$config->replaceArgument(11, $firewall['switch_user'] ?? null);
567575

568-
return [$matcher, $listeners, $exceptionListener, null !== $logoutListenerId ? new Reference($logoutListenerId) : null];
576+
return [$matcher, $listeners, $exceptionListener, null !== $logoutListenerId ? new Reference($logoutListenerId) : null, $firewallAuthenticationProviders];
569577
}
570578

571579
private function createContextListener(ContainerBuilder $container, string $contextKey, ?string $firewallEventDispatcherId)

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,17 @@
8787
->set('security.untracked_token_storage', TokenStorage::class)
8888

8989
->set('security.helper', Security::class)
90-
->args([service_locator([
91-
'security.token_storage' => service('security.token_storage'),
92-
'security.authorization_checker' => service('security.authorization_checker'),
93-
])])
90+
->args([
91+
abstract_arg('authenticators'),
92+
service_locator([
93+
'security.token_storage' => service('security.token_storage'),
94+
'security.authorization_checker' => service('security.authorization_checker'),
95+
'security.user_authenticator' => service('security.user_authenticator'),
96+
'request_stack' => service('request_stack'),
97+
'security.firewall.map' => service('security.firewall.map'),
98+
'security.user_checker' => service('security.user_checker'),
99+
]),
100+
])
94101
->alias(Security::class, 'security.helper')
95102

96103
->set('security.user_value_resolver', UserValueResolver::class)

src/Symfony/Component/Security/Core/Security.php

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
use Psr\Container\ContainerInterface;
1515
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
1616
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
17+
use Symfony\Component\Security\Core\Exception\LogicException;
1718
use Symfony\Component\Security\Core\User\UserInterface;
19+
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
1820

1921
/**
2022
* Helper class for commonly-needed security tasks.
@@ -30,8 +32,14 @@ class Security implements AuthorizationCheckerInterface
3032

3133
private $container;
3234

33-
public function __construct(ContainerInterface $container)
35+
/**
36+
* @var array
37+
*/
38+
private $authenticators;
39+
40+
public function __construct(array $authenticators, ContainerInterface $container)
3441
{
42+
$this->authenticators = $authenticators;
3543
$this->container = $container;
3644
}
3745

@@ -69,4 +77,35 @@ public function getToken(): ?TokenInterface
6977
{
7078
return $this->container->get('security.token_storage')->getToken();
7179
}
80+
81+
public function autoLogin(UserInterface $user, AuthenticatorInterface $authenticator = null): void
82+
{
83+
$request = $this->container->get('request_stack')->getCurrentRequest();
84+
85+
if (null === $authenticator) {
86+
$firewall = $this->container->get('security.firewall.map')->getFirewallConfig($request);
87+
88+
if (null === $firewall) {
89+
throw new LogicException('No firewall found as the current route is not covered by any firewall.');
90+
}
91+
$firewallName = $firewall->getName();
92+
93+
if (!\array_key_exists($firewallName, $this->authenticators) || 0 === \count($this->authenticators[$firewallName])) {
94+
throw new LogicException(sprintf('No authenticators found for the firewall "%s".', $firewallName));
95+
}
96+
$firewallAuthenticators = $this->authenticators[$firewallName];
97+
98+
if (0 === \count($firewallAuthenticators)) {
99+
throw new LogicException('No authenticator was found for the firewall "%s".');
100+
}
101+
102+
if (\count($firewallAuthenticators) > 1) {
103+
throw new LogicException('Too much authenticators were found for the firewall "%s". You must provide an instance of '.AuthenticatorInterface::class.' to allow an auto login');
104+
}
105+
$authenticator = array_pop($firewallAuthenticators);
106+
}
107+
// Throw the exception if any pre-auth check does not pass
108+
$this->container->get('security.user_checker')->checkPreAuth($user);
109+
$this->container->get('security.user_authenticator')->authenticateUser($user, $authenticator, $request);
110+
}
72111
}

src/Symfony/Component/Security/Core/Tests/SecurityTest.php

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,20 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Psr\Container\ContainerInterface;
16+
use Symfony\Bundle\SecurityBundle\Security\FirewallConfig;
17+
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
18+
use Symfony\Component\HttpFoundation\Request;
19+
use Symfony\Component\HttpFoundation\RequestStack;
1620
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
1721
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
1822
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
1923
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
2024
use Symfony\Component\Security\Core\Security;
2125
use Symfony\Component\Security\Core\User\InMemoryUser;
26+
use Symfony\Component\Security\Core\User\UserCheckerInterface;
27+
use Symfony\Component\Security\Core\User\UserInterface;
28+
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
29+
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
2230

2331
class SecurityTest extends TestCase
2432
{
@@ -33,7 +41,7 @@ public function testGetToken()
3341

3442
$container = $this->createContainer('security.token_storage', $tokenStorage);
3543

36-
$security = new Security($container);
44+
$security = new Security([], $container);
3745
$this->assertSame($token, $security->getToken());
3846
}
3947

@@ -54,7 +62,7 @@ public function testGetUser($userInToken, $expectedUser)
5462

5563
$container = $this->createContainer('security.token_storage', $tokenStorage);
5664

57-
$security = new Security($container);
65+
$security = new Security([], $container);
5866
$this->assertSame($expectedUser, $security->getUser());
5967
}
6068

@@ -81,10 +89,62 @@ public function testIsGranted()
8189

8290
$container = $this->createContainer('security.authorization_checker', $authorizationChecker);
8391

84-
$security = new Security($container);
92+
$security = new Security([], $container);
8593
$this->assertTrue($security->isGranted('SOME_ATTRIBUTE', 'SOME_SUBJECT'));
8694
}
8795

96+
public function testAutoLogin()
97+
{
98+
$request = new Request();
99+
$authenticator = $this->createMock(AuthenticatorInterface::class);
100+
$requestStack = $this->createMock(RequestStack::class);
101+
$firewallMap = $this->createMock(FirewallMap::class);
102+
$firewall = new FirewallConfig('main', 'main');
103+
$userAuthenticator = $this->createMock(UserAuthenticatorInterface::class);
104+
$user = $this->createMock(UserInterface::class);
105+
$userChecker = $this->createMock(UserCheckerInterface::class);
106+
107+
$container = $this->createMock(ContainerInterface::class);
108+
$container
109+
->expects($this->atLeastOnce())
110+
->method('get')
111+
->willReturnMap([
112+
['request_stack', $requestStack],
113+
['security.firewall.map', $firewallMap],
114+
['security.user_authenticator', $userAuthenticator],
115+
['security.user_checker', $userChecker],
116+
])
117+
;
118+
119+
$requestStack
120+
->expects($this->once())
121+
->method('getCurrentRequest')
122+
->willReturn($request)
123+
;
124+
125+
$firewallMap
126+
->expects($this->once())
127+
->method('getFirewallConfig')
128+
->willReturn($firewall)
129+
;
130+
$userAuthenticator
131+
->expects($this->once())
132+
->method('authenticateUser')
133+
->with($user, $authenticator, $request)
134+
;
135+
$userChecker
136+
->expects($this->once())
137+
->method('checkPreAuth')
138+
->with($user)
139+
;
140+
141+
$security = new Security([
142+
'main' => [$authenticator],
143+
], $container);
144+
145+
$security->autoLogin($user);
146+
}
147+
88148
private function createContainer($serviceId, $serviceObject)
89149
{
90150
$container = $this->createMock(ContainerInterface::class);

0 commit comments

Comments
 (0)
0