bug #37008 [Security] Fixed AbstractToken::hasUserChanged() (wouterj)

This PR was squashed before being merged into the 4.4 branch.

Discussion
----------

[Security] Fixed AbstractToken::hasUserChanged()

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | Fix #36989
| License       | MIT
| Doc PR        | -

This PR completely reverts #35944.

That PR tried to fix a BC break (ref #35941, #35509) introduced by #31177. However, this broke many authentications (ref #36989), as the User is serialized in the session (as hinted by @stof). Many applications don't include the `roles` property in the serialization (at least, the MakerBundle doesn't include it).

In 5.2, we should probably deprecate having different roles in token and user, which fixes the BC breaks all together.

Commits
-------

f297beb42c [Security] Fixed AbstractToken::hasUserChanged()
This commit is contained in:
Nicolas Grekas 2020-05-30 23:50:18 +02:00
commit bdb01db3dc
2 changed files with 71 additions and 3 deletions

View File

@ -317,10 +317,13 @@ abstract class AbstractToken implements TokenInterface
return true;
}
$currentUserRoles = array_map('strval', (array) $this->user->getRoles());
$userRoles = array_map('strval', (array) $user->getRoles());
if (\count($userRoles) !== \count($currentUserRoles) || \count($userRoles) !== \count(array_intersect($userRoles, $currentUserRoles))) {
if ($this instanceof SwitchUserToken) {
$userRoles[] = 'ROLE_PREVIOUS_ADMIN';
}
if (\count($userRoles) !== \count($this->getRoleNames()) || \count($userRoles) !== \count(array_intersect($userRoles, $this->getRoleNames()))) {
return true;
}

View File

@ -238,7 +238,7 @@ class AbstractTokenTest extends TestCase
*/
public function testSetUserDoesNotSetAuthenticatedToFalseWhenUserDoesNotChange($user)
{
$token = new ConcreteToken(['ROLE_FOO']);
$token = new ConcreteToken();
$token->setAuthenticated(true);
$this->assertTrue($token->isAuthenticated());
@ -248,6 +248,21 @@ class AbstractTokenTest extends TestCase
$token->setUser($user);
$this->assertTrue($token->isAuthenticated());
}
public function testIsUserChangedWhenSerializing()
{
$token = new ConcreteToken(['ROLE_ADMIN']);
$token->setAuthenticated(true);
$this->assertTrue($token->isAuthenticated());
$user = new SerializableUser('wouter', ['ROLE_ADMIN']);
$token->setUser($user);
$this->assertTrue($token->isAuthenticated());
$token = unserialize(serialize($token));
$token->setUser($user);
$this->assertTrue($token->isAuthenticated());
}
}
class TestUser
@ -265,6 +280,56 @@ class TestUser
}
}
class SerializableUser implements UserInterface, \Serializable
{
private $roles;
private $name;
public function __construct($name, array $roles = [])
{
$this->name = $name;
$this->roles = $roles;
}
public function getUsername()
{
return $this->name;
}
public function getPassword()
{
return '***';
}
public function getRoles()
{
if (empty($this->roles)) {
return ['ROLE_USER'];
}
return $this->roles;
}
public function eraseCredentials()
{
}
public function getSalt()
{
return null;
}
public function serialize()
{
return serialize($this->name);
}
public function unserialize($serialized)
{
$this->name = unserialize($serialized);
}
}
class ConcreteToken extends AbstractToken
{
private $credentials = 'credentials_value';