feature #40267 [Security] Decouple passwords from UserInterface (chalasr)
This PR was merged into the 5.3-dev branch.
Discussion
----------
[Security] Decouple passwords from UserInterface
| Q | A
| ------------- | ---
| Branch? | 5.x
| Bug fix? | no
| New feature? | yes
| Deprecations? | yes
| Tickets | #23081, helps with #39308
| License | MIT
| Doc PR | todo
This PR addresses a long-standing issue of the Security component: UserInterface is coupled to passwords.
It does it by moving the `getPassword()` method from `UserInterface` to a `PasswordAuthenticatedUserInterface`, and the `getSalt()` method to a `LegacyPasswordAuthenticatedUserInterface`.
Steps:
- In 5.3, we add the new interface and, at places where password-based authentication happens, trigger deprecation notices when a `UserInterface` object does not implement the new interface(s). The UserInterface is kept as-is until 6.0.
- In 6.0, we can remove the methods from `UserInterface` as well as support for using password authentication with user objects not implementing the new interface(s).
As a side-effect, some password-related interfaces (`UserPasswordHasherInterface` and `PasswordUpgraderInterface`) must change their signatures to type-hint against the new interface.
That is done in a BC way, which is to make the concerned methods virtual until 6.0, with deprecation notices triggered from callers and concrete implementations.
Benefits:
In 6.0, applications that use password-less authentication (e.g. login links) won't need to write no-op `getPassword()` and `getSalt()` in order to fulfil the `UserInterface` contract.
For applications that do use password-based authentication, they will need to opt-in explicitly by implementing the relevant interface(s).
This build on great discussions with @wouterj and @nicolas-grekas, and it is part of the overall rework of the Security component.
Commits
-------
2764225a38
[Security] Decouple passwords from UserInterface
This commit is contained in:
commit
8de664d4f3
@ -80,6 +80,88 @@ Routing
|
|||||||
Security
|
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
|
* 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
|
* Deprecated voters that do not return a valid decision when calling the `vote` method
|
||||||
|
|
||||||
|
@ -172,6 +172,90 @@ Routing
|
|||||||
Security
|
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 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 `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
|
* 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 Doctrine\Persistence\ObjectRepository;
|
||||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
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\PasswordUpgraderInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||||
@ -115,9 +116,15 @@ class EntityUserProvider implements UserProviderInterface, PasswordUpgraderInter
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @final
|
||||||
*/
|
*/
|
||||||
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
|
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();
|
$class = $this->getClass();
|
||||||
if (!$user instanceof $class) {
|
if (!$user instanceof $class) {
|
||||||
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_debug_type($user)));
|
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\Column;
|
||||||
use Doctrine\ORM\Mapping\Entity;
|
use Doctrine\ORM\Mapping\Entity;
|
||||||
use Doctrine\ORM\Mapping\Id;
|
use Doctrine\ORM\Mapping\Id;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
/** @Entity */
|
/** @Entity */
|
||||||
class User implements UserInterface
|
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||||
{
|
{
|
||||||
/** @Id @Column(type="integer") */
|
/** @Id @Column(type="integer") */
|
||||||
protected $id1;
|
protected $id1;
|
||||||
|
@ -22,6 +22,7 @@ use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;
|
|||||||
use Symfony\Bridge\Doctrine\Tests\DoctrineTestHelper;
|
use Symfony\Bridge\Doctrine\Tests\DoctrineTestHelper;
|
||||||
use Symfony\Bridge\Doctrine\Tests\Fixtures\User;
|
use Symfony\Bridge\Doctrine\Tests\Fixtures\User;
|
||||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
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\PasswordUpgraderInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
@ -234,4 +235,5 @@ abstract class UserLoaderRepository implements ObjectRepository, UserLoaderInter
|
|||||||
|
|
||||||
abstract class PasswordUpgraderRepository implements ObjectRepository, PasswordUpgraderInterface
|
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-access": "^4.4|^5.0",
|
||||||
"symfony/property-info": "^5.0",
|
"symfony/property-info": "^5.0",
|
||||||
"symfony/proxy-manager-bridge": "^4.4|^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/expression-language": "^4.4|^5.0",
|
||||||
"symfony/uid": "^5.1",
|
"symfony/uid": "^5.1",
|
||||||
"symfony/validator": "^5.2",
|
"symfony/validator": "^5.2",
|
||||||
@ -60,7 +60,7 @@
|
|||||||
"symfony/messenger": "<4.4",
|
"symfony/messenger": "<4.4",
|
||||||
"symfony/property-info": "<5",
|
"symfony/property-info": "<5",
|
||||||
"symfony/security-bundle": "<5",
|
"symfony/security-bundle": "<5",
|
||||||
"symfony/security-core": "<5",
|
"symfony/security-core": "<5.3",
|
||||||
"symfony/validator": "<5.2"
|
"symfony/validator": "<5.2"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"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\NativePasswordEncoder;
|
||||||
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
|
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
|
||||||
use Symfony\Component\Security\Core\User\ChainUserProvider;
|
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\Core\User\UserProviderInterface;
|
||||||
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
|
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
|
||||||
|
|
||||||
@ -664,6 +665,9 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
|
|||||||
{
|
{
|
||||||
$encoderMap = [];
|
$encoderMap = [];
|
||||||
foreach ($encoders as $class => $encoder) {
|
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);
|
$encoderMap[$class] = $this->createEncoder($encoder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -775,6 +779,10 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
|
|||||||
{
|
{
|
||||||
$hasherMap = [];
|
$hasherMap = [];
|
||||||
foreach ($hashers as $class => $hasher) {
|
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);
|
$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\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle\Security\Core\User\ArrayUserProvider;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
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\User;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
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 $username;
|
||||||
private $password;
|
private $password;
|
||||||
@ -119,7 +120,7 @@ final class UserWithoutEquatable implements UserInterface
|
|||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function getPassword()
|
public function getPassword(): ?string
|
||||||
{
|
{
|
||||||
return $this->password;
|
return $this->password;
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ use Symfony\Component\Ldap\Exception\ConnectionException;
|
|||||||
use Symfony\Component\Ldap\LdapInterface;
|
use Symfony\Component\Ldap\LdapInterface;
|
||||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||||
use Symfony\Component\Security\Core\Exception\LogicException;
|
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\Credentials\PasswordCredentials;
|
||||||
use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface;
|
use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface;
|
||||||
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
|
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
|
||||||
@ -68,6 +69,11 @@ class CheckLdapCredentialsListener implements EventSubscriberInterface
|
|||||||
throw new BadCredentialsException('The presented password cannot be empty.');
|
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 */
|
/** @var LdapInterface $ldap */
|
||||||
$ldap = $this->ldapLocator->get($ldapBadge->getLdapServiceId());
|
$ldap = $this->ldapLocator->get($ldapBadge->getLdapServiceId());
|
||||||
try {
|
try {
|
||||||
@ -77,7 +83,7 @@ class CheckLdapCredentialsListener implements EventSubscriberInterface
|
|||||||
} else {
|
} else {
|
||||||
throw new LogicException('Using the "query_string" config without using a "search_dn" and a "search_password" is not supported.');
|
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());
|
$query = str_replace('{username}', $username, $ldapBadge->getQueryString());
|
||||||
$result = $ldap->query($ldapBadge->getDnString(), $query)->execute();
|
$result = $ldap->query($ldapBadge->getDnString(), $query)->execute();
|
||||||
if (1 !== $result->count()) {
|
if (1 !== $result->count()) {
|
||||||
@ -86,7 +92,7 @@ class CheckLdapCredentialsListener implements EventSubscriberInterface
|
|||||||
|
|
||||||
$dn = $result[0]->getDn();
|
$dn = $result[0]->getDn();
|
||||||
} else {
|
} 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());
|
$dn = str_replace('{username}', $username, $ldapBadge->getDnString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ namespace Symfony\Component\Ldap\Security;
|
|||||||
|
|
||||||
use Symfony\Component\Ldap\Entry;
|
use Symfony\Component\Ldap\Entry;
|
||||||
use Symfony\Component\Security\Core\User\EquatableInterface;
|
use Symfony\Component\Security\Core\User\EquatableInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,7 +21,7 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
|||||||
*
|
*
|
||||||
* @final
|
* @final
|
||||||
*/
|
*/
|
||||||
class LdapUser implements UserInterface, EquatableInterface
|
class LdapUser implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface
|
||||||
{
|
{
|
||||||
private $entry;
|
private $entry;
|
||||||
private $username;
|
private $username;
|
||||||
|
@ -122,6 +122,8 @@ class LdapUserProvider implements UserProviderInterface, PasswordUpgraderInterfa
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @final
|
||||||
*/
|
*/
|
||||||
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
|
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
|
||||||
{
|
{
|
||||||
|
@ -22,11 +22,11 @@
|
|||||||
"ext-ldap": "*"
|
"ext-ldap": "*"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"symfony/security-core": "^5.0"
|
"symfony/security-core": "^5.3"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
"symfony/options-resolver": "<4.4",
|
"symfony/options-resolver": "<4.4",
|
||||||
"symfony/security-core": "<5"
|
"symfony/security-core": "<5.3"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": { "Symfony\\Component\\Ldap\\": "" },
|
"psr-4": { "Symfony\\Component\\Ldap\\": "" },
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
namespace Symfony\Component\PasswordHasher\Hasher;
|
namespace Symfony\Component\PasswordHasher\Hasher;
|
||||||
|
|
||||||
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,7 +26,7 @@ interface PasswordHasherFactoryInterface
|
|||||||
/**
|
/**
|
||||||
* Returns the password hasher to use for the given user.
|
* 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
|
* @throws \RuntimeException When no password hasher could be found for the user
|
||||||
*/
|
*/
|
||||||
|
@ -11,12 +11,16 @@
|
|||||||
|
|
||||||
namespace Symfony\Component\PasswordHasher\Hasher;
|
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;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hashes passwords based on the user and the PasswordHasherFactory.
|
* Hashes passwords based on the user and the PasswordHasherFactory.
|
||||||
*
|
*
|
||||||
* @author Ariel Ferrandini <arielferrandini@gmail.com>
|
* @author Ariel Ferrandini <arielferrandini@gmail.com>
|
||||||
|
*
|
||||||
|
* @final
|
||||||
*/
|
*/
|
||||||
class UserPasswordHasher implements UserPasswordHasherInterface
|
class UserPasswordHasher implements UserPasswordHasherInterface
|
||||||
{
|
{
|
||||||
@ -27,36 +31,70 @@ class UserPasswordHasher implements UserPasswordHasherInterface
|
|||||||
$this->hasherFactory = $hasherFactory;
|
$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);
|
$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()) {
|
if (null === $user->getPassword()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$hasher = $this->hasherFactory->getPasswordHasher($user);
|
$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()) {
|
if (null === $user->getPassword()) {
|
||||||
return false;
|
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);
|
$hasher = $this->hasherFactory->getPasswordHasher($user);
|
||||||
|
|
||||||
return $hasher->needsRehash($user->getPassword());
|
return $hasher->needsRehash($user->getPassword());
|
||||||
|
@ -11,27 +11,17 @@
|
|||||||
|
|
||||||
namespace Symfony\Component\PasswordHasher\Hasher;
|
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.
|
* Interface for the user password hasher service.
|
||||||
*
|
*
|
||||||
* @author Ariel Ferrandini <arielferrandini@gmail.com>
|
* @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
|
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;
|
namespace Symfony\Component\PasswordHasher\Tests\Hasher;
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
|
||||||
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
|
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
|
||||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
|
||||||
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
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\User;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
class UserPasswordHasherTest extends TestCase
|
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()
|
public function testHash()
|
||||||
{
|
{
|
||||||
$userMock = $this->createMock('Symfony\Component\Security\Core\User\UserInterface');
|
$userMock = $this->createMock(TestPasswordAuthenticatedUser::class);
|
||||||
$userMock->expects($this->any())
|
$userMock->expects($this->any())
|
||||||
->method('getSalt')
|
->method('getSalt')
|
||||||
->willReturn('userSalt');
|
->willReturn('userSalt');
|
||||||
@ -48,7 +82,7 @@ class UserPasswordHasherTest extends TestCase
|
|||||||
|
|
||||||
public function testVerify()
|
public function testVerify()
|
||||||
{
|
{
|
||||||
$userMock = $this->createMock(UserInterface::class);
|
$userMock = $this->createMock(TestPasswordAuthenticatedUser::class);
|
||||||
$userMock->expects($this->any())
|
$userMock->expects($this->any())
|
||||||
->method('getSalt')
|
->method('getSalt')
|
||||||
->willReturn('userSalt');
|
->willReturn('userSalt');
|
||||||
@ -93,3 +127,7 @@ class UserPasswordHasherTest extends TestCase
|
|||||||
$this->assertFalse($passwordHasher->needsRehash($user));
|
$this->assertFalse($passwordHasher->needsRehash($user));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract class TestPasswordAuthenticatedUser implements LegacyPasswordAuthenticatedUserInterface, UserInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
|
@ -4,6 +4,8 @@ CHANGELOG
|
|||||||
5.3
|
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 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 `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
|
* 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;
|
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\Authentication\Token\UsernamePasswordToken;
|
||||||
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
||||||
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
|
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
|
||||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
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\PasswordUpgraderInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DaoAuthenticationProvider uses a UserProviderInterface to retrieve the user
|
* DaoAuthenticationProvider uses a UserProviderInterface to retrieve the user
|
||||||
@ -67,11 +69,20 @@ class DaoAuthenticationProvider extends UserAuthenticationProvider
|
|||||||
throw new BadCredentialsException('The presented password is invalid.');
|
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
|
// deprecated since Symfony 5.3
|
||||||
if ($this->hasherFactory instanceof EncoderFactoryInterface) {
|
if ($this->hasherFactory instanceof EncoderFactoryInterface) {
|
||||||
$encoder = $this->hasherFactory->getEncoder($user);
|
$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.');
|
throw new BadCredentialsException('The presented password is invalid.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,12 +95,12 @@ class DaoAuthenticationProvider extends UserAuthenticationProvider
|
|||||||
|
|
||||||
$hasher = $this->hasherFactory->getPasswordHasher($user);
|
$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.');
|
throw new BadCredentialsException('The presented password is invalid.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->userProvider instanceof PasswordUpgraderInterface && $hasher->needsRehash($user->getPassword())) {
|
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;
|
namespace Symfony\Component\Security\Core\Authentication\Token;
|
||||||
|
|
||||||
use Symfony\Component\Security\Core\User\EquatableInterface;
|
use Symfony\Component\Security\Core\User\EquatableInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -262,10 +263,12 @@ abstract class AbstractToken implements TokenInterface
|
|||||||
return !(bool) $this->user->isEqualTo($user);
|
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()) {
|
if ($this->user->getPassword() !== $user->getPassword()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @deprecated since Symfony 5.3, check for LegacyPasswordAuthenticatedUserInterface on both user objects before comparing salts
|
||||||
if ($this->user->getSalt() !== $user->getSalt()) {
|
if ($this->user->getSalt() !== $user->getSalt()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
namespace Symfony\Component\Security\Core\Encoder;
|
namespace Symfony\Component\Security\Core\Encoder;
|
||||||
|
|
||||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
|
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;
|
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));
|
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);
|
$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());
|
return $encoder->encodePassword($plainPassword, $user->getSalt());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,9 @@
|
|||||||
namespace Symfony\Component\Security\Core\Tests\Authentication\Provider;
|
namespace Symfony\Component\Security\Core\Tests\Authentication\Provider;
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
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\Provider\DaoAuthenticationProvider;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||||
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
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\UserCheckerInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
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
|
class DaoAuthenticationProviderTest extends TestCase
|
||||||
{
|
{
|
||||||
@ -380,4 +380,5 @@ class TestUser implements UserInterface
|
|||||||
}
|
}
|
||||||
interface PasswordUpgraderProvider extends UserProviderInterface, PasswordUpgraderInterface
|
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\UnsupportedUserException;
|
||||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||||
use Symfony\Component\Security\Core\User\ChainUserProvider;
|
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\PasswordUpgraderInterface;
|
||||||
use Symfony\Component\Security\Core\User\User;
|
use Symfony\Component\Security\Core\User\User;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
@ -251,14 +252,14 @@ class ChainUserProviderTest extends TestCase
|
|||||||
{
|
{
|
||||||
$user = new User('user', 'pwd');
|
$user = new User('user', 'pwd');
|
||||||
|
|
||||||
$provider1 = $this->createMock(PasswordUpgraderInterface::class);
|
$provider1 = $this->getMockForAbstractClass(MigratingProvider::class);
|
||||||
$provider1
|
$provider1
|
||||||
->expects($this->once())
|
->expects($this->once())
|
||||||
->method('upgradePassword')
|
->method('upgradePassword')
|
||||||
->willThrowException(new UnsupportedUserException('unsupported'))
|
->willThrowException(new UnsupportedUserException('unsupported'))
|
||||||
;
|
;
|
||||||
|
|
||||||
$provider2 = $this->createMock(PasswordUpgraderInterface::class);
|
$provider2 = $this->getMockForAbstractClass(MigratingProvider::class);
|
||||||
$provider2
|
$provider2
|
||||||
->expects($this->once())
|
->expects($this->once())
|
||||||
->method('upgradePassword')
|
->method('upgradePassword')
|
||||||
@ -269,3 +270,8 @@ class ChainUserProviderTest extends TestCase
|
|||||||
$provider->upgradePassword($user, 'foobar');
|
$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) {
|
foreach ($this->providers as $provider) {
|
||||||
try {
|
try {
|
||||||
if (!$provider->supportsClass(\get_class($user))) {
|
if (!$provider->supportsClass(get_debug_type($user))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,10 +110,16 @@ class ChainUserProvider implements UserProviderInterface, PasswordUpgraderInterf
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param PasswordAuthenticatedUserInterface $user
|
||||||
|
*
|
||||||
* {@inheritdoc}
|
* {@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) {
|
foreach ($this->providers as $provider) {
|
||||||
if ($provider instanceof PasswordUpgraderInterface) {
|
if ($provider instanceof PasswordUpgraderInterface) {
|
||||||
try {
|
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>
|
* @author Nicolas Grekas <p@tchwork.com>
|
||||||
*/
|
|
||||||
interface PasswordUpgraderInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Upgrades the hashed password of a user, typically for using a better hash algorithm.
|
|
||||||
*
|
*
|
||||||
|
* @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.
|
* 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:
|
* 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.
|
* it's fine if it does nothing or if it fails without throwing any exception.
|
||||||
*/
|
*/
|
||||||
public function upgradePassword(UserInterface $user, string $newHashedPassword): void;
|
interface PasswordUpgraderInterface
|
||||||
|
{
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ namespace Symfony\Component\Security\Core\User;
|
|||||||
*
|
*
|
||||||
* @author Fabien Potencier <fabien@symfony.com>
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
*/
|
*/
|
||||||
final class User implements UserInterface, EquatableInterface
|
final class User implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface
|
||||||
{
|
{
|
||||||
private $username;
|
private $username;
|
||||||
private $password;
|
private $password;
|
||||||
|
@ -52,6 +52,8 @@ interface UserInterface
|
|||||||
* This should be the hashed password. On authentication, a plain-text
|
* This should be the hashed password. On authentication, a plain-text
|
||||||
* password will be hashed, and then compared to this value.
|
* 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
|
* @return string|null The hashed password if any
|
||||||
*/
|
*/
|
||||||
public function getPassword();
|
public function getPassword();
|
||||||
@ -61,6 +63,8 @@ interface UserInterface
|
|||||||
*
|
*
|
||||||
* This can return null if the password was not hashed using a salt.
|
* 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
|
* @return string|null The salt
|
||||||
*/
|
*/
|
||||||
public function getSalt();
|
public function getSalt();
|
||||||
|
@ -11,11 +11,13 @@
|
|||||||
|
|
||||||
namespace Symfony\Component\Security\Core\Validator\Constraints;
|
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\Authentication\Token\Storage\TokenStorageInterface;
|
||||||
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
||||||
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;
|
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\Security\Core\User\UserInterface;
|
||||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
|
||||||
use Symfony\Component\Validator\Constraint;
|
use Symfony\Component\Validator\Constraint;
|
||||||
use Symfony\Component\Validator\ConstraintValidator;
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
|
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
|
||||||
@ -60,6 +62,15 @@ class UserPasswordValidator extends ConstraintValidator
|
|||||||
throw new ConstraintDefinitionException('The User object must implement the UserInterface interface.');
|
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);
|
$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()))) {
|
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\Authentication\Token\TokenInterface;
|
||||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
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\PasswordUpgraderInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||||
@ -76,6 +77,10 @@ class GuardBridgeAuthenticator implements InteractiveAuthenticatorInterface, Aut
|
|||||||
$user = $this->getUser($credentials);
|
$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));
|
$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))) {
|
if ($this->userProvider instanceof PasswordUpgraderInterface && $this->guard instanceof PasswordAuthenticatedInterface && (null !== $password = $this->guard->getPassword($credentials))) {
|
||||||
$passport->addBadge(new PasswordUpgradeBadge($password, $this->userProvider));
|
$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\AuthenticationExpiredException;
|
||||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
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\PasswordUpgraderInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
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)));
|
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);
|
$this->userChecker->checkPreAuth($user);
|
||||||
if (true !== $checkCredentialsResult = $guardAuthenticator->checkCredentials($token->getCredentials(), $user)) {
|
if (true !== $checkCredentialsResult = $guardAuthenticator->checkCredentials($token->getCredentials(), $user)) {
|
||||||
if (false !== $checkCredentialsResult) {
|
if (false !== $checkCredentialsResult) {
|
||||||
|
@ -12,14 +12,16 @@
|
|||||||
namespace Symfony\Component\Security\Http\EventListener;
|
namespace Symfony\Component\Security\Http\EventListener;
|
||||||
|
|
||||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||||||
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
||||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
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\Badge\PasswordUpgradeBadge;
|
||||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials;
|
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\Credentials\PasswordCredentials;
|
||||||
use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface;
|
use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface;
|
||||||
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
|
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
|
||||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This listeners uses the interfaces of authenticators to
|
* This listeners uses the interfaces of authenticators to
|
||||||
@ -52,6 +54,11 @@ class CheckCredentialsListener implements EventSubscriberInterface
|
|||||||
if ($passport instanceof UserPassportInterface && $passport->hasBadge(PasswordCredentials::class)) {
|
if ($passport instanceof UserPassportInterface && $passport->hasBadge(PasswordCredentials::class)) {
|
||||||
// Use the password hasher to validate the credentials
|
// Use the password hasher to validate the credentials
|
||||||
$user = $passport->getUser();
|
$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 */
|
/** @var PasswordCredentials $badge */
|
||||||
$badge = $passport->getBadge(PasswordCredentials::class);
|
$badge = $passport->getBadge(PasswordCredentials::class);
|
||||||
|
|
||||||
@ -68,13 +75,18 @@ class CheckCredentialsListener implements EventSubscriberInterface
|
|||||||
throw new BadCredentialsException('The presented password is invalid.');
|
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
|
// @deprecated since Symfony 5.3
|
||||||
if ($this->hasherFactory instanceof EncoderFactoryInterface) {
|
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.');
|
throw new BadCredentialsException('The presented password is invalid.');
|
||||||
}
|
}
|
||||||
} else {
|
} 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.');
|
throw new BadCredentialsException('The presented password is invalid.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,10 @@ namespace Symfony\Component\Security\Http\Tests\EventListener;
|
|||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
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\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
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\Authenticator\Passport\UserPassportInterface;
|
||||||
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
|
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
|
||||||
use Symfony\Component\Security\Http\EventListener\PasswordMigratingListener;
|
use Symfony\Component\Security\Http\EventListener\PasswordMigratingListener;
|
||||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
|
||||||
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
|
||||||
|
|
||||||
class PasswordMigratingListenerTest extends TestCase
|
class PasswordMigratingListenerTest extends TestCase
|
||||||
{
|
{
|
||||||
@ -36,7 +37,7 @@ class PasswordMigratingListenerTest extends TestCase
|
|||||||
|
|
||||||
protected function setUp(): void
|
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');
|
$this->user->expects($this->any())->method('getPassword')->willReturn('old-hash');
|
||||||
$encoder = $this->createMock(PasswordHasherInterface::class);
|
$encoder = $this->createMock(PasswordHasherInterface::class);
|
||||||
$encoder->expects($this->any())->method('needsRehash')->willReturn(true);
|
$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); })))];
|
yield [$this->createEvent(new SelfValidatingPassport(new UserBadge('test', function () { return $this->createMock(UserInterface::class); })))];
|
||||||
|
|
||||||
// blank password
|
// 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
|
// no user
|
||||||
yield [$this->createEvent($this->createMock(PassportInterface::class))];
|
yield [$this->createEvent($this->createMock(PassportInterface::class))];
|
||||||
@ -96,7 +97,7 @@ class PasswordMigratingListenerTest extends TestCase
|
|||||||
|
|
||||||
public function testUpgradeWithoutUpgrader()
|
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->any())->method('loadUserByUsername')->willReturn($this->user);
|
||||||
|
|
||||||
$userLoader->expects($this->once())
|
$userLoader->expects($this->once())
|
||||||
@ -110,7 +111,7 @@ class PasswordMigratingListenerTest extends TestCase
|
|||||||
|
|
||||||
private function createPasswordUpgrader()
|
private function createPasswordUpgrader()
|
||||||
{
|
{
|
||||||
return $this->createMock(PasswordUpgraderInterface::class);
|
return $this->getMockForAbstractClass(TestMigratingUserProvider::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createEvent(PassportInterface $passport)
|
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