Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0218f2e

Browse 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 10e1a72 commit 0218f2e

File tree

8 files changed

+248
-91
lines changed

8 files changed

+248
-91
lines changed

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,15 +132,21 @@ 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

143-
// create remember me authenticator (which re-authenticates the user based on the remember me cookie)
149+
// create remember me authenticator (which re-authenticates the user based on the remember-me cookie)
144150
$authenticatorId = 'security.authenticator.remember_me.'.$firewallName;
145151
$container
146152
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.remember_me'))

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

Lines 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

Lines 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

Lines 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 <[email protected]>
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
Lines 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 <[email protected]>
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 <[email protected]>
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

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@
2222
use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface;
2323

2424
/**
25-
* The RememberMe *listener* creates and deletes remember me cookies.
25+
* The RememberMe *listener* creates and deletes remember-me cookies.
2626
*
2727
* Upon login success or failure and support for remember me
2828
* in the firewall and authenticator, this listener will create
29-
* a remember me cookie.
30-
* Upon login failure, all remember me cookies are removed.
29+
* a remember-me cookie.
30+
* Upon login failure, all remember-me cookies are removed.
3131
*
3232
* @author Wouter de Jong <[email protected]>
3333
*
@@ -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',
Lines 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 <[email protected]>
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)