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

Skip to content

Commit bd0881d

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

File tree

4 files changed

+119
-7
lines changed

4 files changed

+119
-7
lines changed

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

Lines changed: 12 additions & 3 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;
@@ -289,16 +290,17 @@ private function createFirewalls(array $config, ContainerBuilder $container)
289290

290291
// load firewall map
291292
$mapDef = $container->getDefinition('security.firewall.map');
292-
$map = $authenticationProviders = $contextRefs = [];
293+
$map = $authenticationProviders = $contextRefs = $authenticators = [];
293294
foreach ($firewalls as $name => $firewall) {
294295
if (isset($firewall['user_checker']) && 'security.user_checker' !== $firewall['user_checker']) {
295296
$customUserChecker = true;
296297
}
297298

298299
$configId = 'security.firewall.map.config.'.$name;
299300

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

303+
$authenticators[$name] = $firewallAuthenticators;
302304
$contextId = 'security.firewall.map.context.'.$name;
303305
$isLazy = !$firewall['stateless'] && (!empty($firewall['anonymous']['lazy']) || $firewall['lazy']);
304306
$context = new ChildDefinition($isLazy ? 'security.firewall.lazy_context' : 'security.firewall.context');
@@ -313,6 +315,13 @@ private function createFirewalls(array $config, ContainerBuilder $container)
313315
$contextRefs[$contextId] = new Reference($contextId);
314316
$map[$contextId] = $matcher;
315317
}
318+
$container
319+
->setDefinition(
320+
'security.helper',
321+
(new Definition(Security::class))
322+
->setArgument('$authenticators', $authenticators)
323+
)
324+
;
316325

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

@@ -561,7 +570,7 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
561570
$config->replaceArgument(10, $listenerKeys);
562571
$config->replaceArgument(11, $firewall['switch_user'] ?? null);
563572

564-
return [$matcher, $listeners, $exceptionListener, null !== $logoutListenerId ? new Reference($logoutListenerId) : null];
573+
return [$matcher, $listeners, $exceptionListener, null !== $logoutListenerId ? new Reference($logoutListenerId) : null, $firewallAuthenticationProviders];
565574
}
566575

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

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@
9090
->args([service_locator([
9191
'security.token_storage' => service('security.token_storage'),
9292
'security.authorization_checker' => service('security.authorization_checker'),
93+
'security.user_authenticator' => service('security.user_authenticator'),
94+
'request_stack' => service('request_stack'),
95+
'security.firewall.map' => service('security.firewall.map'),
96+
'security.user_checker' => service('security.user_checker'),
9397
])])
9498
->alias(Security::class, 'security.helper')
9599

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