Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 2e591bc

Browse filesBrowse files
committed
[Security] Split checking remember me conditions and creating cookie
This is required for 2fa: Upon username+password login, it must know if remember me was requested & supported, but it has to prevent the cookie from being set (so it can set it when the 2nd factor is completed).
1 parent 233f903 commit 2e591bc
Copy full SHA for 2e591bc

File tree

8 files changed

+244
-87
lines changed
Filter options

8 files changed

+244
-87
lines changed

‎src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php
+7-1Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,17 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal
132132
->addTag('security.remember_me_handler', ['firewall' => $firewallName]);
133133
}
134134

135+
// create check remember me conditions listener (which checks if a remember me cookie is supported and requested)
136+
$rememberMeConditionsListenerId = 'security.listener.check_remember_me_conditions.'.$firewallName;
137+
$container->setDefinition($rememberMeConditionsListenerId, new ChildDefinition('security.listener.check_remember_me_conditions'))
138+
->replaceArgument(0, array_intersect_key($config, ['always_remember_me' => true, 'remember_me_parameter' => true]))
139+
->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$firewallName])
140+
;
141+
135142
// create remember me listener (which executes the remember me services for other authenticators and logout)
136143
$rememberMeListenerId = 'security.listener.remember_me.'.$firewallName;
137144
$container->setDefinition($rememberMeListenerId, new ChildDefinition('security.listener.remember_me'))
138145
->replaceArgument(0, new Reference($rememberMeHandlerId))
139-
->replaceArgument(1, array_intersect_key($config, ['always_remember_me' => true, 'remember_me_parameter' => true]))
140146
->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$firewallName])
141147
;
142148

‎src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_remember_me.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_remember_me.php
+8-1Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Bundle\SecurityBundle\RememberMe\FirewallAwareRememberMeHandler;
1515
use Symfony\Component\Security\Core\Signature\SignatureHasher;
1616
use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticator;
17+
use Symfony\Component\Security\Http\EventListener\CheckRememberMeConditionsListener;
1718
use Symfony\Component\Security\Http\EventListener\RememberMeListener;
1819
use Symfony\Component\Security\Http\RememberMe\PersistentRememberMeHandler;
1920
use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface;
@@ -61,11 +62,17 @@
6162
])
6263
->alias(RememberMeHandlerInterface::class, 'security.authenticator.firewall_aware_remember_me_handler')
6364

65+
->set('security.listener.check_remember_me_conditions', CheckRememberMeConditionsListener::class)
66+
->abstract()
67+
->args([
68+
abstract_arg('options'),
69+
service('logger')->nullOnInvalid(),
70+
])
71+
6472
->set('security.listener.remember_me', RememberMeListener::class)
6573
->abstract()
6674
->args([
6775
abstract_arg('remember me handler'),
68-
abstract_arg('options'),
6976
service('logger')->nullOnInvalid(),
7077
])
7178
->tag('monolog.logger', ['channel' => 'security'])

‎src/Symfony/Component/Security/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
5.3
55
---
66

7+
* Add `RememberMeConditionsListener` to check if remember me is requested and supported, and set priority of `RememberMeListener` to -63
78
* Add `Core\Signature\SignatureHasher` and moved `Http\LoginLink\ExpiredLoginLinkStorage` to `Core\Signature\ExpiredLoginLinkStorage`
89
* Add `RememberMeHandlerInterface` and implementations, used as a replacement of `RememberMeServicesInterface` when using the AuthenticatorManager
910
* Add `TokenDeauthenticatedEvent` that is dispatched when the current security token is deauthenticated

