Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[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

Closed
wants to merge 1 commit into from

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Apr 17, 2019

Q A
Branch? master
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

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 a PasswordUpgraderInterface 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.

@nicolas-grekas
Copy link
Member Author

@weaverryan this would impact maker bundle, we should figure out how.

Copy link
Contributor

@linaori linaori left a 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

*/
public function needsRehash(string $encoded): bool
{
return $this->bestEncoder->needsRehash($encoded);
Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Member

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).

Copy link
Member Author

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.

@weaverryan
Copy link
Member

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.

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented May 7, 2019

Thanks for the comments, continuing my experiment (I still didn't run the code yet :) ):

  • maker bundle PR submitted, see Allow rehashing passwords when possible and needed maker-bundle#389
  • upgradePassword() is now split in a dedicated PasswordUpgraderInterface
  • applicable user providers now all implement PasswordUpgraderInterface
  • Guard's AuthenticatorInterface::checkCredentials() method now takes a PasswordUpgraderInterface as 3rd argument

If anyone is courageous enough to give this a try, please do :)

@nicolas-grekas nicolas-grekas force-pushed the sec-rehash branch 3 times, most recently from 008ad54 to 846ad0e Compare May 9, 2019 14:13
*/
public function needsRehash(string $encoded): bool
{
return $this->bestEncoder->needsRehash($encoded);
Copy link
Member

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).

@nicolas-grekas
Copy link
Member Author

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.

@nicolas-grekas nicolas-grekas marked this pull request as ready for review May 21, 2019 14:59
}

$repository = $this->getRepository();
if ($repository instanceof PasswordUpgraderInterface) {
Copy link
Contributor

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.

Copy link
Member Author

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!

Copy link
Contributor

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.

Copy link
Member Author

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()));
Copy link
Contributor

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?

Copy link
Member Author

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?

Copy link
Contributor

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(...);
}

Copy link
Member Author

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.

Copy link
Member Author

@nicolas-grekas nicolas-grekas May 22, 2019

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.

Copy link
Contributor

@rpkamp rpkamp May 22, 2019

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()));
      }
   }
}

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Member Author

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 :)

* 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
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link

@elchris elchris Jan 29, 2020

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.

@nicolas-grekas nicolas-grekas changed the base branch from master to 4.4 June 2, 2019 20:04
chalasr pushed a commit that referenced this pull request Jun 4, 2019
… (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()
fabpot added a commit that referenced this pull request Jun 4, 2019
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
@nicolas-grekas
Copy link
Member Author

Continued in #31843

@nicolas-grekas nicolas-grekas deleted the sec-rehash branch June 4, 2019 10:32
chalasr pushed a commit that referenced this pull request Aug 5, 2019
…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
weaverryan added a commit to symfony/maker-bundle that referenced this pull request Aug 18, 2019
…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
@nicolas-grekas nicolas-grekas modified the milestones: next, 4.4 Oct 27, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants