Skip to content

[Security] Allow RememberMeHandler to use a custom RememberMeDetails class #44459

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: 7.4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions UPGRADE-6.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ Validator
---------

* Deprecate the "loose" e-mail validation mode, use "html5" instead

Security
--------

* Deprecate `RememberMeHandler` not implementing `getUserIdentifierForCookie()`; `AbstractRememberMeHandler` has a default implementation
* Deprecate `RememberMeHandler` not implementing `getRememberMeDetails()`; `AbstractRememberMeHandler` has a default implementation
8 changes: 8 additions & 0 deletions UPGRADE-7.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
UPGRADE FROM 6.4 to 7.0
=======================

Security
--------

* Add `getRememberMeDetails()` to `RememberMeHandlerInterface`.
* Add `getUserIdentifierForCookie()` to `RememberMeHandlerInterface`.
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ CHANGELOG
---

* Add the `Security` helper class
* Add the `getUserIdentifierForCookie()` method to the `Symfony\Bundle\SecurityBundle\RememberMe\DecoratedRememberMeHandler`
and `Symfony\Bundle\SecurityBundle\RememberMe\FirewallAwareRememberMeHandler` classes

6.1
---
Expand Down
8 changes: 8 additions & 0 deletions src/Symfony/Bundle/SecurityBundle/RememberMe/DecoratedRememberMeHandler.php
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ public function consumeRememberMeCookie(RememberMeDetails $rememberMeDetails): U
return $this->handler->consumeRememberMeCookie($rememberMeDetails);
}

/**
* {@inheritDoc}
*/
public function getUserIdentifierForCookie(RememberMeDetails $rememberMeDetails): string
{
return $this->handler->getUserIdentifierForCookie($rememberMeDetails);
}

/**
* {@inheritDoc}
*/
Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Bundle/SecurityBundle/RememberMe/FirewallAwareRememberMeHandler.php
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,9 @@ public function clearRememberMeCookie(): void
{
$this->getForFirewall()->clearRememberMeCookie();
}

public function getUserIdentifierForCookie(RememberMeDetails $rememberMeDetails): string
{
return $this->getForFirewall()->getUserIdentifierForCookie($rememberMeDetails);
}
}
15 changes: 13 additions & 2 deletions src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @author Wouter de Jong <wouter@wouterj.nl>
* @author Patrick Elshof <tyrelcher@protonmail.com>
*
* @final
*/
Expand Down Expand Up @@ -86,9 +87,19 @@ public function authenticate(Request $request): Passport
throw new \LogicException('No remember-me cookie is found.');
}

$rememberMeCookie = RememberMeDetails::fromRawCookie($rawCookie);
if (!\is_callable([$this->rememberMeHandler, 'getRememberMeDetails'])) {
$rememberMeCookie = RememberMeDetails::fromRawCookie($rawCookie);
} else {
$rememberMeCookie = $this->rememberMeHandler->getRememberMeDetails($rawCookie);
}

if (!\is_callable([$this->rememberMeHandler, 'getUserIdentifierForCookie'])) {
$userIdentifier = $rememberMeCookie->getUserIdentifier();
} else {
$userIdentifier = $this->rememberMeHandler->getUserIdentifierForCookie($rememberMeCookie);
}

return new SelfValidatingPassport(new UserBadge($rememberMeCookie->getUserIdentifier(), function () use ($rememberMeCookie) {
return new SelfValidatingPassport(new UserBadge($userIdentifier, function () use ($rememberMeCookie) {
return $this->rememberMeHandler->consumeRememberMeCookie($rememberMeCookie);
}));
}
Expand Down
8 changes: 8 additions & 0 deletions src/Symfony/Component/Security/Http/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
CHANGELOG
=========

6.2
---

* Deprecate `RememberMeHandler` not implementing `getUserIdentifierForCookie()`; `AbstractRememberMeHandler` has a default implementation
* Deprecate `RememberMeHandler` not implementing `getRememberMeDetails()`; `AbstractRememberMeHandler` has a default implementation
* Add `getRememberMeDetails()` method to `Symfony\Component\Security\Http\RememberMe\AbstractRememberMeHandler`
* Add `getUserIdentifierForCookie()` method to `Symfony\Component\Security\Http\RememberMe\AbstractRememberMeHandler`

6.0
---

Expand Down
30 changes: 27 additions & 3 deletions src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeHandler.php
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@

/**
* @author Wouter de Jong <wouter@wouterj.nl>
* @author Patrick Elshof <tyrelcher@protonmail.com>
*/
abstract class AbstractRememberMeHandler implements RememberMeHandlerInterface
{
private UserProviderInterface $userProvider;
protected $requestStack;
protected $options;
protected $logger;
protected RequestStack $requestStack;
protected array $options;
protected ?LoggerInterface $logger;

public function __construct(UserProviderInterface $userProvider, RequestStack $requestStack, array $options = [], LoggerInterface $logger = null)
{
Expand Down Expand Up @@ -89,6 +90,29 @@ public function clearRememberMeCookie(): void
$this->createCookie(null);
}

/**
* Retrieves the User Identifier for the RememberMe cookie.
*
* If the cookie is required to contain the User Identifier {@see UserInterface::getUserIdentifier()}
* then it can simply be retrieved from the provided cookie details; if not, then it could be retrieved
* from elsewhere based on some other information provided (e.g. in a database).
*/
public function getUserIdentifierForCookie(RememberMeDetails $rememberMeDetails): string
{
return $rememberMeDetails->getUserIdentifier();
}

/**
* Retrieves the RememberMeDetails using the raw cookie.
*
* This method allows the authenticator to retrieve the cookie details without needing
* to care about the implementation details used by the RememberMeHandler.
*/
public function getRememberMeDetails(string $rawCookie): RememberMeDetails
{
return RememberMeDetails::fromRawCookie($rawCookie);
}

/**
* Creates the remember-me cookie using the correct configuration.
*
Expand Down
4 changes: 4 additions & 0 deletions src/Symfony/Component/Security/Http/RememberMe/RememberMeHandlerInterface.php
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
* {@see AbstractRememberMeHandler} instead.
*
* @author Wouter de Jong <wouter@wouterj.nl>
* @author Patrick Elshof <tyrelcher@protonmail.com>
*
* @method string getUserIdentifierForCookie(RememberMeDetails $rememberMeDetails) Retrieves the User Identifier for the RememberMe cookie.
* @method RememberMeDetails getRememberMeDetails(string $rawCookie) Retrieves the RememberMeDetails using the raw cookie.
*/
interface RememberMeHandlerInterface
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ class RememberMeAuthenticatorTest extends TestCase

protected function setUp(): void
{
$this->rememberMeHandler = $this->createMock(RememberMeHandlerInterface::class);
$this->rememberMeHandler = $this->getMockBuilder(RememberMeHandlerInterface::class)
->onlyMethods(get_class_methods(RememberMeHandlerInterface::class))
->addMethods(['getRememberMeDetails', 'getUserIdentifierForCookie'])
->getMock();

$this->tokenStorage = new TokenStorage();
$this->authenticator = new RememberMeAuthenticator($this->rememberMeHandler, 's3cr3t', $this->tokenStorage, '_remember_me_cookie');
}
Expand Down Expand Up @@ -67,6 +71,15 @@ public function testAuthenticate()
{
$rememberMeDetails = new RememberMeDetails(InMemoryUser::class, 'wouter', 1, 'secret');
$request = Request::create('/', 'GET', [], ['_remember_me_cookie' => $rememberMeDetails->toString()]);

$this->rememberMeHandler->expects($this->once())->method('getRememberMeDetails')
->with($rememberMeDetails->toString())
->willReturn($rememberMeDetails);

$this->rememberMeHandler->expects($this->once())->method('getUserIdentifierForCookie')
->with($rememberMeDetails)
->willReturn('wouter');

$passport = $this->authenticator->authenticate($request);

$this->rememberMeHandler->expects($this->once())->method('consumeRememberMeCookie')->with($this->callback(function ($arg) use ($rememberMeDetails) {
Expand All @@ -86,7 +99,46 @@ public function testAuthenticateWithoutOldToken()
{
$this->expectException(AuthenticationException::class);

$request = Request::create('/', 'GET', [], ['_remember_me_cookie' => base64_encode('foo:bar')]);
$encodedData = base64_encode('foo:bar');
$request = Request::create('/', 'GET', [], ['_remember_me_cookie' => $encodedData]);

$this->rememberMeHandler->expects($this->once())->method('getRememberMeDetails')->with($encodedData)->willThrowException(new AuthenticationException());
$this->authenticator->authenticate($request);
}

/**
* @group legacy
*/
public function testAuthenticateDeprecatedCodePath()
{
$mock = $this->getMockBuilder(RememberMeHandlerInterface::class)
->getMock();

$rememberMeDetails = new RememberMeDetails(InMemoryUser::class, 'wouter', 1, 'secret');
$request = Request::create('/', 'GET', [], ['_remember_me_cookie' => $rememberMeDetails->toString()]);

$authenticator = new RememberMeAuthenticator($mock, 's3cr3t', $this->tokenStorage, '_remember_me_cookie');
$passport = $authenticator->authenticate($request);

$mock->expects($this->once())->method('consumeRememberMeCookie')->with($this->callback(function ($arg) use ($rememberMeDetails) {
return $rememberMeDetails == $arg;
}));
$passport->getUser(); // trigger the user loader
}

/**
* @group legacy
*/
public function testAuthenticateWithoutOldTokenDeprecatedCodePath()
{
$mock = $this->getMockBuilder(RememberMeHandlerInterface::class)
->getMock();

$this->expectException(AuthenticationException::class);

$request = Request::create('/', 'GET', [], ['_remember_me_cookie' => base64_encode('foo:bar')]);

$authenticator = new RememberMeAuthenticator($mock, 's3cr3t', $this->tokenStorage, '_remember_me_cookie');
$authenticator->authenticate($request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,16 @@ public function testConsumeRememberMeCookieExpired()

$this->handler->consumeRememberMeCookie(new RememberMeDetails(InMemoryUser::class, 'wouter', 360, 'series1:tokenvalue'));
}

public function testGetUserIdentifierForCookie()
{
$this->assertEquals('wouter', $this->handler->getUserIdentifierForCookie(new RememberMeDetails(InMemoryUser::class, 'wouter', 360, 'signature')));
}

public function testGetRememberMeDetails()
{
$details = new RememberMeDetails(InMemoryUser::class, 'wouter', 360, 'signature');

$this->assertEquals($details, $this->handler->getRememberMeDetails($details->toString()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,16 @@ public function testConsumeRememberMeCookieExpired()

$this->handler->consumeRememberMeCookie(new RememberMeDetails(InMemoryUser::class, 'wouter', 360, 'signature'));
}

public function testGetUserIdentifierForCookie()
{
$this->assertEquals('wouter', $this->handler->getUserIdentifierForCookie(new RememberMeDetails(InMemoryUser::class, 'wouter', 360, 'signature')));
}

public function testGetRememberMeDetails()
{
$details = new RememberMeDetails(InMemoryUser::class, 'wouter', 360, 'signature');

$this->assertEquals($details, $this->handler->getRememberMeDetails($details->toString()));
}
}