├── .gitignore
├── .scrutinizer.yml
├── .travis.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bin
├── cost-check
└── version-check
├── composer.json
├── phpdoc.dist.xml
├── phpunit.xml.dist
├── src
└── JeremyKendall
│ └── Password
│ ├── Decorator
│ ├── AbstractDecorator.php
│ ├── StorageDecorator.php
│ └── UpgradeDecorator.php
│ ├── PasswordHashFailureException.php
│ ├── PasswordValidator.php
│ ├── PasswordValidatorInterface.php
│ ├── Result.php
│ └── Storage
│ ├── IdentityMissingException.php
│ └── StorageInterface.php
├── tests
├── JeremyKendall
│ └── Password
│ │ └── Tests
│ │ ├── Decorator
│ │ ├── AbstractDecoratorTest.php
│ │ ├── IntegrationTest.php
│ │ ├── KarptoniteRehashUpgradeDecoratorTest.php
│ │ ├── StorageDecoratorTest.php
│ │ └── UpgradeDecoratorTest.php
│ │ └── PasswordValidatorTest.php
└── bootstrap.php
└── travis.phpunit.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | composer.lock
3 | vendor/
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | inherit: true
2 |
3 | tools:
4 | external_code_coverage: true
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 5.3
5 | - 5.4
6 | - 5.5
7 | - 5.6
8 | - 7.0
9 |
10 | before_script:
11 | - composer self-update
12 | - composer install --prefer-dist
13 |
14 | script: phpunit -c phpunit.xml.dist
15 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | ## Pull Requests
4 |
5 | 1. Create your own [fork][1] of the repo
6 | 2. Create a new branch for each feature or improvement
7 | 3. Send a pull request from each feature branch to the **develop** branch
8 |
9 | It is very important to separate new features or improvements into separate
10 | feature branches, and to send a pull request for each branch. This allows me to
11 | review and pull in new features or improvements individually.
12 |
13 | ## Style Guide
14 |
15 | All pull requests must adhere to the [PSR-2 standard][2].
16 |
17 | ## Unit Testing
18 |
19 | All pull requests must be accompanied by passing unit tests and complete code
20 | coverage. This library uses PHPUnit for testing.
21 |
22 | [Learn about PHPUnit][3]
23 |
24 | [1]: https://help.github.com/articles/fork-a-repo
25 | [2]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md
26 | [3]: https://github.com/sebastianbergmann/phpunit/
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2014 Jeremy Kendall
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so,
8 | subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Password Validator [](https://travis-ci.org/jeremykendall/password-validator) [](https://coveralls.io/r/jeremykendall/password-validator?branch=master)
2 |
3 | **Password Validator** *validates* [`password_hash`][2] generated passwords, *rehashes*
4 | passwords as necessary, and will *upgrade* legacy passwords.
5 |
6 | Read the introductory blog post: [PHP Password Hashing: A Dead Simple Implementation][9]
7 |
8 | *Password Validator is available for all versions of PHP >= 5.3.7.*
9 |
10 | ## Motivation
11 |
12 | Why? Because one must always[ encrypt passwords for highest level of
13 | security][7], and the new [PHP password hashing][1] functions provide that level of
14 | security.
15 |
16 | The **Password Validator** library makes it (more) trivial to use the new
17 | password hash functions in your application. Just add the validator to your
18 | authentication script and you're up and running.
19 |
20 | The really big deal here is the **ease of upgrading** from your current legacy
21 | hashes to the new, more secure PHP password hash hashes. Simply wrap the
22 | `PasswordValidator` in the `UpgradeDecorator`, provide a callback to validate
23 | your existing password hashing scheme, and BOOM, you're using new password
24 | hashes in a manner *completely transparent* to your application's users. Nifty,
25 | huh?
26 |
27 | ## Usage
28 |
29 | ### Password Validation
30 |
31 | If you're already using [`password_hash`][2] generated passwords in your
32 | application, you need do nothing more than add the validator in your
33 | authentication script. The validator uses [`password_verify`][3] to test
34 | the validity of the provided password hash.
35 |
36 | ``` php
37 | use JeremyKendall\Password\PasswordValidator;
38 |
39 | $validator = new PasswordValidator();
40 | $result = $validator->isValid($_POST['password'], $hashedPassword);
41 |
42 | if ($result->isValid()) {
43 | // password is valid
44 | }
45 | ```
46 |
47 | If your application requires options other than the `password_hash` defaults,
48 | you can set the `cost` option with `PasswordValidator::setOptions()`.
49 |
50 | ``` php
51 | $options = array(
52 | 'cost' => 11
53 | );
54 | $validator->setOptions($options);
55 | ```
56 |
57 | **IMPORTANT**: `PasswordValidator` uses a default cost of `10`. If your
58 | existing hash implementation requires a different cost, make sure to specify it
59 | using `PasswordValidator::setOptions()`. If you do not do so, all of your
60 | passwords will be rehashed using a cost of `10`.
61 |
62 | ### Rehashing
63 |
64 | Each valid password is tested using [`password_needs_rehash`][4]. If a rehash
65 | is necessary, the valid password is hashed using `password_hash` with the
66 | provided options. The result code `Result::SUCCESS_PASSWORD_REHASHED` will be
67 | returned from `Result::getCode()` and the new password hash is available via
68 | `Result::getPassword()`.
69 |
70 | ``` php
71 | if ($result->getCode() === Result::SUCCESS_PASSWORD_REHASHED) {
72 | $rehashedPassword = $result->getPassword();
73 | // Persist rehashed password
74 | }
75 | ```
76 |
77 | **IMPORTANT**: If the password has been rehashed, it's critical that you
78 | persist the updated password hash. Otherwise, what's the point, right?
79 |
80 | ### Upgrading Legacy Passwords
81 |
82 | You can use the `PasswordValidator` whether or not you're currently using
83 | `password_hash` generated passwords. The validator will transparently upgrade
84 | your current legacy hashes to the new `password_hash` generated hashes as each
85 | user logs in. All you need to do is provide a validator callback for your
86 | password hash and then [decorate][6] the validator with the `UpgradeDecorator`.
87 |
88 | ``` php
89 | use JeremyKendall\Password\Decorator\UpgradeDecorator;
90 |
91 | // Example callback to validate a sha512 hashed password
92 | $callback = function ($password, $passwordHash, $salt) {
93 | if (hash('sha512', $password . $salt) === $passwordHash) {
94 | return true;
95 | }
96 |
97 | return false;
98 | };
99 |
100 | $validator = new UpgradeDecorator(new PasswordValidator(), $callback);
101 | $result = $validator->isValid('password', 'password-hash', 'legacy-salt');
102 | ```
103 |
104 | The `UpgradeDecorator` will validate a user's current password using the
105 | provided callback. If the user's password is valid, it will be hashed with
106 | `password_hash` and returned in the `Result` object, as above.
107 |
108 | All password validation attempts will eventually pass through the
109 | `PasswordValidator`. This allows a password that has already been upgraded to
110 | be properly validated, even when using the `UpgradeDecorator`.
111 |
112 | #### Alternate Upgrade Technique
113 |
114 | Rather than upgrading each user's password as they log in, it's possible to
115 | preemptively rehash persisted legacy hashes all at once. `PasswordValidator`
116 | and the `UpgradeDecorator` can then be used to validate passwords against the
117 | rehashed legacy hashes, at which point the user's plain text password will be
118 | hashed with `password_hash`, completing the upgrade process.
119 |
120 | For more information on this technique, please see Daniel Karp's
121 | [Rehashing Password Hashes][10] blog post, and review
122 | [`JeremyKendall\Password\Tests\Decorator\KarptoniteRehashUpgradeDecoratorTest`][11]
123 | to see a sample implementation.
124 |
125 | ### Persisting Rehashed Passwords
126 |
127 | Whenever a validation attempt returns `Result::SUCCESS_PASSWORD_REHASHED`, it's
128 | important to persist the updated password hash.
129 |
130 | ``` php
131 | if ($result->getCode() === Result::SUCCESS_PASSWORD_REHASHED) {
132 | $rehashedPassword = $result->getPassword();
133 | // Persist rehashed password
134 | }
135 | ```
136 |
137 | While you can always perform the test and then update your user database
138 | manually, if you choose to use the **Storage Decorator** all rehashed passwords
139 | will be automatically persisted.
140 |
141 | The Storage Decorator takes two constructor arguments: An instance of
142 | `PasswordValidatorInterface` and an instance of the
143 | `JeremyKendall\Password\Storage\StorageInterface`.
144 |
145 | #### StorageInterface
146 |
147 | The `StorageInterface` includes a single method, `updatePassword()`. A class
148 | honoring the interface might look like this:
149 |
150 | ``` php
151 | db = $db;
162 | }
163 |
164 | public function updatePassword($identity, $password)
165 | {
166 | $sql = 'UPDATE users SET password = :password WHERE username = :identity';
167 | $stmt = $this->db->prepare($sql);
168 | $stmt->execute(array('password' => $password, 'identity' => $identity));
169 | }
170 | }
171 | ```
172 |
173 | #### Storage Decorator
174 |
175 | With your `UserDao` in hand, you're ready to decorate a
176 | `PasswordValidatorInterface`.
177 |
178 | ``` php
179 | use Example\UserDao;
180 | use JeremyKendall\Password\Decorator\StorageDecorator;
181 |
182 | $storage = new UserDao($db);
183 | $validator = new StorageDecorator(new PasswordValidator(), $storage);
184 |
185 | // If validation results in a rehash, the new password hash will be persisted
186 | $result = $validator->isValid('password', 'passwordHash', null, 'username');
187 | ```
188 |
189 | **IMPORTANT**: You must pass the optional fourth argument (`$identity`) to
190 | `isValid()` when calling `StorageDecorator::isValid()`. If you do not do so,
191 | the `StorageDecorator` will throw an `IdentityMissingException`.
192 |
193 | #### Combining Storage Decorator with Upgrade Decorator
194 |
195 | It is possible to chain decorators together thanks to the
196 | [Decorator Pattern](https://en.wikipedia.org/wiki/Decorator_pattern). A great way to use this is to combine the
197 | `StorageDecorator` and `UpgradeDecorator` together to first update a legacy hash and then save it. Doing so is very
198 | simple - you just need to pass an instance of the `StorageDecorator` as a constructor argument to `UpgradeDecorator`:
199 |
200 | ``` php
201 | use Example\UserDao;
202 | use JeremyKendall\Password\Decorator\StorageDecorator;
203 | use JeremyKendall\Password\Decorator\UpgradeDecorator;
204 |
205 | // Example callback to validate a sha512 hashed password
206 | $callback = function ($password, $passwordHash, $salt) {
207 | if (hash('sha512', $password . $salt) === $passwordHash) {
208 | return true;
209 | }
210 |
211 | return false;
212 | };
213 |
214 | $storage = new UserDao($db);
215 | $storageDecorator = new StorageDecorator(new PasswordValidator(), $storage);
216 | $validator = new UpgradeDecorator($storageDecorator, $callback);
217 |
218 | // If validation results in a rehash, the new password hash will be persisted
219 | $result = $validator->isValid('password', 'passwordHash', null, 'username');
220 | ```
221 |
222 |
223 | ### Validation Results
224 |
225 | Each validation attempt returns a `JeremyKendall\Password\Result` object. The
226 | object provides some introspection into the status of the validation process.
227 |
228 | * `Result::isValid()` will return `true` if the attempt was successful
229 | * `Result::getCode()` will return one of three possible `int` codes:
230 | * `Result::SUCCESS` if the validation attempt was successful
231 | * `Result::SUCCESS_PASSWORD_REHASHED` if the attempt was successful and the password was rehashed
232 | * `Result::FAILURE_PASSWORD_INVALID` if the attempt was unsuccessful
233 | * `Result::getPassword()` will return the rehashed password, but only if the password was rehashed
234 |
235 | ### Database Schema Changes
236 |
237 | As mentioned above, because this library uses the `PASSWORD_DEFAULT` algorithm,
238 | it's important your password field be `VARCHAR(255)` to account for future
239 | updates to the default password hashing algorithm.
240 |
241 | ## Helper Scripts
242 |
243 | After running `composer install`, there are two helper scripts available, both
244 | related to the password hash functions.
245 |
246 | ### version-check
247 |
248 | If you're not already running PHP 5.5+, you should run `version-check` to
249 | ensure your version of PHP is capable of using password-compat, the userland
250 | implementation of the PHP password hash functions. Run `./vendor/bin/version-check`
251 | from the root of your project. The result of the script is pass/fail.
252 |
253 | ### cost-check
254 |
255 | The default `cost` used by `password_hash` is 10. This may or may not be
256 | appropriate for your production hardware, and it's entirely likely you can use
257 | a higher cost than the default. `cost-check` is based on the [finding a good
258 | cost][8] example in the PHP documentation. Simply run `./vendor/bin/cost-check` from the command line and an appropriate cost will be returned.
259 |
260 | **NOTE**: The default time target is 0.2 seconds. You may choose a higher or lower
261 | target by passing a float argument to `cost-check`, like so:
262 |
263 | ``` bash
264 | $ ./vendor/bin/cost-check 0.4
265 | Appropriate 'PASSWORD_DEFAULT' Cost Found: 13
266 | ```
267 |
268 | ## Installation
269 |
270 | The only officially supported method of installation is via
271 | [Composer](http://getcomposer.org).
272 |
273 | Running the following command will add the latest version of the library to your project:
274 | ``` bash
275 | $ composer require jeremykendall/password-validator
276 | ```
277 |
278 | You can update to the latest version with this command:
279 | ``` bash
280 | $ composer update jeremykendall/password-validator
281 | ```
282 |
283 | If you're not already using Composer in your project, add the autoloader to your project:
284 |
285 | ``` php
286 | $cost));
45 | $end = microtime(true);
46 | } while (($end - $start) < $timeTarget);
47 |
48 | \cli\line("%yAppropriate 'PASSWORD_DEFAULT' Cost Found:%y %2%k $cost %2%k%n");
49 |
--------------------------------------------------------------------------------
/bin/version-check:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | =5.3.7",
31 | "ircmaxell/password-compat": "1.*",
32 | "wp-cli/php-cli-tools": "0.10.*"
33 | },
34 | "require-dev": {
35 | "league/phpunit-coverage-listener": "~1.1",
36 | "phpunit/phpunit": "4.*"
37 | },
38 | "autoload": {
39 | "psr-0": {
40 | "JeremyKendall\\Password\\": "src/"
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/phpdoc.dist.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Password Validator
4 |
5 | ./build/docs
6 |
7 | php
8 |
9 |
10 |
11 | ./build/docs
12 |
13 |
14 | ./src
15 |
16 |
17 |
18 |
19 |
20 | quiet
21 |
22 |
23 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | tests
5 |
6 |
7 |
8 | ./src
9 |
10 |
11 |
12 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/JeremyKendall/Password/Decorator/AbstractDecorator.php:
--------------------------------------------------------------------------------
1 | validator = $validator;
33 | }
34 |
35 | /**
36 | * {@inheritDoc}
37 | */
38 | public function isValid($password, $passwordHash, $legacySalt = null, $identity = null)
39 | {
40 | return $this->validator->isValid($password, $passwordHash, $legacySalt, $identity);
41 | }
42 |
43 | /**
44 | * {@inheritDoc}
45 | */
46 | public function rehash($password)
47 | {
48 | return $this->validator->rehash($password);
49 | }
50 |
51 | /**
52 | * {@inheritDoc}
53 | */
54 | public function getOptions()
55 | {
56 | return $this->validator->getOptions();
57 | }
58 |
59 | /**
60 | * {@inheritDoc}
61 | */
62 | public function setOptions(array $options)
63 | {
64 | $this->validator->setOptions($options);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/JeremyKendall/Password/Decorator/StorageDecorator.php:
--------------------------------------------------------------------------------
1 | storage = $storage;
41 | }
42 |
43 | /**
44 | * {@inheritDoc}
45 | * @throws IdentityMissingException If $identity isn't provided
46 | */
47 | public function isValid($password, $passwordHash, $legacySalt = null, $identity = null)
48 | {
49 | $result = $this->validator->isValid($password, $passwordHash, $legacySalt, $identity);
50 | $rehashed = ($result->getCode() === ValidationResult::SUCCESS_PASSWORD_REHASHED);
51 |
52 | if ($rehashed && $identity === null) {
53 | throw new IdentityMissingException(
54 | 'The StorageDecorator requires an $identity argument.'
55 | );
56 | }
57 |
58 | if ($rehashed) {
59 | $this->storage->updatePassword($identity, $result->getPassword());
60 | }
61 |
62 | return $result;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/JeremyKendall/Password/Decorator/UpgradeDecorator.php:
--------------------------------------------------------------------------------
1 | validationCallback = $validationCallback;
38 | }
39 |
40 | /**
41 | * {@inheritDoc}
42 | */
43 | public function isValid($password, $passwordHash, $legacySalt = null, $identity = null)
44 | {
45 | $isValid = call_user_func(
46 | $this->validationCallback,
47 | $password,
48 | $passwordHash,
49 | $legacySalt
50 | );
51 |
52 | if ($isValid === true) {
53 | $passwordHash = $this->createHashWhichWillForceRehashInValidator($password);
54 | }
55 |
56 | return $this->validator->isValid($password, $passwordHash, $legacySalt, $identity);
57 | }
58 |
59 | /**
60 | * This method returns an upgraded password, one that is hashed by the
61 | * password_hash method in such a way that it forces the PasswordValidator
62 | * to rehash the password. This results in PasswordValidator::isValid()
63 | * returning a Result::$code of Result::SUCCESS_PASSWORD_REHASHED,
64 | * notifying the StorageDecorator or custom application code that the
65 | * returned password hash should be persisted.
66 | *
67 | * @param string $password Password to upgrade
68 | *
69 | * @return string Hashed password
70 | */
71 | private function createHashWhichWillForceRehashInValidator($password)
72 | {
73 | $cost = static::DEFAULT_REHASH_COST;
74 | $options = $this->getOptions();
75 |
76 | if (isset($options['cost']) && (int) $options['cost'] === $cost) {
77 | $cost++;
78 | }
79 |
80 | return password_hash($password, PASSWORD_DEFAULT, array('cost' => $cost));
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/JeremyKendall/Password/PasswordHashFailureException.php:
--------------------------------------------------------------------------------
1 | resultInfo = array(
37 | 'code' => ValidationResult::FAILURE_PASSWORD_INVALID,
38 | 'password' => null,
39 | );
40 |
41 | $isValid = password_verify($password, $passwordHash);
42 |
43 | $needsRehash = password_needs_rehash(
44 | $passwordHash,
45 | PASSWORD_DEFAULT,
46 | $this->getOptions()
47 | );
48 |
49 | if ($isValid === true) {
50 | $this->resultInfo['code'] = ValidationResult::SUCCESS;
51 | }
52 |
53 | if ($isValid === true && $needsRehash === true) {
54 | $this->rehash($password);
55 | }
56 |
57 | return new ValidationResult(
58 | $this->resultInfo['code'],
59 | $this->resultInfo['password']
60 | );
61 | }
62 |
63 | /**
64 | * {@inheritDoc}
65 | */
66 | public function rehash($password)
67 | {
68 | $hash = password_hash(
69 | $password,
70 | PASSWORD_DEFAULT,
71 | $this->getOptions()
72 | );
73 |
74 | // Ignoring b/c I have no idea how to make password_hash return false
75 | // @codeCoverageIgnoreStart
76 | if ($hash === false) {
77 | throw new PasswordHashFailureException('password_hash returned false.');
78 | }
79 | // @codeCoverageIgnoreEnd
80 |
81 | $this->resultInfo = array(
82 | 'code' => ValidationResult::SUCCESS_PASSWORD_REHASHED,
83 | 'password' => $hash,
84 | );
85 |
86 | return $hash;
87 | }
88 |
89 | /**
90 | * {@inheritDoc}
91 | */
92 | public function getOptions()
93 | {
94 | return $this->options;
95 | }
96 |
97 | /**
98 | * {@inheritDoc}
99 | */
100 | public function setOptions(array $options)
101 | {
102 | $this->options = $options;
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/JeremyKendall/Password/PasswordValidatorInterface.php:
--------------------------------------------------------------------------------
1 | code = (int) $code;
66 | $this->password = $password;
67 | }
68 |
69 | /**
70 | * Returns whether the result represents a successful authentication attempt
71 | *
72 | * @return bool
73 | */
74 | public function isValid()
75 | {
76 | return $this->code > 0;
77 | }
78 |
79 | /**
80 | * getCode() - Get the result code for this authentication attempt
81 | *
82 | * @return int
83 | */
84 | public function getCode()
85 | {
86 | return $this->code;
87 | }
88 |
89 | /**
90 | * Returns the rehashed password
91 | *
92 | * Only present if password was rehashed
93 | *
94 | * @return string
95 | */
96 | public function getPassword()
97 | {
98 | return $this->password;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/JeremyKendall/Password/Storage/IdentityMissingException.php:
--------------------------------------------------------------------------------
1 | decoratedValidator = $this->getMockBuilder('JeremyKendall\Password\PasswordValidatorInterface')
26 | ->disableOriginalConstructor()
27 | ->getMock();
28 |
29 | $this->decorator =
30 | $this->getMockBuilder('JeremyKendall\Password\Decorator\AbstractDecorator')
31 | ->setConstructorArgs(array($this->decoratedValidator))
32 | ->getMockForAbstractClass();
33 | }
34 |
35 | public function testIsValidWithoutOptionalArgs()
36 | {
37 | $this->decoratedValidator->expects($this->once())
38 | ->method('isValid')
39 | ->with('password', 'passwordHash', null, null);
40 |
41 | $this->decorator->isValid('password', 'passwordHash');
42 | }
43 |
44 | public function testIsValidWithOptionalArgs()
45 | {
46 | $this->decoratedValidator->expects($this->once())
47 | ->method('isValid')
48 | ->with('password', 'passwordHash', 'legacySalt', 'identity');
49 |
50 | $this->decorator->isValid('password', 'passwordHash', 'legacySalt', 'identity');
51 | }
52 |
53 | public function testRehash()
54 | {
55 | $this->decoratedValidator->expects($this->once())
56 | ->method('rehash')
57 | ->with('password');
58 |
59 | $this->decorator->rehash('password');
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/tests/JeremyKendall/Password/Tests/Decorator/IntegrationTest.php:
--------------------------------------------------------------------------------
1 | callback = function ($credential, $passwordHash) {
31 | if (hash('sha512', $credential) === $passwordHash) {
32 | return true;
33 | }
34 |
35 | return false;
36 | };
37 | $this->storage = $this->getMock('JeremyKendall\Password\Storage\StorageInterface');
38 | }
39 |
40 | public function testLegacyPasswordIsValidUpgradedRehashedStored()
41 | {
42 | $validator = new UpgradeDecorator(
43 | new StorageDecorator(
44 | new PasswordValidator(),
45 | $this->storage
46 | ),
47 | $this->callback
48 | );
49 | $password = 'password';
50 | $hash = hash('sha512', $password);
51 | $identity = 'username';
52 |
53 | $this->storage->expects($this->once())
54 | ->method('updatePassword')
55 | ->with($identity, $this->stringContains('$2y$10$'));
56 |
57 | $result = $validator->isValid($password, $hash, null, $identity);
58 |
59 | $this->assertTrue($result->isValid());
60 | $this->assertEquals(
61 | ValidationResult::SUCCESS_PASSWORD_REHASHED,
62 | $result->getCode()
63 | );
64 | $this->assertNotNull($result->getPassword());
65 | $this->assertStringStartsWith('$2y$10$', $result->getPassword());
66 | }
67 |
68 | public function testLegacyPasswordIsValidUpgradedRehashedStored2()
69 | {
70 | $validator = new StorageDecorator(
71 | new UpgradeDecorator(
72 | new PasswordValidator(),
73 | $this->callback
74 | ),
75 | $this->storage
76 | );
77 | $password = 'password';
78 | $hash = hash('sha512', $password);
79 | $identity = 'username';
80 |
81 | $this->storage->expects($this->once())
82 | ->method('updatePassword')
83 | ->with($identity, $this->stringContains('$2y$10$'));
84 |
85 | $result = $validator->isValid($password, $hash, null, $identity);
86 |
87 | $this->assertTrue($result->isValid());
88 | $this->assertEquals(
89 | ValidationResult::SUCCESS_PASSWORD_REHASHED,
90 | $result->getCode()
91 | );
92 | $this->assertStringStartsWith('$2y$10$', $result->getPassword());
93 | }
94 |
95 | public function testLegacyPasswordIsValidUpgradedRehashedWhenClientCodeUsesSameParametersAsUpgradeDecorator()
96 | {
97 | $validator = new UpgradeDecorator(
98 | new PasswordValidator(),
99 | $this->callback
100 | );
101 | $password = 'password';
102 | $hash = hash('sha512', $password);
103 | $validator->setOptions(array('cost' => UpgradeDecorator::DEFAULT_REHASH_COST));
104 | $result = $validator->isValid($password, $hash);
105 | $this->assertTrue($result->isValid(), "Failed asserting that result is valid");
106 | $this->assertEquals(
107 | ValidationResult::SUCCESS_PASSWORD_REHASHED,
108 | $result->getCode(),
109 | "Failed asserting that password was rehashed"
110 | );
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/tests/JeremyKendall/Password/Tests/Decorator/KarptoniteRehashUpgradeDecoratorTest.php:
--------------------------------------------------------------------------------
1 | validationCallback = function ($credential, $passwordHash, $salt) {
39 | // Recreate the legacy hash. This was the persisted password hash
40 | // prior to upgrading.
41 | $legacyHash = hash('sha512', $credential . $salt);
42 |
43 | // Now test the old hash against the new, upgraded hash
44 | if (password_verify($legacyHash, $passwordHash)) {
45 | return true;
46 | }
47 |
48 | return false;
49 | };
50 |
51 | $interface = 'JeremyKendall\Password\PasswordValidatorInterface';
52 | $this->decoratedValidator = $this->getMockBuilder($interface)
53 | ->disableOriginalConstructor()
54 | ->getMock();
55 |
56 | $this->decorator = new UpgradeDecorator(
57 | $this->decoratedValidator,
58 | $this->validationCallback
59 | );
60 |
61 | $this->plainTextPassword = 'password';
62 | $this->legacySalt = mt_rand(1000, 1000000);
63 |
64 | $legacyHash = hash('sha512', $this->plainTextPassword . $this->legacySalt);
65 | $this->upgradedLegacyHash = password_hash($legacyHash, PASSWORD_DEFAULT);
66 | }
67 |
68 | public function testRehashingPasswordHashesScenarioCredentialIsValid()
69 | {
70 | $finalValidatorRehash = password_hash($this->plainTextPassword, PASSWORD_DEFAULT);
71 |
72 | $validResult = new ValidationResult(
73 | ValidationResult::SUCCESS_PASSWORD_REHASHED,
74 | $finalValidatorRehash
75 | );
76 |
77 | $this->decoratedValidator->expects($this->once())
78 | ->method('isValid')
79 | ->with($this->plainTextPassword, $this->isType('string'), $this->legacySalt)
80 | ->will($this->returnValue($validResult));
81 |
82 | $result = $this->decorator->isValid(
83 | $this->plainTextPassword,
84 | $this->upgradedLegacyHash,
85 | $this->legacySalt
86 | );
87 |
88 | $this->assertTrue($result->isValid());
89 | $this->assertEquals(
90 | ValidationResult::SUCCESS_PASSWORD_REHASHED,
91 | $result->getCode()
92 | );
93 |
94 | // Final rehashed password is a valid hash
95 | $this->assertTrue(
96 | password_verify($this->plainTextPassword, $result->getPassword())
97 | );
98 | }
99 |
100 | public function testRehashingPasswordHashesScenarioCredentialIsNotValid()
101 | {
102 | $wrongPlainTextPassword = 'i-forgot-my-password';
103 |
104 | $invalidResult = new ValidationResult(
105 | ValidationResult::FAILURE_PASSWORD_INVALID
106 | );
107 |
108 | $this->decoratedValidator->expects($this->never())
109 | ->method('rehash');
110 |
111 | $this->decoratedValidator->expects($this->once())
112 | ->method('isValid')
113 | ->with($wrongPlainTextPassword, $this->upgradedLegacyHash, $this->legacySalt)
114 | ->will($this->returnValue($invalidResult));
115 |
116 | $result = $this->decorator->isValid(
117 | $wrongPlainTextPassword,
118 | $this->upgradedLegacyHash,
119 | $this->legacySalt
120 | );
121 |
122 | $this->assertFalse($result->isValid());
123 | $this->assertEquals(
124 | ValidationResult::FAILURE_PASSWORD_INVALID,
125 | $result->getCode()
126 | );
127 | }
128 |
129 | /**
130 | * @dataProvider callbackDataProvider
131 | */
132 | public function testVerifyValidationCallback($password, $result)
133 | {
134 | $isValid = call_user_func(
135 | $this->validationCallback,
136 | $password,
137 | $this->upgradedLegacyHash,
138 | $this->legacySalt
139 | );
140 |
141 | $this->assertEquals($result, $isValid);
142 | }
143 |
144 | public function callbackDataProvider()
145 | {
146 | return array(
147 | array('password', true),
148 | array('wrong-password', false),
149 | );
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/tests/JeremyKendall/Password/Tests/Decorator/StorageDecoratorTest.php:
--------------------------------------------------------------------------------
1 | storage = $this->getMock('JeremyKendall\Password\Storage\StorageInterface');
30 | $this->decoratedValidator = $this->getMockBuilder('JeremyKendall\Password\PasswordValidatorInterface')
31 | ->disableOriginalConstructor()
32 | ->getMock();
33 | $this->decorator = new StorageDecorator(
34 | $this->decoratedValidator,
35 | $this->storage
36 | );
37 | }
38 |
39 | public function testPasswordValidPasswordRehashedAndStored()
40 | {
41 | $valid = new ValidationResult(
42 | ValidationResult::SUCCESS_PASSWORD_REHASHED,
43 | 'rehashedPassword'
44 | );
45 |
46 | $this->storage->expects($this->once())
47 | ->method('updatePassword')
48 | ->with('username', 'rehashedPassword');
49 |
50 | $this->decoratedValidator->expects($this->once())
51 | ->method('isValid')
52 | ->with('password', 'passwordHash', null, 'username')
53 | ->will($this->returnValue($valid));
54 |
55 | $result = $this->decorator->isValid('password', 'passwordHash', null, 'username');
56 |
57 | $this->assertTrue($result->isValid());
58 | $this->assertEquals(
59 | ValidationResult::SUCCESS_PASSWORD_REHASHED,
60 | $result->getCode()
61 | );
62 | }
63 |
64 | public function testFailureToProvideIdentityThrowsException()
65 | {
66 | $this->setExpectedException(
67 | 'JeremyKendall\Password\Storage\IdentityMissingException',
68 | 'The StorageDecorator requires an $identity argument.'
69 | );
70 |
71 | $valid = new ValidationResult(
72 | ValidationResult::SUCCESS_PASSWORD_REHASHED,
73 | 'rehashedPassword'
74 | );
75 |
76 | $this->storage->expects($this->never())
77 | ->method('updatePassword');
78 |
79 | $this->decoratedValidator->expects($this->once())
80 | ->method('isValid')
81 | ->with('password', 'passwordHash')
82 | ->will($this->returnValue($valid));
83 |
84 | $result = $this->decorator->isValid('password', 'passwordHash');
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/tests/JeremyKendall/Password/Tests/Decorator/UpgradeDecoratorTest.php:
--------------------------------------------------------------------------------
1 | validationCallback = function ($credential, $passwordHash) {
28 | if (hash('sha512', $credential) === $passwordHash) {
29 | return true;
30 | }
31 |
32 | return false;
33 | };
34 |
35 | $this->decoratedValidator = $this->getMockBuilder('JeremyKendall\Password\PasswordValidatorInterface')
36 | ->disableOriginalConstructor()
37 | ->getMock();
38 |
39 | $this->decorator = new UpgradeDecorator(
40 | $this->decoratedValidator,
41 | $this->validationCallback
42 | );
43 | }
44 |
45 | public function testPasswordValidAndPasswordRehashed()
46 | {
47 | $password = 'password';
48 | $passwordHash = hash('sha512', $password);
49 |
50 | $validatorRehash = password_hash($password, PASSWORD_DEFAULT);
51 |
52 | $result = new ValidationResult(
53 | ValidationResult::SUCCESS_PASSWORD_REHASHED,
54 | $validatorRehash
55 | );
56 |
57 | $this->decoratedValidator->expects($this->once())
58 | ->method('isValid')
59 | ->with($password, $this->isType('string'))
60 | ->will($this->returnValue($result));
61 |
62 | $result = $this->decorator->isValid($password, $passwordHash);
63 |
64 | $this->assertTrue($result->isValid());
65 | $this->assertEquals(
66 | ValidationResult::SUCCESS_PASSWORD_REHASHED,
67 | $result->getCode()
68 | );
69 | // Rehashed password is a valid hash
70 | $this->assertTrue(password_verify($password, $result->getPassword()));
71 | }
72 |
73 | public function testLegacyPasswordInvalidDecoratedValidatorTakesOver()
74 | {
75 | $passwordHash = hash('sha512', 'passwordz');
76 |
77 | $this->decoratedValidator->expects($this->never())
78 | ->method('rehash');
79 |
80 | $invalid = new ValidationResult(
81 | ValidationResult::FAILURE_PASSWORD_INVALID
82 | );
83 |
84 | $this->decoratedValidator->expects($this->once())
85 | ->method('isValid')
86 | ->with('password', $passwordHash)
87 | ->will($this->returnValue($invalid));
88 |
89 | $result = $this->decorator->isValid('password', $passwordHash);
90 |
91 | $this->assertFalse($result->isValid());
92 | $this->assertEquals(
93 | ValidationResult::FAILURE_PASSWORD_INVALID,
94 | $result->getCode()
95 | );
96 | }
97 |
98 | public function testPasswordHashPasswordValidDecoratedValidatorTakesOver()
99 | {
100 | $passwordHash = password_hash('password', PASSWORD_DEFAULT);
101 |
102 | $this->decoratedValidator->expects($this->never())
103 | ->method('rehash');
104 |
105 | $valid = new ValidationResult(
106 | ValidationResult::SUCCESS
107 | );
108 |
109 | $this->decoratedValidator->expects($this->once())
110 | ->method('isValid')
111 | ->with('password', $passwordHash)
112 | ->will($this->returnValue($valid));
113 |
114 | $result = $this->decorator->isValid('password', $passwordHash);
115 |
116 | $this->assertTrue($result->isValid());
117 | $this->assertEquals(
118 | ValidationResult::SUCCESS,
119 | $result->getCode()
120 | );
121 | $this->assertNull($result->getPassword());
122 | }
123 |
124 | public function testGetSetOptions()
125 | {
126 | $this->decoratedValidator->expects($this->at(0))
127 | ->method('getOptions')
128 | ->will($this->returnValue(array()));
129 |
130 | $this->decoratedValidator->expects($this->at(1))
131 | ->method('setOptions')
132 | ->with(array('cost' => '11'));
133 |
134 | $this->decoratedValidator->expects($this->at(2))
135 | ->method('getOptions')
136 | ->will($this->returnValue(array('cost' => '11')));
137 |
138 | $this->assertEquals(array(), $this->decorator->getOptions());
139 | $this->decorator->setOptions(array('cost' => '11'));
140 | $this->assertEquals(array('cost' => '11'), $this->decorator->getOptions());
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/tests/JeremyKendall/Password/Tests/PasswordValidatorTest.php:
--------------------------------------------------------------------------------
1 | validator = new PasswordValidator();
24 | }
25 |
26 | public function testPasswordIsValidDoesNotNeedRehash()
27 | {
28 | $passwordHash = password_hash('password', PASSWORD_DEFAULT);
29 |
30 | $result = $this->validator->isValid('password', $passwordHash);
31 |
32 | $this->assertTrue($result->isValid());
33 | $this->assertEquals(
34 | ValidationResult::SUCCESS,
35 | $result->getCode()
36 | );
37 | $this->assertNull($result->getPassword());
38 | }
39 |
40 | public function testPasswordIsValidAndIsRehashed()
41 | {
42 | $options = array('cost' => 9);
43 | $passwordHash = password_hash('password', PASSWORD_DEFAULT, $options);
44 | $this->assertStringStartsWith('$2y$09$', $passwordHash);
45 |
46 | $result = $this->validator->isValid('password', $passwordHash);
47 |
48 | $this->assertTrue($result->isValid());
49 | $this->assertEquals(
50 | ValidationResult::SUCCESS_PASSWORD_REHASHED,
51 | $result->getCode()
52 | );
53 | $this->assertStringStartsWith('$2y$10$', $result->getPassword());
54 | // Rehashed password is a valid hash
55 | $this->assertTrue(password_verify('password', $result->getPassword()));
56 | }
57 |
58 | public function testCostNineHashValidAndNotRehashedBecauseOptions()
59 | {
60 | $options = array('cost' => 9);
61 | $passwordHash = password_hash('password', PASSWORD_DEFAULT, $options);
62 | $this->assertStringStartsWith('$2y$09$', $passwordHash);
63 |
64 | $this->validator->setOptions($options);
65 | $result = $this->validator->isValid('password', $passwordHash);
66 |
67 | $this->assertTrue($result->isValid());
68 | $this->assertEquals(
69 | ValidationResult::SUCCESS,
70 | $result->getCode()
71 | );
72 | $this->assertNull($result->getPassword());
73 | }
74 |
75 | public function testPasswordIsInvalid()
76 | {
77 | $passwordHash = password_hash('passwordz', PASSWORD_DEFAULT);
78 |
79 | $result = $this->validator->isValid('password', $passwordHash);
80 |
81 | $this->assertFalse($result->isValid());
82 | $this->assertEquals(
83 | ValidationResult::FAILURE_PASSWORD_INVALID,
84 | $result->getCode()
85 | );
86 | $this->assertNull($result->getPassword());
87 | }
88 |
89 | public function testGetSetOptions()
90 | {
91 | $this->assertEquals(array(), $this->validator->getOptions());
92 | $this->validator->setOptions(array('cost' => '11'));
93 | $this->assertEquals(array('cost' => '11'), $this->validator->getOptions());
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | add('JeremyKendall\\Password\\Tests\\', __DIR__);
18 |
--------------------------------------------------------------------------------
/travis.phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | tests
5 |
6 |
7 |
8 | ./src
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | JeremyKendall\Password
26 |
27 |
28 | KusjEdb4FdcqrUZejvepYNdVyAeZBTHcq
29 |
30 |
31 | https://coveralls.io/api/v1/jobs
32 |
33 |
34 | /tmp/jeremykendall/password-validator
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------