diff --git a/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php b/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php index 20f68399571f9..9a3ff10b823a2 100644 --- a/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php +++ b/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php @@ -14,6 +14,7 @@ use Doctrine\Common\Persistence\ManagerRegistry; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -25,7 +26,7 @@ * @author Fabien Potencier * @author Johannes M. Schmitt */ -class EntityUserProvider implements UserProviderInterface +class EntityUserProvider implements UserProviderInterface, PasswordUpgraderInterface { private $registry; private $managerName; @@ -107,6 +108,22 @@ public function supportsClass($class) return $class === $this->getClass() || is_subclass_of($class, $this->getClass()); } + /** + * {@inheritdoc} + */ + public function upgradePassword(UserInterface $user, string $newEncodedPassword): void + { + $class = $this->getClass(); + if (!$user instanceof $class) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user))); + } + + $repository = $this->getRepository(); + if ($repository instanceof PasswordUpgraderInterface) { + $repository->upgradePassword($user, $newEncodedPassword); + } + } + private function getObjectManager() { return $this->registry->getManager($this->managerName); diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index 819af8522046f..b207e6ec323ac 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -32,7 +32,7 @@ "symfony/property-access": "~3.4|~4.0", "symfony/property-info": "~3.4|~4.0", "symfony/proxy-manager-bridge": "~3.4|~4.0", - "symfony/security": "~3.4|~4.0", + "symfony/security": "^4.4", "symfony/expression-language": "~3.4|~4.0", "symfony/validator": "~3.4|~4.0", "symfony/translation": "~3.4|~4.0", @@ -48,7 +48,8 @@ "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", "symfony/dependency-injection": "<3.4", "symfony/form": "<4.3", - "symfony/messenger": "<4.2" + "symfony/messenger": "<4.2", + "symfony/security-core": "<4.4" }, "suggest": { "symfony/form": "", diff --git a/src/Symfony/Component/Security/CHANGELOG.md b/src/Symfony/Component/Security/CHANGELOG.md index 24d15f7e78467..386792ba47d0e 100644 --- a/src/Symfony/Component/Security/CHANGELOG.md +++ b/src/Symfony/Component/Security/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +4.4.0 +----- + + * Added `MigratingPasswordEncoder` + * Added methods `PasswordEncoderInterface::needsRehash()` and `UserPasswordEncoderInterface::needsRehash()` + * Added and implemented `PasswordUpgraderInterface` + 4.3.0 ----- diff --git a/src/Symfony/Component/Security/Core/Authentication/Provider/DaoAuthenticationProvider.php b/src/Symfony/Component/Security/Core/Authentication/Provider/DaoAuthenticationProvider.php index f8a101972368f..ac635357d6623 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Provider/DaoAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Core/Authentication/Provider/DaoAuthenticationProvider.php @@ -16,6 +16,7 @@ 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\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -54,9 +55,15 @@ protected function checkAuthentication(UserInterface $user, UsernamePasswordToke throw new BadCredentialsException('The presented password cannot be empty.'); } - if (!$this->encoderFactory->getEncoder($user)->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) { + $encoder = $this->encoderFactory->getEncoder($user); + + if (!$encoder->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) { throw new BadCredentialsException('The presented password is invalid.'); } + + if ($this->userProvider instanceof PasswordUpgraderInterface && method_exists($encoder, 'needsRehash') && $encoder->needsRehash($user->getPassword())) { + $this->userProvider->upgradePassword($user, $encoder->encodePassword($presentedPassword, $user->getSalt())); + } } } diff --git a/src/Symfony/Component/Security/Core/Encoder/BasePasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/BasePasswordEncoder.php index 3c3ea1aa17366..2609b3c7aa3d7 100644 --- a/src/Symfony/Component/Security/Core/Encoder/BasePasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/BasePasswordEncoder.php @@ -20,6 +20,14 @@ abstract class BasePasswordEncoder implements PasswordEncoderInterface { const MAX_PASSWORD_LENGTH = 4096; + /** + * {@inheritdoc} + */ + public function needsRehash(string $encoded): bool + { + return false; + } + /** * Demerges a merge password and salt string. * diff --git a/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php b/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php index 150190dc4c161..a9c3a9a01fb55 100644 --- a/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php +++ b/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php @@ -85,7 +85,19 @@ private function createEncoder(array $config) private function getEncoderConfigFromAlgorithm($config) { if ('auto' === $config['algorithm']) { - $config['algorithm'] = SodiumPasswordEncoder::isSupported() ? 'sodium' : 'native'; + $encoderChain = []; + // "plaintext" is not listed as any leaked hashes could then be used to authenticate directly + foreach (['sodium', 'native', 'pbkdf2', $config['hash_algorithm']] as $algo) { + if ('sodium' !== $algo || SodiumPasswordEncoder::isSupported()) { + $config['algorithm'] = $algo; + $encoderChain[] = $this->createEncoder($config); + } + } + + return [ + 'class' => MigratingPasswordEncoder::class, + 'arguments' => $encoderChain, + ]; } switch ($config['algorithm']) { diff --git a/src/Symfony/Component/Security/Core/Encoder/MessageDigestPasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/MessageDigestPasswordEncoder.php index dba0c30d5a9dd..448f301067973 100644 --- a/src/Symfony/Component/Security/Core/Encoder/MessageDigestPasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/MessageDigestPasswordEncoder.php @@ -65,6 +65,6 @@ public function encodePassword($raw, $salt) */ public function isPasswordValid($encoded, $raw, $salt) { - return !$this->isPasswordTooLong($raw) && $this->comparePasswords($encoded, $this->encodePassword($raw, $salt)); + return '$' !== substr($encoded, 0, 1) && !$this->isPasswordTooLong($raw) && $this->comparePasswords($encoded, $this->encodePassword($raw, $salt)); } } diff --git a/src/Symfony/Component/Security/Core/Encoder/MigratingPasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/MigratingPasswordEncoder.php new file mode 100644 index 0000000000000..77e6726808f9b --- /dev/null +++ b/src/Symfony/Component/Security/Core/Encoder/MigratingPasswordEncoder.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Encoder; + +/** + * Hashes passwords using the best available encoder. + * Validates them using a chain of encoders. + * + * /!\ Don't put a PlaintextPasswordEncoder in the list as that'd mean a leaked hash + * could be used to authenticate successfully without knowing the cleartext password. + * + * @author Nicolas Grekas + */ +final class MigratingPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface +{ + private $bestEncoder; + private $extraEncoders; + + public function __construct(PasswordEncoderInterface $bestEncoder, PasswordEncoderInterface ...$extraEncoders) + { + $this->bestEncoder = $bestEncoder; + $this->extraEncoders = $extraEncoders; + } + + /** + * {@inheritdoc} + */ + public function encodePassword($raw, $salt) + { + return $this->bestEncoder->encodePassword($raw, $salt); + } + + /** + * {@inheritdoc} + */ + public function isPasswordValid($encoded, $raw, $salt) + { + if ($this->bestEncoder->isPasswordValid($encoded, $raw, $salt)) { + return true; + } + + if (!$this->bestEncoder->needsRehash($encoded)) { + return false; + } + + foreach ($this->extraEncoders as $encoder) { + if ($encoder->isPasswordValid($encoded, $raw, $salt)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function needsRehash(string $encoded): bool + { + return $this->bestEncoder->needsRehash($encoded); + } +} diff --git a/src/Symfony/Component/Security/Core/Encoder/NativePasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/NativePasswordEncoder.php index a99d064eeb3e2..a05eb288e5de2 100644 --- a/src/Symfony/Component/Security/Core/Encoder/NativePasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/NativePasswordEncoder.php @@ -87,4 +87,12 @@ public function isPasswordValid($encoded, $raw, $salt) return \strlen($raw) <= self::MAX_PASSWORD_LENGTH && password_verify($raw, $encoded); } + + /** + * {@inheritdoc} + */ + public function needsRehash(string $encoded): bool + { + return password_needs_rehash($encoded, $this->algo, $this->options); + } } diff --git a/src/Symfony/Component/Security/Core/Encoder/PasswordEncoderInterface.php b/src/Symfony/Component/Security/Core/Encoder/PasswordEncoderInterface.php index e0573051eb273..748b82d859a15 100644 --- a/src/Symfony/Component/Security/Core/Encoder/PasswordEncoderInterface.php +++ b/src/Symfony/Component/Security/Core/Encoder/PasswordEncoderInterface.php @@ -17,6 +17,8 @@ * PasswordEncoderInterface is the interface for all encoders. * * @author Fabien Potencier + * + * @method bool needsRehash(string $encoded) */ interface PasswordEncoderInterface { diff --git a/src/Symfony/Component/Security/Core/Encoder/Pbkdf2PasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/Pbkdf2PasswordEncoder.php index 4c4eee75d8078..8383eb9782425 100644 --- a/src/Symfony/Component/Security/Core/Encoder/Pbkdf2PasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/Pbkdf2PasswordEncoder.php @@ -72,6 +72,6 @@ public function encodePassword($raw, $salt) */ public function isPasswordValid($encoded, $raw, $salt) { - return !$this->isPasswordTooLong($raw) && $this->comparePasswords($encoded, $this->encodePassword($raw, $salt)); + return '$' !== substr($encoded, 0, 1) && !$this->isPasswordTooLong($raw) && $this->comparePasswords($encoded, $this->encodePassword($raw, $salt)); } } diff --git a/src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php index 96fbdca173324..7e4719e27311a 100644 --- a/src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php @@ -94,4 +94,20 @@ public function isPasswordValid($encoded, $raw, $salt) throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.'); } + + /** + * {@inheritdoc} + */ + public function needsRehash(string $encoded): bool + { + if (\function_exists('sodium_crypto_pwhash_str_needs_rehash')) { + return \sodium_crypto_pwhash_str_needs_rehash($encoded, $this->opsLimit, $this->memLimit); + } + + if (\extension_loaded('libsodium')) { + return \Sodium\crypto_pwhash_str_needs_rehash($encoded, $this->opsLimit, $this->memLimit); + } + + throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.'); + } } diff --git a/src/Symfony/Component/Security/Core/Encoder/UserPasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/UserPasswordEncoder.php index 3efc8c6d48bb5..ad9d929deb4cd 100644 --- a/src/Symfony/Component/Security/Core/Encoder/UserPasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/UserPasswordEncoder.php @@ -46,4 +46,14 @@ public function isPasswordValid(UserInterface $user, $raw) return $encoder->isPasswordValid($user->getPassword(), $raw, $user->getSalt()); } + + /** + * {@inheritdoc} + */ + public function needsRehash(UserInterface $user, string $encoded): bool + { + $encoder = $this->encoderFactory->getEncoder($user); + + return method_exists($encoder, 'needsRehash') && $encoder->needsRehash($encoded); + } } diff --git a/src/Symfony/Component/Security/Core/Encoder/UserPasswordEncoderInterface.php b/src/Symfony/Component/Security/Core/Encoder/UserPasswordEncoderInterface.php index 7861caab20ca6..911bbe5282d9d 100644 --- a/src/Symfony/Component/Security/Core/Encoder/UserPasswordEncoderInterface.php +++ b/src/Symfony/Component/Security/Core/Encoder/UserPasswordEncoderInterface.php @@ -17,6 +17,8 @@ * UserPasswordEncoderInterface is the interface for the password encoder service. * * @author Ariel Ferrandini + * + * @method bool needsRehash(UserInterface $user, string $encoded) */ interface UserPasswordEncoderInterface { diff --git a/src/Symfony/Component/Security/Core/User/ChainUserProvider.php b/src/Symfony/Component/Security/Core/User/ChainUserProvider.php index 4106ad190afea..b5dff59870cdf 100644 --- a/src/Symfony/Component/Security/Core/User/ChainUserProvider.php +++ b/src/Symfony/Component/Security/Core/User/ChainUserProvider.php @@ -22,7 +22,7 @@ * * @author Johannes M. Schmitt */ -class ChainUserProvider implements UserProviderInterface +class ChainUserProvider implements UserProviderInterface, PasswordUpgraderInterface { private $providers; @@ -104,4 +104,20 @@ public function supportsClass($class) return false; } + + /** + * {@inheritdoc} + */ + public function upgradePassword(UserInterface $user, string $newEncodedPassword): void + { + foreach ($this->providers as $provider) { + if ($provider instanceof PasswordUpgraderInterface) { + try { + $provider->upgradePassword($user, $newEncodedPassword); + } catch (UnsupportedUserException $e) { + // ignore: password upgrades are opportunistic + } + } + } + } } diff --git a/src/Symfony/Component/Security/Core/User/LdapUserProvider.php b/src/Symfony/Component/Security/Core/User/LdapUserProvider.php index adb820fccaf35..be602ed7847db 100644 --- a/src/Symfony/Component/Security/Core/User/LdapUserProvider.php +++ b/src/Symfony/Component/Security/Core/User/LdapUserProvider.php @@ -13,6 +13,7 @@ use Symfony\Component\Ldap\Entry; use Symfony\Component\Ldap\Exception\ConnectionException; +use Symfony\Component\Ldap\Exception\ExceptionInterface; use Symfony\Component\Ldap\LdapInterface; use Symfony\Component\Security\Core\Exception\InvalidArgumentException; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; @@ -24,7 +25,7 @@ * @author Grégoire Pineau * @author Charles Sarrazin */ -class LdapUserProvider implements UserProviderInterface +class LdapUserProvider implements UserProviderInterface, PasswordUpgraderInterface { private $ldap; private $baseDn; @@ -34,6 +35,7 @@ class LdapUserProvider implements UserProviderInterface private $uidKey; private $defaultSearch; private $passwordAttribute; + private $entry; public function __construct(LdapInterface $ldap, string $baseDn, string $searchDn = null, string $searchPassword = null, array $defaultRoles = [], string $uidKey = null, string $filter = null, string $passwordAttribute = null) { @@ -89,6 +91,11 @@ public function loadUserByUsername($username) } catch (InvalidArgumentException $e) { } + if (null !== $this->entry) { + // Keep $entry around when called from upgradePassword() + $this->entry = $entry; + } + return $this->loadUser($username, $entry); } @@ -112,6 +119,35 @@ public function supportsClass($class) return 'Symfony\Component\Security\Core\User\User' === $class; } + /** + * {@inheritdoc} + */ + public function upgradePassword(UserInterface $user, string $newEncodedPassword): void + { + if (!$user instanceof User) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user))); + } + + if (null === $this->passwordAttribute) { + return; + } + + try { + // Tell loadUserByUsername() to keep the $entry around + $this->entry = true; + + if ($user->isEqualTo($this->loadUserByUsername($user->getUsername())) && \is_object($this->entry)) { + $this->entry->setAttribute($this->passwordAttribute, [$newEncodedPassword]); + $this->ldap->getEntryManager()->update($this->entry); + $user->setPassword($newEncodedPassword); + } + } catch (ExceptionInterface $e) { + // ignore failed password upgrades + } finally { + $this->entry = null; + } + } + /** * Loads a user from an LDAP entry. * diff --git a/src/Symfony/Component/Security/Core/User/PasswordUpgraderInterface.php b/src/Symfony/Component/Security/Core/User/PasswordUpgraderInterface.php new file mode 100644 index 0000000000000..9c65298b07f56 --- /dev/null +++ b/src/Symfony/Component/Security/Core/User/PasswordUpgraderInterface.php @@ -0,0 +1,27 @@ + + * + * 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; + +/** + * @author Nicolas Grekas + */ +interface PasswordUpgraderInterface +{ + /** + * Upgrades the encoded 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 $newEncodedPassword): void; +} diff --git a/src/Symfony/Component/Security/Core/User/User.php b/src/Symfony/Component/Security/Core/User/User.php index 18faeb7af0402..09b17471c3dc2 100644 --- a/src/Symfony/Component/Security/Core/User/User.php +++ b/src/Symfony/Component/Security/Core/User/User.php @@ -157,4 +157,9 @@ public function isEqualTo(UserInterface $user) return true; } + + public function setPassword(string $password) + { + $this->password = $password; + } } diff --git a/src/Symfony/Component/Security/Guard/AuthenticatorInterface.php b/src/Symfony/Component/Security/Guard/AuthenticatorInterface.php index 851241f08a486..5565b47822cf6 100644 --- a/src/Symfony/Component/Security/Guard/AuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Guard/AuthenticatorInterface.php @@ -94,14 +94,15 @@ public function getUser($credentials, UserProviderInterface $userProvider); * * The *credentials* are the return value from getCredentials() * - * @param mixed $credentials - * @param UserInterface $user + * @param mixed $credentials + * @param UserInterface $user + * @param PasswordUpgraderInterface $passwordUpgrader * * @return bool * * @throws AuthenticationException */ - public function checkCredentials($credentials, UserInterface $user); + public function checkCredentials($credentials, UserInterface $user/*, PasswordUpgraderInterface $passwordUpgrader = null*/); /** * Create an authenticated token for the given user. diff --git a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php index 7e68574a37808..ff128dedcb7dc 100644 --- a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php @@ -17,6 +17,7 @@ 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\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -110,7 +111,7 @@ private function authenticateViaGuard($guardAuthenticator, PreAuthenticationGuar } $this->userChecker->checkPreAuth($user); - if (true !== $guardAuthenticator->checkCredentials($token->getCredentials(), $user)) { + if (true !== $guardAuthenticator->checkCredentials($token->getCredentials(), $user, $this->userProvider instanceof PasswordUpgraderInterface ? $this->userProvider : null)) { throw new BadCredentialsException(sprintf('Authentication failed because %s::checkCredentials() did not return true.', \get_class($guardAuthenticator))); } $this->userChecker->checkPostAuth($user); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy