Skip to content

Commit c84aeeb

Browse files
wouterjchalasr
authored andcommitted
[Security] Rework the remember me system
1 parent 457218d commit c84aeeb

25 files changed

+1179
-275
lines changed

Authenticator/Passport/Badge/RememberMeBadge.php

Lines changed: 37 additions & 8 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,6 +25,40 @@
3025
*/
3126
class RememberMeBadge implements BadgeInterface
3227
{
28+
private $enabled = false;
29+
30+
/**
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
38+
*/
39+
public function enable(): self
40+
{
41+
$this->enabled = true;
42+
43+
return $this;
44+
}
45+
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
53+
{
54+
$this->enabled = false;
55+
}
56+
57+
public function isEnabled(): bool
58+
{
59+
return $this->enabled;
60+
}
61+
3362
public function isResolved(): bool
3463
{
3564
return true; // remember me does not need to be explicitly resolved

Authenticator/RememberMeAuthenticator.php

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,28 @@
1111

1212
namespace Symfony\Component\Security\Http\Authenticator;
1313

14+
use Psr\Log\LoggerInterface;
1415
use Symfony\Component\HttpFoundation\Request;
1516
use Symfony\Component\HttpFoundation\Response;
1617
use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken;
1718
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
1819
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
1920
use Symfony\Component\Security\Core\Exception\AuthenticationException;
21+
use Symfony\Component\Security\Core\Exception\CookieTheftException;
22+
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
23+
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
2024
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
2125
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
2226
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
23-
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
27+
use Symfony\Component\Security\Http\RememberMe\RememberMeDetails;
28+
use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface;
29+
use Symfony\Component\Security\Http\RememberMe\ResponseListener;
2430

2531
/**
2632
* The RememberMe *Authenticator* performs remember me authentication.
2733
*
2834
* This authenticator is executed whenever a user's session
29-
* expired and a remember me cookie was found. This authenticator
35+
* expired and a remember-me cookie was found. This authenticator
3036
* then "re-authenticates" the user using the information in the
3137
* cookie.
3238
*
@@ -37,17 +43,19 @@
3743
*/
3844
class RememberMeAuthenticator implements InteractiveAuthenticatorInterface
3945
{
40-
private $rememberMeServices;
46+
private $rememberMeHandler;
4147
private $secret;
4248
private $tokenStorage;
43-
private $options = [];
49+
private $cookieName;
50+
private $logger;
4451

45-
public function __construct(RememberMeServicesInterface $rememberMeServices, string $secret, TokenStorageInterface $tokenStorage, array $options)
52+
public function __construct(RememberMeHandlerInterface $rememberMeHandler, string $secret, TokenStorageInterface $tokenStorage, string $cookieName, LoggerInterface $logger = null)
4653
{
47-
$this->rememberMeServices = $rememberMeServices;
54+
$this->rememberMeHandler = $rememberMeHandler;
4855
$this->secret = $secret;
4956
$this->tokenStorage = $tokenStorage;
50-
$this->options = $options;
57+
$this->cookieName = $cookieName;
58+
$this->logger = $logger;
5159
}
5260

5361
public function supports(Request $request): ?bool
@@ -57,33 +65,34 @@ public function supports(Request $request): ?bool
5765
return false;
5866
}
5967

60-
// if the attribute is set, this is a lazy firewall. The previous
61-
// support call already indicated support, so return null and avoid
62-
// recreating the cookie
63-
if ($request->attributes->has('_remember_me_token')) {
64-
return null;
68+
if (($cookie = $request->attributes->get(ResponseListener::COOKIE_ATTR_NAME)) && null === $cookie->getValue()) {
69+
return false;
6570
}
6671

67-
$token = $this->rememberMeServices->autoLogin($request);
68-
if (null === $token) {
72+
if (!$request->cookies->has($this->cookieName)) {
6973
return false;
7074
}
7175

72-
$request->attributes->set('_remember_me_token', $token);
76+
if (null !== $this->logger) {
77+
$this->logger->debug('Remember-me cookie detected.');
78+
}
7379

7480
// the `null` return value indicates that this authenticator supports lazy firewalls
7581
return null;
7682
}
7783

7884
public function authenticate(Request $request): PassportInterface
7985
{
80-
$token = $request->attributes->get('_remember_me_token');
81-
if (null === $token) {
82-
throw new \LogicException('No remember me token is set.');
86+
$rawCookie = $request->cookies->get($this->cookieName);
87+
if (!$rawCookie) {
88+
throw new \LogicException('No remember-me cookie is found.');
8389
}
8490

85-
// @deprecated since 5.3, change to $token->getUserIdentifier() in 6.0
86-
return new SelfValidatingPassport(new UserBadge(method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getUsername(), [$token, 'getUser']));
91+
$rememberMeCookie = RememberMeDetails::fromRawCookie($rawCookie);
92+
93+
return new SelfValidatingPassport(new UserBadge($rememberMeCookie->getUserIdentifier(), function () use ($rememberMeCookie) {
94+
return $this->rememberMeHandler->consumeRememberMeCookie($rememberMeCookie);
95+
}));
8796
}
8897

8998
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
@@ -98,7 +107,15 @@ public function onAuthenticationSuccess(Request $request, TokenInterface $token,
98107

99108
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
100109
{
101-
$this->rememberMeServices->loginFail($request, $exception);
110+
if (null !== $this->logger) {
111+
if ($exception instanceof UsernameNotFoundException) {
112+
$this->logger->info('User for remember-me cookie not found.', ['exception' => $exception]);
113+
} elseif ($exception instanceof UnsupportedUserException) {
114+
$this->logger->warning('User class for remember-me cookie not supported.', ['exception' => $exception]);
115+
} elseif (!$exception instanceof CookieTheftException) {
116+
$this->logger->debug('Remember me authentication failed.', ['exception' => $exception]);
117+
}
118+
}
102119

103120
return null;
104121
}

Event/DeauthenticatedEvent.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
use Symfony\Contracts\EventDispatcher\Event;
1616

1717
/**
18-
* Deauthentication happens in case the user has changed when trying to refresh the token.
18+
* Deauthentication happens in case the user has changed when trying to
19+
* refresh the token.
20+
*
21+
* Use {@see TokenDeauthenticatedEvent} if you want to cover all cases where
22+
* a session is deauthenticated.
1923
*
2024
* @author Hamza Amrouche <[email protected]>
2125
*/

Event/TokenDeauthenticatedEvent.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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\Event;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
16+
use Symfony\Contracts\EventDispatcher\Event;
17+
18+
/**
19+
* This event is dispatched when the current security token is deauthenticated
20+
* when trying to reference the token.
21+
*
22+
* This includes changes in the user ({@see DeauthenticatedEvent}), but
23+
* also cases where there is no user provider available to refresh the user.
24+
*
25+
* Use this event if you want to trigger some actions whenever a user is
26+
* deauthenticated and redirected back to the authentication entry point
27+
* (e.g. clearing all remember-me cookies).
28+
*
29+
* @author Wouter de Jong <[email protected]>
30+
*/
31+
final class TokenDeauthenticatedEvent extends Event
32+
{
33+
private $originalToken;
34+
private $request;
35+
36+
public function __construct(TokenInterface $originalToken, Request $request)
37+
{
38+
$this->originalToken = $originalToken;
39+
$this->request = $request;
40+
}
41+
42+
public function getOriginalToken(): TokenInterface
43+
{
44+
return $this->originalToken;
45+
}
46+
47+
public function getRequest(): Request
48+
{
49+
return $this->request;
50+
}
51+
}
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+
}

0 commit comments

Comments
 (0)