Skip to content

Commit

Permalink
Merge pull request #6 from jeremykendall/example/daniel-karps-rehash-…
Browse files Browse the repository at this point in the history
…upgrade-scenario

Adds legacySalt argument to PasswordValidatorInterface
  • Loading branch information
jeremykendall committed May 14, 2014
2 parents 4730fd0 + 594f618 commit 52cd57f
Show file tree
Hide file tree
Showing 10 changed files with 269 additions and 21 deletions.
32 changes: 24 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,24 +81,25 @@ persist the updated password hash. Otherwise, what's the point, right?
### Upgrading Legacy Passwords

You can use the `PasswordValidator` whether or not you're currently using
`password_hash` generated passwords. The validator will upgrade your current
legacy hashes to the new `password_hash` generated hashes. All you need to
do is provide a validator callback for your password hash and then
[decorate][6] the validator with the `UpgradeDecorator`.
`password_hash` generated passwords. The validator will transparently upgrade
your current legacy hashes to the new `password_hash` generated hashes as each
user logs in. All you need to do is provide a validator callback for your
password hash and then [decorate][6] the validator with the `UpgradeDecorator`.

``` php
use JeremyKendall\Password\Decorator\UpgradeDecorator;

// Example callback to validate a sha512 hashed password
$callback = function ($password, $passwordHash) {
if (hash('sha512', $password) === $passwordHash) {
$callback = function ($password, $passwordHash, $salt) {
if (hash('sha512', $password . $salt) === $passwordHash) {
return true;
}

return false;
};

$validator = new UpgradeDecorator(new PasswordValidator(), $callback);
$result = $validator->isValid('password', 'password-hash', 'legacy-salt');
```

The `UpgradeDecorator` will validate a user's current password using the
Expand All @@ -109,6 +110,19 @@ All password validation attempts will eventually pass through the
`PasswordValidator`. This allows a password that has already been upgraded to
be properly validated, even when using the `UpgradeDecorator`.

#### Alternate Upgrade Technique

Rather than upgrading each user's password as they log in, it's possible to
preemptively rehash persisted legacy hashes all at once. `PasswordValidator`
and the `UpgradeDecorator` can then be used to validate passwords against the
rehashed legacy hashes, at which point the user's plain text password will be
hashed with `password_hash`, completing the upgrade process.

For more information on this technique, please see Daniel Karp's
[Rehashing Password Hashes][10] blog post, and review
[`JeremyKendall\Password\Tests\Decorator\KarptoniteRehashUpgradeDecoratorTest`][11]
to see a sample implementation.

### Persisting Rehashed Passwords

Whenever a validation attempt returns `Result::SUCCESS_PASSWORD_REHASHED`, it's
Expand Down Expand Up @@ -170,10 +184,10 @@ $storage = new UserDao($db);
$validator = new StorageDecorator(new PasswordValidator(), $storage);

