Skip to content

Commit c3061f0

Browse files
Merge branch '8.0' into 8.1
* 8.0: (49 commits) [Runtime][FrameworkBundle] Trust argv on CLI-like SAPIs in the remaining QUERY_STRING gates Fix tests and merge resolution after merging 6.4 into 7.4 [FrameworkBundle] Allow to pass `doctrine_open_transaction_logger`’s entity manager name positionally Remove protectedHeaderOnly from claim checkers [String][Mime] Reject objects in typed-string properties during __unserialize [Cache] Fix strlen(null) deprecation on RelayCluster path in RedisTrait::doClear() [Scheduler] Recover pending RecurringMessages after consumer stops midway [SecurityBundle] Fix Security::login() across firewalls [Routing][RateLimiter][Mime][Security] Harden __unserialize against __toString trampolines [Security] Initialize lazy users before serializing them in the session [Process] Stop leaking CGI/FastCGI request-context vars to subprocesses [Runtime] Trust argv on CLI-like SAPIs to fix subprocess args [Mailer] Harden default IP allowlist for Postmark and Brevo webhook parsers [Cache] Accept '_' and ':' in prefix passed to AbstractAdapter::clear() [HtmlSanitizer] Honor universal attribute sanitizers, apply maxInputLength to text contexts, document forceAttribute and allowAttribute caveats [Mailer][Notifier] Harden Mailchimp signature comparison and Smsbox IP allowlist [Translation] Don’t check the error message to know if Lokalise keys are missing [AssetMapper] Rewrite relative paths in `export ... from` statements [Yaml] Allow trailing newlines after the end-of-document marker [HttpKernel][WebProfilerBundle] Check logs priority name for both `WARNING` and `warning` ... # Conflicts: # src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig # src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php # src/Symfony/Component/HttpClient/CachingHttpClient.php # src/Symfony/Component/HttpClient/Tests/CachingHttpClientTest.php # src/Symfony/Component/HttpKernel/Kernel.php
2 parents ad6f847 + 641934c commit c3061f0

4 files changed

Lines changed: 78 additions & 5 deletions

File tree

DependencyInjection/SecurityExtension.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ private function createFirewalls(array $config, ContainerBuilder $container): vo
305305

306306
// load firewall map
307307
$mapDef = $container->getDefinition('security.firewall.map');
308-
$map = $authenticationProviders = $contextRefs = $authenticators = [];
308+
$map = $authenticationProviders = $contextRefs = $authenticators = $firewallConfigRefs = [];
309309
foreach ($firewalls as $name => $firewall) {
310310
if (isset($firewall['user_checker']) && 'security.user_checker' !== $firewall['user_checker']) {
311311
$customUserChecker = true;
@@ -337,13 +337,15 @@ private function createFirewalls(array $config, ContainerBuilder $container): vo
337337

338338
$contextRefs[$contextId] = new Reference($contextId);
339339
$map[$contextId] = $matcher;
340+
$firewallConfigRefs[$name] = new Reference($configId);
340341
}
341342
$container
342343
->getDefinition('security.helper')
343344
->replaceArgument(1, $authenticators)
344345
;
345346

346347
$container->setAlias('security.firewall.context_locator', (string) ServiceLocatorTagPass::register($container, $contextRefs));
348+
$container->setAlias('security.firewall_config_locator', (string) ServiceLocatorTagPass::register($container, $firewallConfigRefs));
347349

348350
$mapDef->replaceArgument(0, new Reference('security.firewall.context_locator'));
349351
$mapDef->replaceArgument(1, new IteratorArgument($map));

Resources/config/security.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
'security.user_checker_locator' => service('security.user_checker_locator'),
9696
'security.firewall.event_dispatcher_locator' => service('security.firewall.event_dispatcher_locator'),
9797
'security.csrf.token_manager' => service('security.csrf.token_manager')->ignoreOnInvalid(),
98+
'security.firewall_config_locator' => service('security.firewall_config_locator')->ignoreOnInvalid(),
9899
]),
99100
abstract_arg('authenticators'),
100101
])

Security.php

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313

1414
use Psr\Container\ContainerInterface;
1515
use Symfony\Bundle\SecurityBundle\Security\FirewallConfig;
16+
use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
1617
use Symfony\Component\HttpFoundation\Request;
1718
use Symfony\Component\HttpFoundation\Response;
19+
use Symfony\Component\HttpFoundation\Session\SessionInterface;
1820
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
1921
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
2022
use Symfony\Component\Security\Core\Authorization\AccessDecision;
@@ -118,9 +120,9 @@ public function login(UserInterface $user, ?string $authenticatorName = null, ?s
118120
throw new LogicException('Unable to login without a request context.');
119121
}
120122

