[Security] Decouple passwords from UserInterface
This commit is contained in:
parent
fdabaf20cc
commit
2764225a38
@ -80,6 +80,88 @@ Routing
|
||||
Security
|
||||
--------
|
||||
|
||||
* Deprecate `UserInterface::getPassword()`
|
||||
If your `getPassword()` method does not return `null` (i.e. you are using password-based authentication),
|
||||
you should implement `PasswordAuthenticatedUserInterface`.
|
||||
|
||||
Before:
|
||||
```php
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
class User implements UserInterface
|
||||
{
|
||||
// ...
|
||||
|
||||
public function getPassword()
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```php
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
|
||||
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
// ...
|
||||
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Deprecate `UserInterface::getSalt()`
|
||||
If your `getSalt()` method does not return `null` (i.e. you are using password-based authentication with an old password hash algorithm that requires user-provided salts),
|
||||
implement `LegacyPasswordAuthenticatedUserInterface`.
|
||||
|
||||
Before:
|
||||
```php
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
class User implements UserInterface
|
||||
{
|
||||
// ...
|
||||
|
||||
public function getPassword()
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function getSalt()
|
||||
{
|
||||
return $this->salt;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```php
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface;
|
||||
|
||||
class User implements UserInterface, LegacyPasswordAuthenticatedUserInterface
|
||||
{
|
||||
// ...
|
||||
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function getSalt(): ?string
|
||||
{
|
||||
return $this->salt;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Deprecate calling `PasswordUpgraderInterface::upgradePassword()` with a `UserInterface` instance that does not implement `PasswordAuthenticatedUserInterface`
|
||||
* Deprecate calling methods `hashPassword()`, `isPasswordValid()` and `needsRehash()` on `UserPasswordHasherInterface` with a `UserInterface` instance that does not implement `PasswordAuthenticatedUserInterface`
|
||||
* Deprecate all classes in the `Core\Encoder\` sub-namespace, use the `PasswordHasher` component instead
|
||||
* Deprecated voters that do not return a valid decision when calling the `vote` method
|
||||
|
||||
|
@ -172,6 +172,90 @@ Routing
|
||||
Security
|
||||
--------
|
||||
|
||||
* Remove `UserInterface::getPassword()`
|
||||
If your `getPassword()` method does not return `null` (i.e. you are using password-based authentication),
|
||||
you should implement `PasswordAuthenticatedUserInterface`.
|
||||
|
||||
Before:
|
||||
```php
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
class User implements UserInterface
|
||||
{
|
||||
// ...
|
||||
|
||||
public function getPassword()
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```php
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
|
||||
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
// ...
|
||||
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Remove `UserInterface::getSalt()`
|
||||
If your `getSalt()` method does not return `null` (i.e. you are using password-based authentication with an old password hash algorithm that requires user-provided salts),
|
||||
implement `LegacyPasswordAuthenticatedUserInterface`.
|
||||
|
||||
Before:
|
||||
```php
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
class User implements UserInterface
|
||||
{
|
||||
// ...
|
||||
|
||||
public function getPassword()
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function getSalt()
|
||||
{
|
||||
return $this->salt;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```php
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface;
|
||||
|
||||
class User implements UserInterface, LegacyPasswordAuthenticatedUserInterface
|
||||
{
|
||||
// ...
|
||||
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function getSalt(): ?string
|
||||
{
|
||||
return $this->salt;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Calling `PasswordUpgraderInterface::upgradePassword()` with a `UserInterface` instance that
|
||||
does not implement `PasswordAuthenticatedUserInterface` now throws a `\TypeError`.
|
||||
* Calling methods `hashPassword()`, `isPasswordValid()` and `needsRehash()` on `UserPasswordHasherInterface`
|
||||
with a `UserInterface` instance that does not implement `PasswordAuthenticatedUserInterface` now throws a `\TypeError`
|
||||
* Drop all classes in the `Core\Encoder\` sub-namespace, use the `PasswordHasher` component instead
|
||||
* Drop support for `SessionInterface $session` as constructor argument of `SessionTokenStorage`, inject a `\Symfony\Component\HttpFoundation\RequestStack $requestStack` instead
|
||||
* Drop support for `session` provided by the ServiceLocator injected in `UsageTrackingTokenStorage`, provide a `request_stack` service instead
|
||||
|
@ -17,6 +17,7 @@ use Doctrine\Persistence\ObjectManager;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
@ -115,9 +116,15 @@ class EntityUserProvider implements UserProviderInterface, PasswordUpgraderInter
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
|
||||
{
|
||||
if (!$user instanceof PasswordAuthenticatedUserInterface) {
|
||||
trigger_deprecation('symfony/doctrine-bridge', '5.3', 'The "%s::upgradePassword()" method expects an instance of "%s" as first argument, the "%s" class should implement it.', PasswordUpgraderInterface::class, PasswordAuthenticatedUserInterface::class, get_debug_type($user));
|
||||
}
|
||||
|
||||
$class = $this->getClass();
|
||||
if (!$user instanceof $class) {
|
||||
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_debug_type($user)));
|
||||
|
@ -14,10 +14,11 @@ namespace Symfony\Bridge\Doctrine\Tests\Fixtures;
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/** @Entity */
|
||||
class User implements UserInterface
|
||||
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
/** @Id @Column(type="integer") */
|
||||
protected $id1;
|
||||
|
@ -22,6 +22,7 @@ use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;
|
||||
use Symfony\Bridge\Doctrine\Tests\DoctrineTestHelper;
|
||||
use Symfony\Bridge\Doctrine\Tests\Fixtures\User;
|
||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
@ -234,4 +235,5 @@ abstract class UserLoaderRepository implements ObjectRepository, UserLoaderInter
|
||||
|
||||
abstract class PasswordUpgraderRepository implements ObjectRepository, PasswordUpgraderInterface
|
||||
{
|
||||
abstract public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void;
|
||||
}
|
||||
|
@ -38,7 +38,7 @@
|
||||
"symfony/property-access": "^4.4|^5.0",
|
||||
"symfony/property-info": "^5.0",
|
||||
"symfony/proxy-manager-bridge": "^4.4|^5.0",
|
||||
"symfony/security-core": "^5.0",
|
||||
"symfony/security-core": "^5.3",
|
||||
"symfony/expression-language": "^4.4|^5.0",
|
||||
"symfony/uid": "^5.1",
|
||||
"symfony/validator": "^5.2",
|
||||
@ -60,7 +60,7 @@
|
||||
"symfony/messenger": "<4.4",
|
||||
"symfony/property-info": "<5",
|
||||
"symfony/security-bundle": "<5",
|
||||
"symfony/security-core": "<5",
|
||||
"symfony/security-core": "<5.3",
|
||||
"symfony/validator": "<5.2"
|
||||
},
|
||||
"suggest": {
|
||||
|
@ -42,6 +42,7 @@ use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
|
||||
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
|
||||
use Symfony\Component\Security\Core\User\ChainUserProvider;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
|
||||
|
||||
@ -664,6 +665,9 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
|
||||
{
|
||||
$encoderMap = [];
|
||||
foreach ($encoders as $class => $encoder) {
|
||||
if (class_exists($class) && !is_a($class, PasswordAuthenticatedUserInterface::class, true)) {
|
||||
trigger_deprecation('symfony/security-bundle', '5.3', 'Configuring an encoder for a user class that does not implement "%s" is deprecated, class "%s" should implement it.', PasswordAuthenticatedUserInterface::class, $class);
|
||||
}
|
||||
$encoderMap[$class] = $this->createEncoder($encoder);
|
||||
}
|
||||
|
||||
@ -775,6 +779,10 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
|
||||
{
|
||||
$hasherMap = [];
|
||||
foreach ($hashers as $class => $hasher) {
|
||||
// @deprecated since Symfony 5.3, remove the check in 6.0
|
||||
if (class_exists($class) && !is_a($class, PasswordAuthenticatedUserInterface::class, true)) {
|
||||
trigger_deprecation('symfony/security-bundle', '5.3', 'Configuring a password hasher for a user class that does not implement "%s" is deprecated, class "%s" should implement it.', PasswordAuthenticatedUserInterface::class, $class);
|
||||
}
|
||||
$hasherMap[$class] = $this->createHasher($hasher);
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
|
||||
|
||||
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle\Security\Core\User\ArrayUserProvider;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\User;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
@ -78,7 +79,7 @@ class SecurityTest extends AbstractWebTestCase
|
||||
}
|
||||
}
|
||||
|
||||
final class UserWithoutEquatable implements UserInterface
|
||||
final class UserWithoutEquatable implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
private $username;
|
||||
private $password;
|
||||
@ -119,7 +120,7 @@ final class UserWithoutEquatable implements UserInterface
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPassword()
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ use Symfony\Component\Ldap\Exception\ConnectionException;
|
||||
use Symfony\Component\Ldap\LdapInterface;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\Exception\LogicException;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface;
|
||||
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
|
||||
@ -68,6 +69,11 @@ class CheckLdapCredentialsListener implements EventSubscriberInterface
|
||||
throw new BadCredentialsException('The presented password cannot be empty.');
|
||||
}
|
||||
|
||||
$user = $passport->getUser();
|
||||
if (!$user instanceof PasswordAuthenticatedUserInterface) {
|
||||
trigger_deprecation('symfony/ldap', '5.3', 'Not implementing the "%s" interface in class "%s" while using password-based authenticators is deprecated.', PasswordAuthenticatedUserInterface::class, get_debug_type($user));
|
||||
}
|
||||
|
||||
/** @var LdapInterface $ldap */
|
||||
$ldap = $this->ldapLocator->get($ldapBadge->getLdapServiceId());
|
||||
try {
|
||||
@ -77,7 +83,7 @@ class CheckLdapCredentialsListener implements EventSubscriberInterface
|
||||
} else {
|
||||
throw new LogicException('Using the "query_string" config without using a "search_dn" and a "search_password" is not supported.');
|
||||
}
|
||||
$username = $ldap->escape($passport->getUser()->getUsername(), '', LdapInterface::ESCAPE_FILTER);
|
||||
$username = $ldap->escape($user->getUsername(), '', LdapInterface::ESCAPE_FILTER);
|
||||
$query = str_replace('{username}', $username, $ldapBadge->getQueryString());
|
||||
$result = $ldap->query($ldapBadge->getDnString(), $query)->execute();
|
||||
if (1 !== $result->count()) {
|
||||
@ -86,7 +92,7 @@ class CheckLdapCredentialsListener implements EventSubscriberInterface
|
||||
|
||||
$dn = $result[0]->getDn();
|
||||
} else {
|
||||
$username = $ldap->escape($passport->getUser()->getUsername(), '', LdapInterface::ESCAPE_DN);
|
||||
$username = $ldap->escape($user->getUsername(), '', LdapInterface::ESCAPE_DN);
|
||||
$dn = str_replace('{username}', $username, $ldapBadge->getDnString());
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ namespace Symfony\Component\Ldap\Security;
|
||||
|
||||
use Symfony\Component\Ldap\Entry;
|
||||
use Symfony\Component\Security\Core\User\EquatableInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
@ -20,7 +21,7 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class LdapUser implements UserInterface, EquatableInterface
|
||||
class LdapUser implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface
|
||||
{
|
||||
private $entry;
|
||||
private $username;
|
||||
|
@ -122,6 +122,8 @@ class LdapUserProvider implements UserProviderInterface, PasswordUpgraderInterfa
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
|
||||
{
|
||||
|
@ -22,11 +22,11 @@
|
||||
"ext-ldap": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/security-core": "^5.0"
|
||||
"symfony/security-core": "^5.3"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/options-resolver": "<4.4",
|
||||
"symfony/security-core": "<5"
|
||||
"symfony/security-core": "<5.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\Ldap\\": "" },
|
||||
|
@ -12,6 +12,7 @@
|
||||
namespace Symfony\Component\PasswordHasher\Hasher;
|
||||
|
||||
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
@ -25,7 +26,7 @@ interface PasswordHasherFactoryInterface
|
||||
/**
|
||||
* Returns the password hasher to use for the given user.
|
||||
*
|
||||
* @param UserInterface|string $user A UserInterface instance or a class name
|
||||
* @param PasswordAuthenticatedUserInterface|UserInterface|string $user A PasswordAuthenticatedUserInterface/UserInterface instance or a class name
|
||||
*
|
||||
* @throws \RuntimeException When no password hasher could be found for the user
|
||||
*/
|
||||
|
@ -11,12 +11,16 @@
|
||||
|
||||
namespace Symfony\Component\PasswordHasher\Hasher;
|
||||
|
||||
use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* Hashes passwords based on the user and the PasswordHasherFactory.
|
||||
*
|
||||
* @author Ariel Ferrandini <arielferrandini@gmail.com>
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class UserPasswordHasher implements UserPasswordHasherInterface
|
||||
{
|
||||
@ -27,36 +31,70 @@ class UserPasswordHasher implements UserPasswordHasherInterface
|
||||
$this->hasherFactory = $hasherFactory;
|
||||
}
|
||||
|
||||
public function hashPassword(UserInterface $user, string $plainPassword): string
|
||||
/**
|
||||
* @param PasswordAuthenticatedUserInterface $user
|
||||
*/
|
||||
public function hashPassword($user, string $plainPassword): string
|
||||
{
|
||||
if (!$user instanceof PasswordAuthenticatedUserInterface) {
|
||||
if (!$user instanceof UserInterface) {
|
||||
throw new \TypeError(sprintf('Expected an instance of "%s" as first argument, but got "%s".', UserInterface::class, get_debug_type($user)));
|
||||
}
|
||||
trigger_deprecation('symfony/password-hasher', '5.3', 'The "%s()" method expects a "%s" instance as first argument. Not implementing it in class "%s" is deprecated.', __METHOD__, PasswordAuthenticatedUserInterface::class, get_debug_type($user));
|
||||
}
|
||||
|
||||
$salt = $user->getSalt();
|
||||
if ($salt && !$user instanceof LegacyPasswordAuthenticatedUserInterface) {
|
||||
trigger_deprecation('symfony/password-hasher', '5.3', 'Returning a string from "getSalt()" without implementing the "%s" interface is deprecated, the "%s" class should implement it.', LegacyPasswordAuthenticatedUserInterface::class, get_debug_type($user));
|
||||
}
|
||||
|
||||
$hasher = $this->hasherFactory->getPasswordHasher($user);
|
||||
|
||||
return $hasher->hash($plainPassword, $user->getSalt());
|
||||
return $hasher->hash($plainPassword, $salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @param PasswordAuthenticatedUserInterface $user
|
||||
*/
|
||||
public function isPasswordValid(UserInterface $user, string $plainPassword): bool
|
||||
public function isPasswordValid($user, string $plainPassword): bool
|
||||
{
|
||||
if (!$user instanceof PasswordAuthenticatedUserInterface) {
|
||||
if (!$user instanceof UserInterface) {
|
||||
throw new \TypeError(sprintf('Expected an instance of "%s" as first argument, but got "%s".', UserInterface::class, get_debug_type($user)));
|
||||
}
|
||||
trigger_deprecation('symfony/password-hasher', '5.3', 'The "%s()" method expects a "%s" instance as first argument. Not implementing it in class "%s" is deprecated.', __METHOD__, PasswordAuthenticatedUserInterface::class, get_debug_type($user));
|
||||
}
|
||||
|
||||
$salt = $user->getSalt();
|
||||
if ($salt && !$user instanceof LegacyPasswordAuthenticatedUserInterface) {
|
||||
trigger_deprecation('symfony/password-hasher', '5.3', 'Returning a string from "getSalt()" without implementing the "%s" interface is deprecated, the "%s" class should implement it.', LegacyPasswordAuthenticatedUserInterface::class, get_debug_type($user));
|
||||
}
|
||||
|
||||
if (null === $user->getPassword()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hasher = $this->hasherFactory->getPasswordHasher($user);
|
||||
|
||||
return $hasher->verify($user->getPassword(), $plainPassword, $user->getSalt());
|
||||
return $hasher->verify($user->getPassword(), $plainPassword, $salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @param PasswordAuthenticatedUserInterface $user
|
||||
*/
|
||||
public function needsRehash(UserInterface $user): bool
|
||||
public function needsRehash($user): bool
|
||||
{
|
||||
if (null === $user->getPassword()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$user instanceof PasswordAuthenticatedUserInterface) {
|
||||
if (!$user instanceof UserInterface) {
|
||||
throw new \TypeError(sprintf('Expected an instance of "%s" as first argument, but got "%s".', UserInterface::class, get_debug_type($user)));
|
||||
}
|
||||
trigger_deprecation('symfony/password-hasher', '5.3', 'The "%s()" method expects a "%s" instance as first argument. Not implementing it in class "%s" is deprecated.', __METHOD__, PasswordAuthenticatedUserInterface::class, get_debug_type($user));
|
||||
}
|
||||
|
||||
$hasher = $this->hasherFactory->getPasswordHasher($user);
|
||||
|
||||
return $hasher->needsRehash($user->getPassword());
|
||||
|
@ -11,27 +11,17 @@
|
||||
|
||||
namespace Symfony\Component\PasswordHasher\Hasher;
|
||||
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
|
||||
/**
|
||||
* Interface for the user password hasher service.
|
||||
*
|
||||
* @author Ariel Ferrandini <arielferrandini@gmail.com>
|
||||
*
|
||||
* @method string hashPassword(PasswordAuthenticatedUserInterface $user, string $plainPassword) Hashes the plain password for the given user.
|
||||
* @method string isPasswordValid(PasswordAuthenticatedUserInterface $user, string $plainPassword) Checks if the plaintext password matches the user's password.
|
||||
* @method bool needsRehash(PasswordAuthenticatedUserInterface $user) Checks if the plaintext password matches the user's password.
|
||||
*/
|
||||
interface UserPasswordHasherInterface
|
||||
{
|
||||
/**
|
||||
* Hashes the plain password for the given user.
|
||||
*/
|
||||
public function hashPassword(UserInterface $user, string $plainPassword): string;
|
||||
|
||||
/**
|
||||
* Checks if the plaintext password matches the user's password.
|
||||
*/
|
||||
public function isPasswordValid(UserInterface $user, string $plainPassword): bool;
|
||||
|
||||
/**
|
||||
* Checks if a password hash would benefit from rehashing.
|
||||
*/
|
||||
public function needsRehash(UserInterface $user): bool;
|
||||
}
|
||||
|
@ -12,18 +12,52 @@
|
||||
namespace Symfony\Component\PasswordHasher\Tests\Hasher;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
|
||||
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
|
||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
|
||||
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
||||
use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\User;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
class UserPasswordHasherTest extends TestCase
|
||||
{
|
||||
use ExpectDeprecationTrait;
|
||||
|
||||
/**
|
||||
* @group legacy
|
||||
*/
|
||||
public function testHashWithNonPasswordAuthenticatedUser()
|
||||
{
|
||||
$this->expectDeprecation('Since symfony/password-hasher 5.3: Returning a string from "getSalt()" without implementing the "Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface" interface is deprecated, the "%s" class should implement it.');
|
||||
|
||||
$userMock = $this->createMock('Symfony\Component\Security\Core\User\UserInterface');
|
||||
$userMock->expects($this->any())
|
||||
->method('getSalt')
|
||||
->willReturn('userSalt');
|
||||
|
||||
$mockHasher = $this->createMock(PasswordHasherInterface::class);
|
||||
$mockHasher->expects($this->any())
|
||||
->method('hash')
|
||||
->with($this->equalTo('plainPassword'), $this->equalTo('userSalt'))
|
||||
->willReturn('hash');
|
||||
|
||||
$mockPasswordHasherFactory = $this->createMock(PasswordHasherFactoryInterface::class);
|
||||
$mockPasswordHasherFactory->expects($this->any())
|
||||
->method('getPasswordHasher')
|
||||
->with($this->equalTo($userMock))
|
||||
->willReturn($mockHasher);
|
||||
|
||||
$passwordHasher = new UserPasswordHasher($mockPasswordHasherFactory);
|
||||
|
||||
$encoded = $passwordHasher->hashPassword($userMock, 'plainPassword');
|
||||
$this->assertEquals('hash', $encoded);
|
||||
}
|
||||
|
||||
public function testHash()
|
||||
{
|
||||
$userMock = $this->createMock('Symfony\Component\Security\Core\User\UserInterface');
|
||||
$userMock = $this->createMock(TestPasswordAuthenticatedUser::class);
|
||||
$userMock->expects($this->any())
|
||||
->method('getSalt')
|
||||
->willReturn('userSalt');
|
||||
@ -48,7 +82,7 @@ class UserPasswordHasherTest extends TestCase
|
||||
|
||||
public function testVerify()
|
||||
{
|
||||
$userMock = $this->createMock(UserInterface::class);
|
||||
$userMock = $this->createMock(TestPasswordAuthenticatedUser::class);
|
||||
$userMock->expects($this->any())
|
||||
->method('getSalt')
|
||||
->willReturn('userSalt');
|
||||
@ -93,3 +127,7 @@ class UserPasswordHasherTest extends TestCase
|
||||
$this->assertFalse($passwordHasher->needsRehash($user));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class TestPasswordAuthenticatedUser implements LegacyPasswordAuthenticatedUserInterface, UserInterface
|
||||
{
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ CHANGELOG
|
||||
5.3
|
||||
---
|
||||
|
||||
* Add `PasswordAuthenticatedUserInterface` for user classes that use passwords
|
||||
* Add `LegacyPasswordAuthenticatedUserInterface` for user classes that use user-provided salts in addition to passwords
|
||||
* Deprecate all classes in the `Core\Encoder\` sub-namespace, use the `PasswordHasher` component instead
|
||||
* Deprecate the `SessionInterface $session` constructor argument of `SessionTokenStorage`, inject a `\Symfony\Component\HttpFoundation\RequestStack $requestStack` instead
|
||||
* Deprecate the `session` service provided by the ServiceLocator injected in `UsageTrackingTokenStorage`, provide a `request_stack` service instead
|
||||
|
@ -11,16 +11,18 @@
|
||||
|
||||
namespace Symfony\Component\Security\Core\Authentication\Provider;
|
||||
|
||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||||
|
||||
/**
|
||||
* DaoAuthenticationProvider uses a UserProviderInterface to retrieve the user
|
||||
@ -67,11 +69,20 @@ class DaoAuthenticationProvider extends UserAuthenticationProvider
|
||||
throw new BadCredentialsException('The presented password is invalid.');
|
||||
}
|
||||
|
||||
if (!$user instanceof PasswordAuthenticatedUserInterface) {
|
||||
trigger_deprecation('symfony/security-core', '5.3', 'Using password-based authentication listeners while not implementing "%s" interface from class "%s" is deprecated.', PasswordAuthenticatedUserInterface::class, get_debug_type($user));
|
||||
}
|
||||
|
||||
$salt = $user->getSalt();
|
||||
if ($salt && !$user instanceof LegacyPasswordAuthenticatedUserInterface) {
|
||||
trigger_deprecation('symfony/security-core', '5.3', 'Returning a string from "getSalt()" without implementing the "%s" interface is deprecated, the "%s" class should implement it.', LegacyPasswordAuthenticatedUserInterface::class, get_debug_type($user));
|
||||
}
|
||||
|
||||
// deprecated since Symfony 5.3
|
||||
if ($this->hasherFactory instanceof EncoderFactoryInterface) {
|
||||
$encoder = $this->hasherFactory->getEncoder($user);
|
||||
|
||||
if (!$encoder->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) {
|
||||
if (!$encoder->isPasswordValid($user->getPassword(), $presentedPassword, $salt)) {
|
||||
throw new BadCredentialsException('The presented password is invalid.');
|
||||
}
|
||||
|
||||
@ -84,12 +95,12 @@ class DaoAuthenticationProvider extends UserAuthenticationProvider
|
||||
|
||||
$hasher = $this->hasherFactory->getPasswordHasher($user);
|
||||
|
||||
if (!$hasher->verify($user->getPassword(), $presentedPassword, $user->getSalt())) {
|
||||
if (!$hasher->verify($user->getPassword(), $presentedPassword, $salt)) {
|
||||
throw new BadCredentialsException('The presented password is invalid.');
|
||||
}
|
||||
|
||||
if ($this->userProvider instanceof PasswordUpgraderInterface && $hasher->needsRehash($user->getPassword())) {
|
||||
$this->userProvider->upgradePassword($user, $hasher->hash($presentedPassword, $user->getSalt()));
|
||||
$this->userProvider->upgradePassword($user, $hasher->hash($presentedPassword, $salt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@
|
||||
namespace Symfony\Component\Security\Core\Authentication\Token;
|
||||
|
||||
use Symfony\Component\Security\Core\User\EquatableInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
@ -262,10 +263,12 @@ abstract class AbstractToken implements TokenInterface
|
||||
return !(bool) $this->user->isEqualTo($user);
|
||||
}
|
||||
|
||||
// @deprecated since Symfony 5.3, check for PasswordAuthenticatedUserInterface on both user objects before comparing passwords
|
||||
if ($this->user->getPassword() !== $user->getPassword()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// @deprecated since Symfony 5.3, check for LegacyPasswordAuthenticatedUserInterface on both user objects before comparing salts
|
||||
if ($this->user->getSalt() !== $user->getSalt()) {
|
||||
return true;
|
||||
}
|
||||
|
@ -12,6 +12,8 @@
|
||||
namespace Symfony\Component\Security\Core\Encoder;
|
||||
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
|
||||
use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', UserPasswordEncoder::class, UserPasswordHasher::class));
|
||||
@ -39,6 +41,15 @@ class UserPasswordEncoder implements UserPasswordEncoderInterface
|
||||
{
|
||||
$encoder = $this->encoderFactory->getEncoder($user);
|
||||
|
||||
if (!$user instanceof PasswordAuthenticatedUserInterface) {
|
||||
trigger_deprecation('symfony/password-hasher', '5.3', 'Not implementing the "%s" interface while using "%s" is deprecated, the "%s" class should implement it.', PasswordAuthenticatedUserInterface::class, __CLASS__, get_debug_type($user));
|
||||
}
|
||||
|
||||
$salt = $user->getSalt();
|
||||
if ($salt && !$user instanceof LegacyPasswordAuthenticatedUserInterface) {
|
||||
trigger_deprecation('symfony/password-hasher', '5.3', 'Returning a string from "getSalt()" without implementing the "%s" interface is deprecated, the "%s" class should implement it.', LegacyPasswordAuthenticatedUserInterface::class, get_debug_type($user));
|
||||
}
|
||||
|
||||
return $encoder->encodePassword($plainPassword, $user->getSalt());
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,9 @@
|
||||
namespace Symfony\Component\Security\Core\Tests\Authentication\Provider;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||||
use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher;
|
||||
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
||||
@ -23,9 +26,6 @@ use Symfony\Component\Security\Core\User\User;
|
||||
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||||
use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher;
|
||||
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
||||
|
||||
class DaoAuthenticationProviderTest extends TestCase
|
||||
{
|
||||
@ -380,4 +380,5 @@ class TestUser implements UserInterface
|
||||
}
|
||||
interface PasswordUpgraderProvider extends UserProviderInterface, PasswordUpgraderInterface
|
||||
{
|
||||
public function upgradePassword(UserInterface $user, string $newHashedPassword): void;
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\ChainUserProvider;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\User;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
@ -251,14 +252,14 @@ class ChainUserProviderTest extends TestCase
|
||||
{
|
||||
$user = new User('user', 'pwd');
|
||||
|
||||
$provider1 = $this->createMock(PasswordUpgraderInterface::class);
|
||||
$provider1 = $this->getMockForAbstractClass(MigratingProvider::class);
|
||||
$provider1
|
||||
->expects($this->once())
|
||||
->method('upgradePassword')
|
||||
->willThrowException(new UnsupportedUserException('unsupported'))
|
||||
;
|
||||
|
||||
$provider2 = $this->createMock(PasswordUpgraderInterface::class);
|
||||
$provider2 = $this->getMockForAbstractClass(MigratingProvider::class);
|
||||
$provider2
|
||||
->expects($this->once())
|
||||
->method('upgradePassword')
|
||||
@ -269,3 +270,8 @@ class ChainUserProviderTest extends TestCase
|
||||
$provider->upgradePassword($user, 'foobar');
|
||||
}
|
||||
}
|
||||
|
||||
abstract class MigratingProvider implements PasswordUpgraderInterface
|
||||
{
|
||||
abstract public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void;
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ class ChainUserProvider implements UserProviderInterface, PasswordUpgraderInterf
|
||||
|
||||
foreach ($this->providers as $provider) {
|
||||
try {
|
||||
if (!$provider->supportsClass(\get_class($user))) {
|
||||
if (!$provider->supportsClass(get_debug_type($user))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -110,10 +110,16 @@ class ChainUserProvider implements UserProviderInterface, PasswordUpgraderInterf
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PasswordAuthenticatedUserInterface $user
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
|
||||
public function upgradePassword($user, string $newEncodedPassword): void
|
||||
{
|
||||
if (!$user instanceof PasswordAuthenticatedUserInterface) {
|
||||
trigger_deprecation('symfony/security-core', '5.3', 'The "%s::upgradePassword()" method expects an instance of "%s" as first argument, the "%s" class should implement it.', PasswordUpgraderInterface::class, PasswordAuthenticatedUserInterface::class, get_debug_type($user));
|
||||
}
|
||||
|
||||
foreach ($this->providers as $provider) {
|
||||
if ($provider instanceof PasswordUpgraderInterface) {
|
||||
try {
|
||||
|
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Core\User;
|
||||
|
||||
/**
|
||||
* For users that can be authenticated using a password/salt couple.
|
||||
*
|
||||
* Once all password hashes have been upgraded to a modern algorithm via password migrations,
|
||||
* implement {@see PasswordAuthenticatedUserInterface} instead.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
interface LegacyPasswordAuthenticatedUserInterface extends PasswordAuthenticatedUserInterface
|
||||
{
|
||||
/**
|
||||
* Returns the salt that was originally used to hash the password.
|
||||
*/
|
||||
public function getSalt(): ?string;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Core\User;
|
||||
|
||||
/**
|
||||
* For users that can be authenticated using a password.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
interface PasswordAuthenticatedUserInterface
|
||||
{
|
||||
/**
|
||||
* Returns the hashed password used to authenticate the user.
|
||||
*
|
||||
* Usually on authentication, a plain-text password will be compared to this value.
|
||||
*
|
||||
* @return string|null The hashed password or null (if not set or erased)
|
||||
*/
|
||||
public function getPassword(): ?string;
|
||||
}
|
@ -13,15 +13,12 @@ namespace Symfony\Component\Security\Core\User;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @method void upgradePassword(PasswordAuthenticatedUserInterface|UserInterface $user, string $newHashedPassword) Upgrades the hashed password of a user, typically for using a better hash algorithm.
|
||||
* This method should persist the new password in the user storage and update the $user object accordingly.
|
||||
* Because you don't want your users not being able to log in, this method should be opportunistic:
|
||||
* it's fine if it does nothing or if it fails without throwing any exception.
|
||||
*/
|
||||
interface PasswordUpgraderInterface
|
||||
{
|
||||
/**
|
||||
* Upgrades the hashed password of a user, typically for using a better hash algorithm.
|
||||
*
|
||||
* This method should persist the new password in the user storage and update the $user object accordingly.
|
||||
* Because you don't want your users not being able to log in, this method should be opportunistic:
|
||||
* it's fine if it does nothing or if it fails without throwing any exception.
|
||||
*/
|
||||
public function upgradePassword(UserInterface $user, string $newHashedPassword): void;
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ namespace Symfony\Component\Security\Core\User;
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
final class User implements UserInterface, EquatableInterface
|
||||
final class User implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface
|
||||
{
|
||||
private $username;
|
||||
private $password;
|
||||
|
@ -52,6 +52,8 @@ interface UserInterface
|
||||
* This should be the hashed password. On authentication, a plain-text
|
||||
* password will be hashed, and then compared to this value.
|
||||
*
|
||||
* This method is deprecated since Symfony 5.3, implement it from {@link PasswordAuthenticatedUserInterface} instead.
|
||||
*
|
||||
* @return string|null The hashed password if any
|
||||
*/
|
||||
public function getPassword();
|
||||
@ -61,6 +63,8 @@ interface UserInterface
|
||||
*
|
||||
* This can return null if the password was not hashed using a salt.
|
||||
*
|
||||
* This method is deprecated since Symfony 5.3, implement it from {@link LegacyPasswordAuthenticatedUserInterface} instead.
|
||||
*
|
||||
* @return string|null The salt
|
||||
*/
|
||||
public function getSalt();
|
||||
|
@ -11,11 +11,13 @@
|
||||
|
||||
namespace Symfony\Component\Security\Core\Validator\Constraints;
|
||||
|
||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
||||
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;
|
||||
use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
|
||||
@ -32,7 +34,7 @@ class UserPasswordValidator extends ConstraintValidator
|
||||
public function __construct(TokenStorageInterface $tokenStorage, $hasherFactory)
|
||||
{
|
||||
if ($hasherFactory instanceof EncoderFactoryInterface) {
|
||||
trigger_deprecation('symfony/security-core', '5.3', 'Passing a "%s" instance to the "%s" constructor is deprecated, use "%s" instead.', EncoderFactoryInterface::class, __CLASS__, PasswordHasherFactoryInterface::class);
|
||||
trigger_deprecation('symfony/security-core', '5.3', 'Passing a "%s" instance to the "%s" constructor is deprecated, use "%s" instead.', EncoderFactoryInterface::class, __CLASS__, PasswordHasherFactoryInterface::class);
|
||||
}
|
||||
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
@ -60,6 +62,15 @@ class UserPasswordValidator extends ConstraintValidator
|
||||
throw new ConstraintDefinitionException('The User object must implement the UserInterface interface.');
|
||||
}
|
||||
|
||||
if (!$user instanceof PasswordAuthenticatedUserInterface) {
|
||||
trigger_deprecation('symfony/security-core', '5.3', 'Using the "%s" validation constraint is deprecated.', PasswordAuthenticatedUserInterface::class, get_debug_type($user), UserPassword::class);
|
||||
}
|
||||
|
||||
$salt = $user->getSalt();
|
||||
if ($salt && !$user instanceof LegacyPasswordAuthenticatedUserInterface) {
|
||||
trigger_deprecation('symfony/security-core', '5.3', 'Returning a string from "getSalt()" without implementing the "%s" interface is deprecated, the "%s" class should implement it.', LegacyPasswordAuthenticatedUserInterface::class, get_debug_type($user));
|
||||
}
|
||||
|
||||
$hasher = $this->hasherFactory instanceof EncoderFactoryInterface ? $this->hasherFactory->getEncoder($user) : $this->hasherFactory->getPasswordHasher($user);
|
||||
|
||||
if (null === $user->getPassword() || !($hasher instanceof PasswordEncoderInterface ? $hasher->isPasswordValid($user->getPassword(), $password, $user->getSalt()) : $hasher->verify($user->getPassword(), $password, $user->getSalt()))) {
|
||||
|
@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
@ -76,6 +77,10 @@ class GuardBridgeAuthenticator implements InteractiveAuthenticatorInterface, Aut
|
||||
$user = $this->getUser($credentials);
|
||||
}
|
||||
|
||||
if ($this->guard instanceof PasswordAuthenticatedInterface && !$user instanceof PasswordAuthenticatedUserInterface) {
|
||||
trigger_deprecation('symfony/security-guard', '5.3', 'Not implementing the "%s" interface in class "%s" while using password-based guard authenticators is deprecated.', PasswordAuthenticatedUserInterface::class, get_debug_type($user));
|
||||
}
|
||||
|
||||
$passport = new Passport($user, new CustomCredentials([$this->guard, 'checkCredentials'], $credentials));
|
||||
if ($this->userProvider instanceof PasswordUpgraderInterface && $this->guard instanceof PasswordAuthenticatedInterface && (null !== $password = $this->guard->getPassword($credentials))) {
|
||||
$passport->addBadge(new PasswordUpgradeBadge($password, $this->userProvider));
|
||||
|
@ -19,6 +19,7 @@ use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationExpiredException;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
@ -121,6 +122,10 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface
|
||||
throw new \UnexpectedValueException(sprintf('The "%s::getUser()" method must return a UserInterface. You returned "%s".', get_debug_type($guardAuthenticator), get_debug_type($user)));
|
||||
}
|
||||
|
||||
if ($guardAuthenticator instanceof PasswordAuthenticatedInterface && !$user instanceof PasswordAuthenticatedUserInterface) {
|
||||
trigger_deprecation('symfony/security-guard', '5.3', 'Not implementing the "%s" interface in class "%s" while using password-based guard authenticators is deprecated.', PasswordAuthenticatedUserInterface::class, get_debug_type($user));
|
||||
}
|
||||
|
||||
$this->userChecker->checkPreAuth($user);
|
||||
if (true !== $checkCredentialsResult = $guardAuthenticator->checkCredentials($token->getCredentials(), $user)) {
|
||||
if (false !== $checkCredentialsResult) {
|
||||
|
@ -12,14 +12,16 @@
|
||||
namespace Symfony\Component\Security\Http\EventListener;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||||
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface;
|
||||
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
|
||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||||
|
||||
/**
|
||||
* This listeners uses the interfaces of authenticators to
|
||||
@ -52,6 +54,11 @@ class CheckCredentialsListener implements EventSubscriberInterface
|
||||
if ($passport instanceof UserPassportInterface && $passport->hasBadge(PasswordCredentials::class)) {
|
||||
// Use the password hasher to validate the credentials
|
||||
$user = $passport->getUser();
|
||||
|
||||
if (!$user instanceof PasswordAuthenticatedUserInterface) {
|
||||
trigger_deprecation('symfony/security-http', '5.3', 'Not implementing the "%s" interface in class "%s" while using password-based authentication is deprecated.', PasswordAuthenticatedUserInterface::class, get_debug_type($user));
|
||||
}
|
||||
|
||||
/** @var PasswordCredentials $badge */
|
||||
$badge = $passport->getBadge(PasswordCredentials::class);
|
||||
|
||||
@ -68,13 +75,18 @@ class CheckCredentialsListener implements EventSubscriberInterface
|
||||
throw new BadCredentialsException('The presented password is invalid.');
|
||||
}
|
||||
|
||||
$salt = $user->getSalt();
|
||||
if ($salt && !$user instanceof LegacyPasswordAuthenticatedUserInterface) {
|
||||
trigger_deprecation('symfony/security-http', '5.3', 'Returning a string from "getSalt()" without implementing the "%s" interface is deprecated, the "%s" class should implement it.', LegacyPasswordAuthenticatedUserInterface::class, get_debug_type($user));
|
||||
}
|
||||
|
||||
// @deprecated since Symfony 5.3
|
||||
if ($this->hasherFactory instanceof EncoderFactoryInterface) {
|
||||
if (!$this->hasherFactory->getEncoder($user)->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) {
|
||||
if (!$this->hasherFactory->getEncoder($user)->isPasswordValid($user->getPassword(), $presentedPassword, $salt)) {
|
||||
throw new BadCredentialsException('The presented password is invalid.');
|
||||
}
|
||||
} else {
|
||||
if (!$this->hasherFactory->getPasswordHasher($user)->verify($user->getPassword(), $presentedPassword, $user->getSalt())) {
|
||||
if (!$this->hasherFactory->getPasswordHasher($user)->verify($user->getPassword(), $presentedPassword, $salt)) {
|
||||
throw new BadCredentialsException('The presented password is invalid.');
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,10 @@ namespace Symfony\Component\Security\Http\Tests\EventListener;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||||
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
@ -25,8 +28,6 @@ use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPasspor
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface;
|
||||
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
|
||||
use Symfony\Component\Security\Http\EventListener\PasswordMigratingListener;
|
||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||||
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
||||
|
||||
class PasswordMigratingListenerTest extends TestCase
|
||||
{
|
||||
@ -36,7 +37,7 @@ class PasswordMigratingListenerTest extends TestCase
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->user = $this->createMock(UserInterface::class);
|
||||
$this->user = $this->createMock(TestPasswordAuthenticatedUser::class);
|
||||
$this->user->expects($this->any())->method('getPassword')->willReturn('old-hash');
|
||||
$encoder = $this->createMock(PasswordHasherInterface::class);
|
||||
$encoder->expects($this->any())->method('needsRehash')->willReturn(true);
|
||||
@ -62,7 +63,7 @@ class PasswordMigratingListenerTest extends TestCase
|
||||
yield [$this->createEvent(new SelfValidatingPassport(new UserBadge('test', function () { return $this->createMock(UserInterface::class); })))];
|
||||
|
||||
// blank password
|
||||
yield [$this->createEvent(new SelfValidatingPassport(new UserBadge('test', function () { return $this->createMock(UserInterface::class); }), [new PasswordUpgradeBadge('', $this->createPasswordUpgrader())]))];
|
||||
yield [$this->createEvent(new SelfValidatingPassport(new UserBadge('test', function () { return $this->createMock(TestPasswordAuthenticatedUser::class); }), [new PasswordUpgradeBadge('', $this->createPasswordUpgrader())]))];
|
||||
|
||||
// no user
|
||||
yield [$this->createEvent($this->createMock(PassportInterface::class))];
|
||||
@ -96,7 +97,7 @@ class PasswordMigratingListenerTest extends TestCase
|
||||
|
||||
public function testUpgradeWithoutUpgrader()
|
||||
{
|
||||
$userLoader = $this->createMock(MigratingUserProvider::class);
|
||||
$userLoader = $this->getMockForAbstractClass(TestMigratingUserProvider::class);
|
||||
$userLoader->expects($this->any())->method('loadUserByUsername')->willReturn($this->user);
|
||||
|
||||
$userLoader->expects($this->once())
|
||||
@ -110,7 +111,7 @@ class PasswordMigratingListenerTest extends TestCase
|
||||
|
||||
private function createPasswordUpgrader()
|
||||
{
|
||||
return $this->createMock(PasswordUpgraderInterface::class);
|
||||
return $this->getMockForAbstractClass(TestMigratingUserProvider::class);
|
||||
}
|
||||
|
||||
private function createEvent(PassportInterface $passport)
|
||||
@ -119,6 +120,12 @@ class PasswordMigratingListenerTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
abstract class MigratingUserProvider implements UserProviderInterface, PasswordUpgraderInterface
|
||||
abstract class TestMigratingUserProvider implements UserProviderInterface, PasswordUpgraderInterface
|
||||
{
|
||||
abstract public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void;
|
||||
}
|
||||
|
||||
abstract class TestPasswordAuthenticatedUser implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
abstract public function getPassword(): ?string;
|
||||
}
|
||||
|
Reference in New Issue
Block a user