10000 [Security] Support hashing the hashed password using crc32c when putting the user in the session by nicolas-grekas · Pull Request #59562 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Security] Support hashing the hashed password using crc32c when putting the user in the session #59562

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

Merged
merged 1 commit into from
Feb 10, 2025
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,26 @@
/**
* For users that can be authenticated using a password.
*
* The __serialize/__unserialize() magic methods can be implemented on the user
* class to prevent hashed passwords from being put in the session storage.
* If the password is not stored at all in the session, getPassword() should
* return null after unserialization, and then, changing the user's password
* won't invalidate its sessions.
* In order to invalidate the user sessions while not storing the password hash
* in the session, it's also possible to hash the password hash before
* serializing it; crc32c is the only algorithm supported.
* For example:
*
* public function __serialize(): array
* {
* $data = (array) $this;
* $data["\0".self::class."\0password"] = hash('crc32c', $this->password);
*
* return $data;
* }
*
* Implement EquatableInteface if you need another logic.
*
* @author Robin Chalas <robin.chalas@gmail.com>
* @author Wouter de Jong <wouter@wouterj.nl>
*/
Expand All @@ -23,9 +43,6 @@ interface PasswordAuthenticatedUserInterface
* Returns the hashed password used to authenticate the user.
*
* Usually on authentication, a plain-text password will be compared to this value.
*
* The __serialize/__unserialize() magic methods can be implemented on the user
* class to prevent hashed passwords from being put in the session storage.
*/
public function getPassword(): ?string;
}
1 change: 1 addition & 0 deletions src/Symfony/Component/Security/Http/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ CHANGELOG
* Add encryption support to `OidcTokenHandler` (JWE)
* Replace `$hideAccountStatusExceptions` argument with `$exposeSecurityErrors` in `AuthenticatorManager` constructor
* Add argument `$identifierNormalizer` to `UserBadge::__construct()` to allow normalizing the identifier
* Support hashing the hashed password using crc32c when putting the user in the session

7.2
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,9 +292,16 @@
}

if ($originalUser instanceof PasswordAuthenticatedUserInterface || $refreshedUser instanceof PasswordAuthenticatedUserInterface) {
if (!$originalUser instanceof PasswordAuthenticatedUserInterface
|| !$refreshedUser instanceof PasswordAuthenticatedUserInterface
|| $refreshedUser->getPassword() !== ($originalUser->getPassword() ?? $refreshedUser->getPassword())
if (!$originalUser instanceof PasswordAuthenticatedUserInterface || !$refreshedUser instanceof PasswordAuthenticatedUserInterface) {
return true;
}

$originalPassword = $originalUser->getPassword();
$refreshedPassword = $refreshedUser->getPassword();

if (null !== $originalPassword
&& $refreshedPassword !== $originalPassword
&& (8 !== \strlen($originalPassword) || hash('crc32c', $refreshedPassword ?? $originalPassword) !== $originalPassword)
) {
return true;
}
Expand All @@ -303,7 +310,7 @@
return true;
}

if ($originalUser instanceof LegacyPasswordAuthenticatedUserInterface && $refreshedUser instanceof LegacyPasswordAuthenticatedUserInterface && $originalUser->getSalt() !== $refreshedUser->getSalt()) {
if ($originalUser instanceof LegacyPasswordAuthenticatedUserInterface && $originalUser->getSalt() !== $refreshedUser->getSalt()) {

Check failure on line 313 in src/Symfony/Component/Security/Http/Firewall/ContextListener.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedInterfaceMethod

src/Symfony/Component/Security/Http/Firewall/ContextListener.php:313:131: UndefinedInterfaceMethod: Method (Symfony\Component\Security\Core\User\UserInterface&Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface)::getSalt does not exist (see https://psalm.dev/181)

Check failure on line 313 in src/Symfony/Component/Security/Http/Firewall/ContextListener.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedInterfaceMethod

src/Symfony/Component/Security/Http/Firewall/ContextListener.php:313:131: UndefinedInterfaceMethod: Method (Symfony\Component\Security\Core\User\UserInterface&Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface)::getSalt does not exist (see https://psalm.dev/181)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The psalm false-positive is reported vimeo/psalm#9956

return true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,9 +377,14 @@ public function testOnKernelResponseRemoveListener()
$this->assertEmpty($dispatcher->getListeners());
}

public function testRemovingPasswordFromSessionDoesntInvalidateTheToken()
/**
* @testWith [true]
* [false]
* [null]
*/
public function testNullOrHashedPasswordInSessionDoesntInvalidateTheToken(?bool $hashPassword)
{
$user = new CustomUser('user', ['ROLE_USER'], 'pass');
$user = new CustomUser('user', ['ROLE_USER'], 'pass', $hashPassword);

$userProvider = $this->createMock(UserProviderInterface::class);
$userProvider->expects($this->once())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ final class CustomUser implements UserInterface, PasswordAuthenticatedUserInterf
public function __construct(
private string $username,
private array $roles,
private ?string $password = null,
private ?string $password,
private ?bool $hashPassword,
) {
}

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

public function __serialize(): array
{
return [\sprintf("\0%s\0username", self::class) => $this->username];
$data = (array) $this;
$passwordKey = \sprintf("\0%s\0password", self::class);

if ($this->hashPassword) {
$data[$passwordKey] = hash('crc32c', $this->password);
} elseif (null !== $this->hashPassword) {
unset($data[$passwordKey]);
}

return $data;
}
}
Loading
0