-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[Security] add password rehashing capabilities #31153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 BadCredentialsExcept 741A ion('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())); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This gives this method two responsibilies: checking the password and upgrading it if needed. Maybe this should be done at some higher -orchistration- level? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You mean the checkAuthentication method? I've no better idea, can you elaborate? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, the
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll have a look but I think what misses in your example is the encoder: the "if" must involve it before calling upgrade. Yet the encoder is internal detail of the provider. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I confirm - and so is the cleartext password btw. Same for guard. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems to me it could be moved to public function authenticate(TokenInterface $token)
{
// ...
try {
$this->userChecker->checkPreAuth($user);
$this->checkAuthentication($user, $token);
if ($this instanceof PasswordUpgradingAuthenticationProvider) {
// optional: if ($this->needsPasswordRehash($user, $token)) {
$this->upgradeUserPassword($user, $token);
// optional: }
}
$this->userChecker->checkPostAuth($user);
} catch (BadCredentialsException $e) {
if ($this->hideUserNotFoundExceptions) {
throw new BadCredentialsException('Bad credentials.', 0, $e);
}
throw $e;
} and then class DaoAuthenticationProvider extends UserAuthenticationProvider implements PasswordUpgradingAuthenticationProvider
{
// ...
public function upgradeUserPassword(UserInterface $user, UsernamePasswordToken $token): void
{
if ($this->userProvider instanceof PasswordUpgraderInterface && method_exists($encoder, 'needsRehash') && $encoder->needsRehash($user->getPassword())) {
$this->userProvider->upgradePassword($user, $encoder->encodePassword($token->getCredentials(), $user->getSalt()));
}
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
yes but no :) this is not optional: this is part of the critical lifecycle of password migrations. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My issue was with CQS, not SRP. My issue with SRP was in the naming of the My issue here is still CQS, and I think it would be solved by my proposal. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry it wouldn't, see my objections about encoders. See also the guard interface. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But please submit a PR on my fork if I'm missing the point :) |
||
} | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
<?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\Encoder; | ||
|
||
/** | ||
* Hashes passwords using the best available encoder. | ||
* Validates them using a chain of encoders. | ||
* | ||
* /!\ F41A 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 <p@tchwork.com> | ||
*/ | ||
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); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A silent failure seems odd here. Maybe log something in debug that the user will not be saved because the repository doesn't support it? At least that way when people are trying to figure out why the feature doesn't appear to be working there is something in the logs to tell them why.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A hard failure would be even more odd: you don't want your users not being able to log in when password upgrades cannot happen. They must be opportunistic to me. Logging why not!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree a hard failure would be incorrect here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure anymore about the logger: it would be noisy for ppl that just don't want to implement password upgrades for some reason.
Better document this: if you need pwd upgrade, you must implement the interface.