8000 [Security] Support hashing the hashed password using xxh3 when puttin… · symfony/symfony@e45fa9e · GitHub
[go: up one dir, main page]

Skip to content

Commit e45fa9e

Browse files
[Security] Support hashing the hashed password using xxh3 when putting the user in the session
1 parent 84d0b6a commit e45fa9e

File tree

4 files changed

+57
-5
lines changed

4 files changed

+57
-5
lines changed

src/Symfony/Component/Security/Core/User/PasswordAuthenticatedUserInterface.php

+18
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,24 @@
1414
/**
1515
* For users that can be authenticated using a password.
1616
*
17+
* The __serialize/__unserialize() magic methods can be used on the user class to prevent the password hash from being
18+
* stored in the session. If the password is not stored at all in the session, getPassword() should return null after
19+
* unserialization, and then, changing the user's password won't invalidate its sessions.
20+
* In order to invalidate the user sessions while not storing the password hash in the session, it's also possible to
21+
* hash the password hash before serializing it; xxh3 is the only algorithm supported. For example:
22+
*
23+
* public function __serialize(): array
24+
* {
25+
* return [$this->username, hash('xxh3', $this->password)];
26+
* }
27+
*
28+
* public function __unserialize(array $data): void
29+
* {
30+
* [$this->username, $this->password] = $data;
31+
* }
32+
*
33+
* Implement EquatableInteface if you need another logic.
34+
*
1735
* @author Robin Chalas <robin.chalas@gmail.com>
1836
* @author Wouter de Jong <wouter@wouterj.nl>
1937
*/

src/Symfony/Component/Security/Http/Firewall/ContextListener.php

+9-4
Original file line numberDiff line numberDiff line change
@@ -288,18 +288,23 @@ private static function hasUserChanged(UserInterface $originalUser, TokenInterfa
288288
}
289289

290290
if ($originalUser instanceof PasswordAuthenticatedUserInterface || $refreshedUser instanceof PasswordAuthenticatedUserInterface) {
291-
if (!$originalUser instanceof PasswordAuthenticatedUserInterface
292-
|| !$refreshedUser instanceof PasswordAuthenticatedUserInterface
293-
|| $refreshedUser->getPassword() !== ($originalUser->getPassword() ?? $refreshedUser->getPassword())
291+
if (!$originalUser instanceof PasswordAuthenticatedUserInterface || !$refreshedUser instanceof PasswordAuthenticatedUserInterface) {
292+
return true;
293+
}
294+
295+
if (null !== ($originalPassword = $originalUser->getPassword())
296+
&& ($refreshedPassword = $refreshedUser->getPassword()) !== $originalPassword
297+
&& (16 !== \strlen($originalPassword) || hash('xxh3', $refreshedPassword) !== $originalPassword)
294298
) {
295299
return true;
296300
}
297301

302+
298303
if ($originalUser instanceof LegacyPasswordAuthenticatedUserInterface xor $refreshedUser instanceof LegacyPasswordAuthenticatedUserInterface) {
299304
return true;
300305
}
301306

302-
if ($originalUser instanceof LegacyPasswordAuthenticatedUserInterface && $refreshedUser instanceof LegacyPasswordAuthenticatedUserInterface && $originalUser->getSalt() !== $refreshedUser->getSalt()) {
307+
if ($originalUser instanceof LegacyPasswordAuthenticatedUserInterface && $originalUser->getSalt() !== $refreshedUser->getSalt()) {
303308
return true;
304309
}
305310
}

src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php

+19
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,25 @@ public function testRemovingPasswordFromSessionDoesntInvalidateTheToken()
371371
$this->assertSame($user, $tokenStorage->getToken()->getUser());
372372
}
373373

374+
public function testHashingPasswordInSession()
375+
{
376+
$user = new CustomUser('user', ['ROLE_USER'], 'pass', true);
377+
378+
$userProvider = $this->createMock(UserProviderInterface::class);
379+
$userProvider->expects($this->once())
380+
->method('supportsClass')
381+
->with(CustomUser::class)
382+
->willReturn(true);
383+
$userProvider->expects($this->once())
384+
->method('refreshUser')
385+
->willReturn($user);
386+
387+
$tokenStorage = $this->handleEventWithPreviousSession([$userProvider], $user);
388+
389+
$this->assertInstanceOf(UsernamePasswordToken::class, $tokenStorage->getToken());
390+
$this->assertSame($user, $tokenStorage->getToken()->getUser());
391+
}
392+
374393
protected function runSessionOnKernelResponse($newToken, $original = null)
375394
{
376395
$session = new Session(new MockArraySessionStorage());

src/Symfony/Component/Security/Http/Tests/Fixtures/CustomUser.php

+11-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public function __construct(
2020
private string $username,
2121
private array $roles,
2222
private ?string $password = null,
23+
private bool $hashPassword = false,
2324
) {
2425
}
2526

@@ -44,6 +45,15 @@ public function eraseCredentials(): void
4445

4546
public function __serialize(): array
4647
{
47-
return [\sprintf("\0%s\0username", self::class) => $this->username];
48+
$data = (array) $this;
49+
$passwordKey = \sprintf("\0%s\0password", self::class);
50+
51+
if ($this->hashPassword) {
52+
$data[$passwordKey] = hash('xxh3', $this->password);
53+
} else {
54+
unset($data[$passwordKey]);
55+
}
56+
57+
return $data;
4858
}
4959
}

0 commit comments

Comments
 (0)
0