‎src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/RememberMeBadge.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/RememberMeBadge.php
+25-16Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,9 @@
1414
/**
1515
* Adds support for remember me to this authenticator.
1616
*
17-
* Remember me cookie will be set if *all* of the following are met:
18-
* A) This badge is present in the Passport
19-
* B) The remember_me key under your firewall is configured
20-
* C) The "remember me" functionality is activated. This is usually
21-
* done by having a _remember_me checkbox in your form, but
22-
* can be configured by the "always_remember_me" and "remember_me_parameter"
23-
* parameters under the "remember_me" firewall key
24-
* D) The authentication process returns a success Response object
17+
* The presence of this badge doesn't create the remember me cookie. The actual
18+
* cookie is only created if this badge is enabled. By default, this is done
19+
* by the {@see RememberMeConditionsListener} if all conditions are met.
2520
*
2621
* @author Wouter de Jong <wouter@wouterj.nl>
2722
*
@@ -30,24 +25,38 @@
3025
*/
3126
class RememberMeBadge implements BadgeInterface
3227
{
33-
private $useRememberMe;
28+
private $enabled = false;
3429

3530
/**
36-
* @param string|bool|null $saveRememberMe can be used to opt-in/out from remember me (e.g. using a checkbox on the login form)
31+
* Enables remember me cookie creation.
32+
*
33+
* In most cases, {@see RememberMeConditionsListener} enables this
34+
* automatically if always_remember_me is true or the remember_me_parameter
35+
* exists in the request.
36+
*
37+
* @return $this
3738
*/
38-
public function __construct($useRememberMe = null)
39+
public function enable(): self
3940
{
40-
$this->useRememberMe = $useRememberMe;
41+
$this->enabled = true;
42+
43+
return $this;
4144
}
4245

43-
public function setUseRememberMe(bool $useRememberMe)
46+
/**
47+
* Disables remember me cookie creation.
48+
*
49+
* The default is disabled, this can be called to suppress creation
50+
* after it was enabled.
51+
*/
52+
public function disable(): void
4453
{
45-
$this->useRememberMe = $useRememberMe;
54+
$this->enabled = false;
4655
}
4756

48-
public function getUseRememberMe(): ?bool
57+
public function isEnabled(): bool
4958
{
50-
return $this->useRememberMe;
59+
return $this->enabled;
5160
}
5261

5362
public function isResolved(): bool
+79Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Component\Security\Http\EventListener;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
16+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
17+
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
18+
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
19+
use Symfony\Component\Security\Http\Event\LogoutEvent;
20+
use Symfony\Component\Security\Http\Event\TokenDeauthenticatedEvent;
21+
use Symfony\Component\Security\Http\ParameterBagUtils;
22+
use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface;
23+
24+
/**
25+
* Checks if all conditions are met for remember me.
26+
*
27+
* The conditions that must be met for this listener to enable remember me:
28+
* A) This badge is present in the Passport
29+
* B) The remember_me key under your firewall is configured
30+
* C) The "remember me" functionality is activated. This is usually
31+
* done by having a _remember_me checkbox in your form, but
32+
* can be configured by the "always_remember_me" and "remember_me_parameter"
33+
* parameters under the "remember_me" firewall key (or "always_remember_me"
34+
* is enabled)
35+
*
36+
* @author Wouter de Jong <wouter@wouterj.nl>
37+
*
38+
* @final
39+
* @experimental in 5.3
40+
*/
41+
class CheckRememberMeConditionsListener implements EventSubscriberInterface
42+
{
43+
private $options;
44+
private $logger;
45+
46+
public function __construct(array $options = [], ?LoggerInterface $logger = null)
47+
{
48+
$this->options = $options + ['always_remember_me' => false, 'remember_me_parameter' => '_remember_me'];
49+
$this->logger = $logger;
50+
}
51+
52+
public function onSuccessfulLogin(LoginSuccessEvent $event): void
53+
{
54+
$passport = $event->getPassport();
55+
if (!$passport->hasBadge(RememberMeBadge::class)) {
56+
return;
57+
}
58+
59+
/** @var RememberMeBadge $badge */
60+
$badge = $passport->getBadge(RememberMeBadge::class);
61+
if (!$this->options['always_remember_me']) {
62+
$parameter = ParameterBagUtils::getRequestParameterValue($event->getRequest(), $this->options['remember_me_parameter']);
63+
if (!('true' === $parameter || 'on' === $parameter || '1' === $parameter || 'yes' === $parameter || true === $parameter)) {
64+
if (null !== $this->logger) {
65+
$this->logger->debug('Remember me disabled; request does not contain remember me parameter ("{parameter}").', ['parameter' => $this->options['remember_me_parameter']]);
66+
}
67+
68+
return;
69+
}
70+
}
71+
72+
$badge->enable();
73+
}
74+
75+
public static function getSubscribedEvents(): array
76+
{
77+
return [LoginSuccessEvent::class => ['onSuccessfulLogin', -32]];
78+
}
79+
}

‎src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php
+6-19Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,11 @@
3737
class RememberMeListener implements EventSubscriberInterface
3838
{
3939
private $rememberMeHandler;
40-
private $options;
4140
private $logger;
4241

43-
public function __construct(RememberMeHandlerInterface $rememberMeHandler, array $options = [], ?LoggerInterface $logger = null)
42+
public function __construct(RememberMeHandlerInterface $rememberMeHandler, ?LoggerInterface $logger = null)
4443
{
4544
$this->rememberMeHandler = $rememberMeHandler;
46-
$this->options = $options + ['always_remember_me' => false, 'remember_me_parameter' => '_remember_me'];
4745
$this->logger = $logger;
4846
}
4947

@@ -63,23 +61,12 @@ public function onSuccessfulLogin(LoginSuccessEvent $event): void
6361

6462
/** @var RememberMeBadge $badge */
6563
$badge = $passport->getBadge(RememberMeBadge::class);
66-
if (!$this->options['always_remember_me']) {
67-
if (false === $badge->getUseRememberMe()) {
68-
if (null !== $this->logger) {
69-
$this->logger->debug('Did not send remember-me cookie; remember-me was opted-out by the first argument of RememberMeBadge.');
70-
}
71-
72-
return;
64+
if (!$badge->isEnabled()) {
65+
if (null !== $this->logger) {
66+
$this->logger->debug('Remember me skipped: the RememberMeBadge is not enabled.');
7367
}
7468

75-
$parameter = ParameterBagUtils::getRequestParameterValue($event->getRequest(), $this->options['remember_me_parameter']);
76-
if (!('true' === $parameter || 'on' === $parameter || '1' === $parameter || 'yes' === $parameter || true === $parameter)) {
77-
if (null !== $this->logger) {
78-
$this->logger->debug('Did not send remember-me cookie; request does not contain remember me parameter ("{parameter}").', ['parameter' => $this->options['remember_me_parameter']]);
79-
}
80-
81-
return;
82-
}
69+
return;
8370
}
8471

8572
if (null !== $this->logger) {
@@ -97,7 +84,7 @@ public function clearCookie(): void
9784
public static function getSubscribedEvents(): array
9885
{
9986
return [
100-
LoginSuccessEvent::class => 'onSuccessfulLogin',
87+
LoginSuccessEvent::class => ['onSuccessfulLogin', -64],
10188
LoginFailureEvent::class => 'clearCookie',
10289
LogoutEvent::class => 'clearCookie',
10390
TokenDeauthenticatedEvent::class => 'clearCookie',
+101Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Component\Security\Http\Tests\EventListener;
13+
14+
use PHPUnit\Framework\TestCase;
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\User\User;
19+
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
20+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
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+
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
25+
use Symfony\Component\Security\Http\EventListener\CheckRememberMeConditionsListener;
26+
27+
class CheckRememberMeConditionsListenerTest extends TestCase
28+
{
29+
private $listener;
30+
private $request;
31+
private $response;
32+
33+
protected function setUp(): void
34+
{
35+
$this->listener = new CheckRememberMeConditionsListener();
36+
$this->request = Request::create('/login');
37+
$this->request->request->set('_remember_me', true);
38+
$this->response = new Response();
39+
}
40+
41+
public function testSuccessfulLoginWithoutSupportingAuthenticator()
42+
{
43+
$passport = $this->createPassport([]);
44+
45+
$this->listener->onSuccessfulLogin($this->createLoginSuccessfulEvent($passport));
46+
47+
$this->assertFalse($passport->hasBadge(RememberMeBadge::class));
48+
}
49+
50+
public function testSuccessfulLoginWithoutRequestParameter()
51+
{
52+
$this->request = Request::create('/login');
53+
$passport = $this->createPassport();
54+
55+
$this->listener->onSuccessfulLogin($this->createLoginSuccessfulEvent($passport));
56+
57+
$this->assertFalse($passport->getBadge(RememberMeBadge::class)->isEnabled());
58+
}
59+
60+
public function testSuccessfulLoginWhenRememberMeAlwaysIsTrue()
61+
{
62+
$passport = $this->createPassport();
63+
$listener = new CheckRememberMeConditionsListener(['always_remember_me' => true]);
64+
65+
$this->listener->onSuccessfulLogin($this->createLoginSuccessfulEvent($passport));
66+
67+
$this->assertTrue($passport->getBadge(RememberMeBadge::class)->isEnabled());
68+
}
69+
70+
/**
71+
* @dataProvider provideRememberMeOptInValues
72+
*/
73+
public function testSuccessfulLoginWithOptInRequestParameter($optInValue)
74+
{
75+
$this->request->request->set('_remember_me', $optInValue);
76+
$passport = $this->createPassport();
77+
78+
$this->listener->onSuccessfulLogin($this->createLoginSuccessfulEvent($passport));
79+
80+
$this->assertTrue($passport->getBadge(RememberMeBadge::class)->isEnabled());
81+
}
82+
83+
public function provideRememberMeOptInValues()
84+
{
85+
yield ['true'];
86+
yield ['1'];
87+
yield ['on'];
88+
yield ['yes'];
89+
yield [true];
90+
}
91+
92+
private function createLoginSuccessfulEvent(PassportInterface $passport)
93+
{
94+
return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $passport, $this->createMock(TokenInterface::class), $this->request, $this->response, 'main_firewall');
95+
}
96+
97+
private function createPassport(array $badges = null)
98+
{
99+
return new SelfValidatingPassport(new UserBadge('test', function ($username) { return new User($username, null); }), $badges ?? [new RememberMeBadge()]);
100+
}
101+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.