Skip to content

Commit 80658d3

Browse files
johnkrovitchchalasr
authored andcommitted
[Security] Add a method in the security helper to ease programmatic login (#40662)
1 parent 893ff0b commit 80658d3

File tree

4 files changed

+88
-10
lines changed

4 files changed

+88
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add the `Security` helper class
88
* Deprecate the `Symfony\Component\Security\Core\Security` service alias, use `Symfony\Bundle\SecurityBundle\Security\Security` instead
99
* Add `Security::getFirewallConfig()` to help to get the firewall configuration associated to the Request
10+
* Add `Security::login()` to login programmatically
1011

1112
6.1
1213
---

DependencyInjection/SecurityExtension.php

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,16 +277,25 @@ private function createFirewalls(array $config, ContainerBuilder $container)
277277

278278
// load firewall map
279279
$mapDef = $container->getDefinition('security.firewall.map');
280-
$map = $authenticationProviders = $contextRefs = [];
280+
$map = $authenticationProviders = $contextRefs = $authenticators = [];
281281
foreach ($firewalls as $name => $firewall) {
282282
if (isset($firewall['user_checker']) && 'security.user_checker' !== $firewall['user_checker']) {
283283
$customUserChecker = true;
284284
}
285285

286286
$configId = 'security.firewall.map.config.'.$name;
287287

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

290+
if (!$firewallAuthenticators) {
291+
$authenticators[$name] = null;
292+
} else {
293+
$firewallAuthenticatorRefs = [];
294+
foreach ($firewallAuthenticators as $authenticatorId) {
295+
$firewallAuthenticatorRefs[$authenticatorId] = new Reference($authenticatorId);
296+
}
297+
$authenticators[$name] = ServiceLocatorTagPass::register($container, $firewallAuthenticatorRefs);
298+
}
290299
$contextId = 'security.firewall.map.context.'.$name;
291300
$isLazy = !$firewall['stateless'] && (!empty($firewall['anonymous']['lazy']) || $firewall['lazy']);
292301
$context = new ChildDefinition($isLazy ? 'security.firewall.lazy_context' : 'security.firewall.context');
@@ -301,6 +310,10 @@ private function createFirewalls(array $config, ContainerBuilder $container)
301310
$contextRefs[$contextId] = new Reference($contextId);
302311
$map[$contextId] = $matcher;
303312
}
313+
$container
314+
->getDefinition('security.helper')
315+
->replaceArgument(1, $authenticators)
316+
;
304317

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

@@ -335,7 +348,7 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
335348

336349
// Security disabled?
337350
if (false === $firewall['security']) {
338-
return [$matcher, [], null, null];
351+
return [$matcher, [], null, null, []];
339352
}
340353

341354
$config->replaceArgument(4, $firewall['stateless']);
@@ -528,7 +541,7 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
528541
$config->replaceArgument(10, $listenerKeys);
529542
$config->replaceArgument(11, $firewall['switch_user'] ?? null);
530543

531-
return [$matcher, $listeners, $exceptionListener, null !== $logoutListenerId ? new Reference($logoutListenerId) : null];
544+
return [$matcher, $listeners, $exceptionListener, null !== $logoutListenerId ? new Reference($logoutListenerId) : null, $firewallAuthenticationProviders];
532545
}
533546

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

Resources/config/security.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,17 @@
7777
->set('security.untracked_token_storage', TokenStorage::class)
7878

7979
->set('security.helper', Security::class)
80-
->args([service_locator([
81-
'security.token_storage' => service('security.token_storage'),
82-
'security.authorization_checker' => service('security.authorization_checker'),
83-
'security.firewall.map' => service('security.firewall.map'),
84-
])])
80+
->args([
81+
service_locator([
82+
'security.token_storage' => service('security.token_storage'),
83+
'security.authorization_checker' => service('security.authorization_checker'),
84+
'security.user_authenticator' => service('security.user_authenticator')->ignoreOnInvalid(),
85+
'request_stack' => service('request_stack'),
86+
'security.firewall.map' => service('security.firewall.map'),
87+
'security.user_checker' => service('security.user_checker'),
88+
]),
89+
abstract_arg('authenticators'),
90+
])
8591
->alias(Security::class, 'security.helper')
8692
->alias(LegacySecurity::class, 'security.helper')
8793
->deprecate('symfony/security-bundle', '6.2', 'The "%alias_id%" service alias is deprecated, use "'.Security::class.'" instead.')

Security/Security.php

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313

1414
use Psr\Container\ContainerInterface;
1515
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\Security\Core\Exception\LogicException;
1617
use Symfony\Component\Security\Core\Security as LegacySecurity;
18+
use Symfony\Component\Security\Core\User\UserInterface;
19+
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
20+
use Symfony\Contracts\Service\ServiceProviderInterface;
1721

1822
/**
1923
* Helper class for commonly-needed security tasks.
@@ -22,7 +26,7 @@
2226
*/
2327
class Security extends LegacySecurity
2428
{
25-
public function __construct(private ContainerInterface $container)
29+
public function __construct(private ContainerInterface $container, private array $authenticators = [])
2630
{
2731
parent::__construct($container, false);
2832
}
@@ -31,4 +35,58 @@ public function getFirewallConfig(Request $request): ?FirewallConfig
3135
{
3236
return $this->container->get('security.firewall.map')->getFirewallConfig($request);
3337
}
38+
39+
public function login(UserInterface $user, string $authenticatorName = null, string $firewallName = null): void
40+
{
41+
$request = $this->container->get('request_stack')->getCurrentRequest();
42+
43+
if (!class_exists(AuthenticatorInterface::class)) {
44+
throw new \LogicException('Security HTTP is missing. Try running "composer require symfony/security-http".');
45+
}
46+
$authenticator = $this->getAuthenticator($authenticatorName, $firewallName ?? $this->getFirewallName($request));
47+
48+
$this->container->get('security.user_checker')->checkPreAuth($user);
49+
$this->container->get('security.user_authenticator')->authenticateUser($user, $authenticator, $request);
50+
}
51+
52+
private function getAuthenticator(?string $authenticatorName, string $firewallName): AuthenticatorInterface
53+
{
54+
if (!\array_key_exists($firewallName, $this->authenticators)) {
55+
throw new LogicException(sprintf('No authenticators found for firewall "%s".', $firewallName));
56+
}
57+
/** @var ServiceProviderInterface $firewallAuthenticatorLocator */
58+
$firewallAuthenticatorLocator = $this->authenticators[$firewallName];
59+
60+
if (!$authenticatorName) {
61+
$authenticatorIds = array_keys($firewallAuthenticatorLocator->getProvidedServices());
62+
63+
if (!$authenticatorIds) {
64+
throw new LogicException('No authenticator was found for the firewall "%s".');
65+
}
66+
67+
if (1 < \count($authenticatorIds)) {
68+
throw new LogicException(sprintf('Too much authenticators were found for the current firewall "%s". You must provide an instance of "%s" to login programmatically. The available authenticators for the firewall "%s" are "%s".', $firewallName, AuthenticatorInterface::class, $firewallName, implode('" ,"', $authenticatorIds)));
69+
}
70+
71+
return $firewallAuthenticatorLocator->get($authenticatorIds[0]);
72+
}
73+
$authenticatorId = 'security.authenticator.'.$authenticatorName.'.'.$firewallName;
74+
75+
if (!$firewallAuthenticatorLocator->has($authenticatorId)) {
76+
throw new LogicException(sprintf('Unable to find an authenticator named "%s" for the firewall "%s". Try to pass a firewall name in the Security::login() method.', $authenticatorName, $firewallName));
77+
}
78+
79+
return $firewallAuthenticatorLocator->get($authenticatorId);
80+
}
81+
82+
private function getFirewallName(Request $request): string
83+
{
84+
$firewall = $this->container->get('security.firewall.map')->getFirewallConfig($request);
85+
86+
if (null === $firewall) {
87+
throw new LogicException('No firewall found as the current route is not covered by any firewall.');
88+
}
89+
90+
return $firewall->getName();
91+
}
3492
}

0 commit comments

Comments
 (0)