121-
$firewallName ??= $this->getFirewallConfig($request)?->getName();
123+
$currentFirewallConfig = $this->getFirewallConfig($request);
122124

123-
if (!$firewallName) {
125+
if (!$firewallName ??= $currentFirewallConfig?->getName()) {
124126
throw new LogicException('Unable to login as the current route is not covered by any firewall.');
125127
}
126128

@@ -129,7 +131,59 @@ public function login(UserInterface $user, ?string $authenticatorName = null, ?s
129131
$userCheckerLocator = $this->container->get('security.user_checker_locator');
130132
$userCheckerLocator->get($firewallName)->checkPreAuth($user);
131133

132-
return $this->container->get('security.authenticator.managers_locator')->get($firewallName)->authenticateUser($user, $authenticator, $request, $badges, $attributes);
134+
$response = $this->container->get('security.authenticator.managers_locator')->get($firewallName)->authenticateUser($user, $authenticator, $request, $badges, $attributes);
135+
136+
if ($currentFirewallConfig && $firewallName !== $currentFirewallConfig->getName()) {
137+
$this->persistTokenInTargetFirewall($request, $firewallName);
138+
}
139+
140+
return $response;
141+
}
142+
143+
/**
144+
* Persists the freshly minted token under the target firewall's session key
145+
* and prevents the current firewall's ContextListener from overwriting its
146+
* own session bucket with a token that belongs to another firewall.
147+
*/
148+
private function persistTokenInTargetFirewall(Request $request, string $firewallName): void
149+
{
150+
$token = $this->container->get('security.token_storage')->getToken();
151+
if (null === $token) {
152+
return;
153+
}
154+
155+
$targetConfig = $this->getNamedFirewallConfig($firewallName);
156+
if (null === $targetConfig || $targetConfig->isStateless()) {
157+
return;
158+
}
159+
160+
if (null === $session = $this->getSessionForWrite($request)) {
161+
return;
162+
}
163+
164+
$session->set('_security_'.$targetConfig->getContext(), serialize($token));
165+
166+
$request->attributes->remove('_security_firewall_run');
167+
}
168+
169+
private function getNamedFirewallConfig(string $firewallName): ?FirewallConfig
170+
{
171+
if (!$this->container->has('security.firewall_config_locator')) {
172+
return null;
173+
}
174+
175+
$locator = $this->container->get('security.firewall_config_locator');
176+
177+
return $locator->has($firewallName) ? $locator->get($firewallName) : null;
178+
}
179+
180+
private function getSessionForWrite(Request $request): ?SessionInterface
181+
{
182+
try {
183+
return $request->getSession();
184+
} catch (SessionNotFoundException) {
185+
return null;
186+
}
133187
}
134188

135189
/**

Tests/Functional/SecurityTest.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,21 @@ public function testLogin(string $authenticator)
124124
$this->assertSame('chalasr', static::getContainer()->get('security.helper')->getUser()->getUserIdentifier());
125125
}
126126

127+
public function testLoginBetweenStatefulFirewalls()
128+
{
129+
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml']);
130+
$client->loginUser(new InMemoryUser('no-role-username', 'the-password'), 'main');
131+
132+
static::getContainer()->get(ForceLoginController::class)->firewallName = 'second';
133+
$client->request('GET', '/main/force-login');
134+
135+
$client->request('GET', '/second/logged-in');
136+
$this->assertSame(['message' => 'Welcome back @chalasr'], json_decode($client->getResponse()->getContent(), true));
137+
138+
$client->request('GET', '/main/logged-in');
139+
$this->assertSame(['message' => 'Welcome back @no-role-username'], json_decode($client->getResponse()->getContent(), true));
140+
}
141+
127142
public function testLogout()
128143
{
129144
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml', 'debug' => true]);
@@ -258,6 +273,7 @@ public function eraseCredentials(): void
258273
class ForceLoginController
259274
{
260275
public string $authenticator = 'form_login';
276+
public ?string $firewallName = null;
261277

262278
public function __construct(private Security $security)
263279
{
@@ -266,7 +282,7 @@ public function __construct(private Security $security)
266282
public function welcome()
267283
{
268284
$user = new InMemoryUser('chalasr', 'the-password', ['ROLE_FOO']);
269-
$this->security->login($user, $this->authenticator);
285+
$this->security->login($user, $this->authenticator, $this->firewallName);
270286

271287
return new JsonResponse(['message' => \sprintf('Welcome @%s!', $this->security->getUser()->getUserIdentifier())]);
272288
}

0 commit comments

Comments
 (0)