-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[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
Conversation
880d7b6
to
ff71522
Compare
@weaverryan this would impact maker bundle, we should figure out how. |
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 think this is a good idea! I'm not too sure about the opt-in vs opt-out of #30955. The current implementation in this PR is really nice and I prefer it over an event listener. The issue I see here, is that the functionality is shifted from something you can hook into every authenticator via an event, to you must implement this in your authenticator for it to work. This means people will have to wait for upstream changes for every implementation. In this PR I only see it in the Dao authentication provider, meaning every other built-in is still lacking this feature.
TL;DR
While I really like the idea, I think it's something harder to -implement for/get control over by- vendors and applications
src/Symfony/Component/Security/Core/User/UserProviderInterface.php
Outdated
Show resolved
Hide resolved
*/ | ||
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.
this implementation is not enough. We also need to rehash if any other encoder than the best one was actually validating it.
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 don't understand why. To me, only the best encoder should be used. Can you elaborate?
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.
would this encoder return true
in this method if the hash was not generated by this encoder but by one of the extra encoders ? Or would it fail entirely or return false
?
There is 2 reasons to rehash a password:
- the best encoder was already used, but it now has a stronger hashing and wants to rehash the password
- a weaker encoder was used. In this case, we want to rehash, but this code expects the best encoder to detect that case when passing it the hash generated by the other encoder (and so potentially in a different format).
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.
would this encoder return true in this method if the hash was not generated by this encoder but by one of the extra encoders
Yes, absolutely! There cannot be any other return value in this situation. The encoder must return true
only if it supports the hash of course - that's what implementations do also.
src/Symfony/Component/Security/Core/Encoder/ChainPasswordEncoder.php
Outdated
Show resolved
Hide resolved
Description updated with a link to an issue on MakerBundle. Changes would be minimal there, but there would be some. Thanks for thinking about that. |
Thanks for the comments, continuing my experiment (I still didn't run the code yet :) ):
If anyone is courageous enough to give this a try, please do :) |
008ad54
to
846ad0e
Compare
*/ | ||
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.
would this encoder return true
in this method if the hash was not generated by this encoder but by one of the extra encoders ? Or would it fail entirely or return false
?
There is 2 reasons to rehash a password:
- the best encoder was already used, but it now has a stronger hashing and wants to rehash the password
- a weaker encoder was used. In this case, we want to rehash, but this code expects the best encoder to detect that case when passing it the hash generated by the other encoder (and so potentially in a different format).
846ad0e
to
26057d3
Compare
PR ready for deeper review before I invest more time in adding tests and/or split in several PRs: the code together with symfony/maker-bundle#389 works well now. |
26057d3
to
41bc2c0
Compare
} | ||
|
||
$repository = $this->getRepository(); | ||
if ($repository instanceof PasswordUpgraderInterface) { |
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.
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 comment
The 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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the checkAuthentication
method.
So instead of doing this inside the checkAuthentication
do this in the code that calls checkAuthentication
, so move it one level up to decouple the behaviours:
if ($provider->checkAuthentication(...)) {
$provider->upgradePassword(...);
}
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'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 comment
The reason will be displayed to describe this comment to others. Learn more.
encoder is internal detail of the provider
I confirm - and so is the cleartext password btw. Same for guard.
Deadend to me.
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.
Seems to me it could be moved to \Symfony\Component\Security\Core\Authentication\Provider\UserAuthenticationProvider
. That way it's part of the template method so all AuthenticationProviders can benefit from it, instead of being part of a particular implementation.
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 \Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider
would implement PasswordUpgradingAuthenticationProvider
:
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 comment
The reason will be displayed to describe this comment to others. Learn more.
optional: if ($this->needsPasswordRehash($user, $token)) {
yes but no :) this is not optional: this is part of the critical lifecycle of password migrations.
Not all auth providers have a concept of encoders. Only those should know and care about migrations. Said another way, your initial issue was SRP for the method, this proposal is moving the issue somewhere else. Let's say there is no SRP issue in the first place and everything is fine: checkAuthentication is the correct place to wire opportunistic password upgrades to me.
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.
My issue was with CQS, not SRP. My issue with SRP was in the naming of the ChainPasswordEncoder
.
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 comment
The 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.
Opportunistic password upgrades don't align to CQS here to me.
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.
But please submit a PR on my fork if I'm missing the point :)
src/Symfony/Component/Security/Core/Encoder/ChainPasswordEncoder.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Security/Core/User/InMemoryUserProvider.php
Outdated
Show resolved
Hide resolved
41bc2c0
to
d0dd991
Compare
* 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 |
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.
Instead of (or: in addition to) this warning, why not put a check in the constructor that throws an exception when the PlaintextPasswordEncoder
is passed?
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 wouldn't do this: technically one might have a reason to do so, we shouldn't close the door.
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.
fwiw, I use plaintextpasswordencoder in my unit test harness.
d0dd991
to
6e0487b
Compare
… (nicolas-grekas) This PR was merged into the 4.4 branch. Discussion ---------- [Security] add PasswordEncoderInterface::needsRehash() | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | yes | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - Split from #31153, with tests. Commits ------- 50590dc [Security] add PasswordEncoderInterface::needsRehash()
This PR was merged into the 4.4 branch. Discussion ---------- [Security] add MigratingPasswordEncoder | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - Split from #31153: the proposed `MigratingPasswordEncoder` is able to validate password using a chain of encoders, and encodes new them using the best-provided algorithm. This chained encoder is used when the "auto" algorithm is configured. This is seamless for 4.3 app. Commits ------- 765f14c [Security] add MigratingPasswordEncoder
Continued in #31843 |
…ations (nicolas-grekas) This PR was merged into the 4.4 branch. Discussion ---------- [Security] add support for opportunistic password migrations | Q | A | ------------- | --- | Branch? | 4.4 | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #31139 | License | MIT | Doc PR | - | Maker PR | symfony/maker-bundle#389 With this last piece, we'll provide opportunistic password migrations out of the box. This finishes the story drafted in #31153, see there for more info. Commits ------- 2cfc5c7 [Security] add support for opportunistic password migrations
…las-grekas) This PR was merged into the 1.0-dev branch. Discussion ---------- Allow rehashing passwords when possible and needed Fixes #382 Needs symfony/symfony#31153 Commits ------- d81a55b Allow rehashing passwords when possible and needed
This PR adds a new
PasswordUpgraderInterface
to migrate passwords to better hash algos when users log in.The interface is implemented on relevant user-encoder in core. When Users are managed via Doctrine, the interface should be implemented on the app's UserRepository. This means the feature is opt-in. For new projects, symfony/maker-bundle#389 generates user repositories that provide the needed implementation, so that this feature works by default. Existing projects should add the new method to their repo.
Because it was needed, Guard's
AuthenticatorInterface::checkCredentials()
method now takes aPasswordUpgraderInterface
as 3rd argument.On the validation side, a new
MigratingPasswordEncoder
allows validating a hash using several algos.TL;DR, this should provide state of the art password management, with an authentication layer that can validate old password hashes and turn them into new ones.