// If validation results in a rehash, the new password hash will be persisted
$result = $validator->isValid('password', 'passwordHash', 'username');
$result = $validator->isValid('password', 'passwordHash', null, 'username');
```

**IMPORTANT**: You must pass the optional third argument (`$identity`) to
**IMPORTANT**: You must pass the optional fourth argument (`$identity`) to
`isValid()` when calling `StorageDecorator::isValid()`. If you do not do so,
the `StorageDecorator` will throw an `IdentityMissingException`.

Expand Down Expand Up @@ -264,3 +278,5 @@ submitting pull requests.
[7]: http://csiphp.com/blog/2012/02/16/encrypt-passwords-for-highest-level-of-security/
[8]: http://php.net/password_hash#example-875
[9]: http://jeremykendall.net/2014/01/04/php-password-hashing-a-dead-simple-implementation/
[10]: http://karptonite.com/2014/05/11/rehashing-password-hashes/
[11]: tests/JeremyKendall/Password/Tests/Decorator/KarptoniteRehashUpgradeDecoratorTest.php
4 changes: 2 additions & 2 deletions src/JeremyKendall/Password/Decorator/AbstractDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ public function __construct(PasswordValidatorInterface $validator)
/**
* {@inheritDoc}
*/
public function isValid($password, $passwordHash, $identity = null)
public function isValid($password, $passwordHash, $legacyHash = null, $identity = null)
{
return $this->validator->isValid($password, $passwordHash, $identity);
return $this->validator->isValid($password, $passwordHash, $legacyHash = null, $identity);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/JeremyKendall/Password/Decorator/StorageDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ public function __construct(
* {@inheritDoc}
* @throws IdentityMissingException If $identity isn't provided
*/
public function isValid($password, $passwordHash, $identity = null)
public function isValid($password, $passwordHash, $legacyHash = null, $identity = null)
{
$result = $this->validator->isValid($password, $passwordHash, $identity);
$result = $this->validator->isValid($password, $passwordHash, $legacyHash, $identity);
$rehashed = ($result->getCode() === ValidationResult::SUCCESS_PASSWORD_REHASHED);

if ($rehashed && $identity === null) {
Expand Down
7 changes: 4 additions & 3 deletions src/JeremyKendall/Password/Decorator/UpgradeDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@ public function __construct(PasswordValidatorInterface $validator, $validationCa
/**
* {@inheritDoc}
*/
public function isValid($password, $passwordHash, $identity = null)
public function isValid($password, $passwordHash, $legacySalt = null, $identity = null)
{
$isValid = call_user_func(
$this->validationCallback,
$password,
$passwordHash
$passwordHash,
$legacySalt
);

if ($isValid === true) {
Expand All @@ -53,6 +54,6 @@ public function isValid($password, $passwordHash, $identity = null)
));
}

return $this->validator->isValid($password, $passwordHash, $identity);
return $this->validator->isValid($password, $passwordHash, $legacySalt, $identity);
}
}
2 changes: 1 addition & 1 deletion src/JeremyKendall/Password/PasswordValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class PasswordValidator implements PasswordValidatorInterface
/**
* {@inheritDoc}
*/
public function isValid($password, $passwordHash, $identity = null)
public function isValid($password, $passwordHash, $legacyHash = null, $identity = null)
{
$this->resultInfo = array(
'code' => ValidationResult::FAILURE_PASSWORD_INVALID,
Expand Down
3 changes: 2 additions & 1 deletion src/JeremyKendall/Password/PasswordValidatorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ interface PasswordValidatorInterface
*
* @param string $password Password provided by user during login
* @param string $passwordHash User's current hashed password
* @param string $legacySalt OPTIONAL salt used in legacy password hashing
* @param string $identity OPTIONAL unique user identifier
* @return Password\Result
*/
public function isValid($password, $passwordHash, $identity = null);
public function isValid($password, $passwordHash, $legacySalt = null, $identity = null);

/**
* Hashes password using password_hash. Uses PASSWORD_DEFAULT encryption.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public function testLegacyPasswordIsValidUpgradedRehashedStored()
->method('updatePassword')
->with($identity, $this->stringContains('$2y$10$'));

$result = $validator->isValid($password, $hash, $identity);
$result = $validator->isValid($password, $hash, null, $identity);

$this->assertTrue($result->isValid());
$this->assertEquals(
Expand Down Expand Up @@ -82,7 +82,7 @@ public function testLegacyPasswordIsValidUpgradedRehashedStored2()
->method('updatePassword')
->with($identity, $this->stringContains('$2y$10$'));

$result = $validator->isValid($password, $hash, $identity);
$result = $validator->isValid($password, $hash, null, $identity);

$this->assertTrue($result->isValid());
$this->assertEquals(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<?php

/**
* Password Validator
*
* @link http://github.com/jeremykendall/password-validator Canonical source repo
* @copyright Copyright (c) 2014 Jeremy Kendall (http://about.me/jeremykendall)
* @license http://github.com/jeremykendall/password-validator/blob/master/LICENSE MIT
*/

namespace JeremyKendall\Password\Tests\Decorator;

use JeremyKendall\Password\Decorator\UpgradeDecorator;
use JeremyKendall\Password\Result as ValidationResult;

/**
* This test validates the upgrade scenario outlined in Daniel Karp's blog post
* {@link http://bit.ly/T0gwRN "Rehashing Password Hashes"}.
*
* In order to properly validate this scenario, the $validationCallback should
* be written to use {@link http://php.net/password_verify password_verify} to
* test the plain text password's legacy hash against the upgraded, persisted
* hash.
*/
class KarptoniteRehashUpgradeDecoratorTest extends \PHPUnit_Framework_TestCase
{
private $decorator;
private $decoratedValidator;
private $validationCallback;
private $plainTextPassword;
private $legacySalt;
private $upgradedLegacyHash;

protected function setUp()
{
parent::setUp();

$this->validationCallback = function ($credential, $passwordHash, $salt) {
// Recreate the legacy hash. This was the persisted password hash
// prior to upgrading.
$legacyHash = hash('sha512', $credential . $salt);

// Now test the old hash against the new, upgraded hash
if (password_verify($legacyHash, $passwordHash)) {
return true;
}

return false;
};

$interface = 'JeremyKendall\Password\PasswordValidatorInterface';
$this->decoratedValidator = $this->getMockBuilder($interface)
->disableOriginalConstructor()
->getMock();

$this->decorator = new UpgradeDecorator(
$this->decoratedValidator,
$this->validationCallback
);

$this->plainTextPassword = 'password';
$this->legacySalt = mt_rand(1000, 1000000);

$legacyHash = hash('sha512', $this->plainTextPassword . $this->legacySalt);
$this->upgradedLegacyHash = password_hash($legacyHash, PASSWORD_DEFAULT);
}

public function testRehashingPasswordHashesScenarioCredentialIsValid()
{
$upgradeValidatorRehash = password_hash(
$this->plainTextPassword,
PASSWORD_DEFAULT,
array(
'cost' => 4,
'salt' => 'CostAndSaltForceRehash',
)
);
$finalValidatorRehash = password_hash($this->plainTextPassword, PASSWORD_DEFAULT);

$validResult = new ValidationResult(
ValidationResult::SUCCESS_PASSWORD_REHASHED,
$finalValidatorRehash
);

$this->decoratedValidator->expects($this->once())
->method('isValid')
->with($this->plainTextPassword, $upgradeValidatorRehash, $this->legacySalt)
->will($this->returnValue($validResult));

$result = $this->decorator->isValid(
$this->plainTextPassword,
$this->upgradedLegacyHash,
$this->legacySalt
);

$this->assertTrue($result->isValid());
$this->assertEquals(
ValidationResult::SUCCESS_PASSWORD_REHASHED,
$result->getCode()
);

// Final rehashed password is a valid hash
$this->assertTrue(
password_verify($this->plainTextPassword, $result->getPassword())
);
}

public function testRehashingPasswordHashesScenarioCredentialIsNotValid()
{
$wrongPlainTextPassword = 'i-forgot-my-password';

$invalidResult = new ValidationResult(
ValidationResult::FAILURE_PASSWORD_INVALID
);

$this->decoratedValidator->expects($this->never())
->method('rehash');

$this->decoratedValidator->expects($this->once())
->method('isValid')
->with($wrongPlainTextPassword, $this->upgradedLegacyHash, $this->legacySalt)
->will($this->returnValue($invalidResult));

$result = $this->decorator->isValid(
$wrongPlainTextPassword,
$this->upgradedLegacyHash,
$this->legacySalt
);

$this->assertFalse($result->isValid());
$this->assertEquals(
ValidationResult::FAILURE_PASSWORD_INVALID,
$result->getCode()
);
}

/**
* @dataProvider callbackDataProvider
*/
public function testVerifyValidationCallback($password, $result)
{
$isValid = call_user_func(
$this->validationCallback,
$password,
$this->upgradedLegacyHash,
$this->legacySalt
);

$this->assertEquals($result, $isValid);
}

public function callbackDataProvider()
{
return array(
array('password', true),
array('wrong-password', false),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ public function testPasswordValidPasswordRehashedAndStored()

$this->decoratedValidator->expects($this->once())
->method('isValid')
->with('password', 'passwordHash', 'username')
->with('password', 'passwordHash', null, 'username')
->will($this->returnValue($valid));

$result = $this->decorator->isValid('password', 'passwordHash', 'username');
$result = $this->decorator->isValid('password', 'passwordHash', null, 'username');

$this->assertTrue($result->isValid());
$this->assertEquals(
Expand Down
Loading

0 comments on commit 52cd57f

Please sign in to comment.