Skip to content

Commit fba8976

Browse files
authored
Merge pull request #8 from activecollab/feature/password-handling
Port password handling from Active Collab application
2 parents 00243ee + 8c3a06b commit fba8976

17 files changed

+915
-66
lines changed

README.md

+87
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,90 @@ Authentication library builds on top of `activecollab/user` package. There are t
99
1. Users with accounts - People with actual accounts in our application.
1010

1111
Only users with accounts in our application can be authenticated.
12+
13+
## Working with Passwords
14+
15+
### Hashing and Validating Passwords
16+
17+
Passwords can be hashed using one of the three mechanisms:
18+
19+
1. PHP's built in `password_*` functions. This is default and recommended method
20+
1. Using PBKDF2
21+
1. Using SHA1
22+
23+
Later two are there for compatibility reasons only, so you can transition your hashed passwords to PHP's password management system if you have not done that already. Password manager's `needsRehash()` method will always recommend rehashing for PBKDF2 and SHA1 hashed passwords.
24+
25+
Example:
26+
27+
```php
28+
$manager = new PasswordManager('global salt, if needed');
29+
30+
$hash = $manager->hash('easy to remember, hard to guess');
31+
32+
if ($manager->verify('easy to remember, hard to guess', $hash, PasswordManagerInterface::HASHED_WITH_PHP)) {
33+
print "All good\n";
34+
} else {
35+
print "Not good\n";
36+
}
37+
```
38+
39+
Library offers a way to check if password needs to be rehashed, usually after you successfully checked if password that user provided is correct one:
40+
41+
```php
42+
$manager = new PasswordManager('global salt, if needed');
43+
44+
if ($manager->verify($user_provided_password, $hash_from_storage, PasswordManagerInterface::HASHED_WITH_PHP)) {
45+
if ($manager->needsRehash($hash_from_storage, PasswordManagerInterface::HASHED_WITH_PHP)) {
46+
// Update hash in our data storage
47+
}
48+
49+
// Proceed with user authentication
50+
} else {
51+
print "Invalid password\n";
52+
}
53+
```
54+
55+
### Password Policy
56+
57+
All passwords are validated against password policies. By default, policy will accept any non-empty string:
58+
59+
```php
60+
(new PasswordStrengthValidator())->isPasswordValid('weak', new PasswordPolicy()); // Will return TRUE
61+
```
62+
63+
Policy can enforce following rules:
64+
65+
1. Password is longer than N characters
66+
1. Password contains at least one number
67+
1. Password contains mixed case (uppercase and lowercase) letters
68+
1. Password contains at least one of the following symbols: `,.;:!$\%^&~@#*`
69+
70+
Here's an example where all rules are enforced:
71+
72+
```php
73+
// Weak password, not accepted
74+
(new PasswordStrengthValidator())->isPasswordValid('weak', new PasswordPolicy(32, true, true, true));
75+
76+
// Strong password, accepted
77+
(new PasswordStrengthValidator())->isPasswordValid('BhkXuemYY#WMdU;QQd4QpXpcEjbw2XHP', new PasswordPolicy(32, true, true, true));
78+
```
79+
80+
### Generating Random Passwords
81+
82+
Password strength validator can also be used to prepare new passwords that meed the requirements of provided policies:
83+
84+
```php
85+
$validator = new PasswordStrengthValidator();
86+
$policy = new PasswordPolicy(32, true, true, true);
87+
88+
// Prepare 32 characters long password that mixes case, numbers and symbols
89+
$password = $validator->generateValidPassword(32, $policy);
90+
```
91+
92+
Password generator uses letters and numbers by default, unless symbols are required by the provided password policy.
93+
94+
Note that generator may throw an exeception if it fails to prepare a password in 10000 tries.
95+
96+
## To Do
97+
98+
1. Consider adding previously used passwords repository, so library can enforce no-repeat policy for passwords

composer.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
],
1717
"require": {
1818
"php": ">=5.6.0",
19+
"ext-mbstring": "*",
1920
"activecollab/user": "^3.0",
2021
"activecollab/cookies": "~0.1",
21-
"guzzlehttp/psr7": "~1.2",
22-
"google/apiclient": "^1.1"
22+
"guzzlehttp/psr7": "^1.2",
23+
"google/apiclient": "^1.1",
24+
"ircmaxell/random-lib" : "^1.2"
2325
},
2426
"require-dev": {
2527
"friendsofphp/php-cs-fixer": "^1.0",

composer.lock

+105-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Authorizer/AuthorizerInterface.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ interface AuthorizerInterface
1616
/**
1717
* Perform user credentials verification against the real user database provider.
1818
*
19-
* @param array $credentials
20-
* @return AuthenticatedUserInterface|null
19+
* @param array $credentials
20+
* @return \ActiveCollab\Authentication\AuthenticatedUser\AuthenticatedUserInterface|null
2121
*/
2222
public function verifyCredentials(array $credentials);
2323

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Active Collab Authentication project.
5+
*
6+
* (c) A51 doo <[email protected]>. All rights reserved.
7+
*/
8+
9+
namespace ActiveCollab\Authentication\Password\Manager;
10+
11+
use InvalidArgumentException;
12+
13+
/**
14+
* @package ActiveCollab\Authentication\Password
15+
*/
16+
class PasswordManager implements PasswordManagerInterface
17+
{
18+
/**
19+
* @var string
20+
*/
21+
private $global_salt;
22+
23+
/**
24+
* @param string $global_salt
25+
*/
26+
public function __construct($global_salt = '')
27+
{
28+
$this->global_salt = (string) $global_salt;
29+
}
30+
31+
/**
32+
* {@inheritdoc}
33+
*/
34+
public function verify($password, $hash, $hashed_with)
35+
{
36+
switch ($hashed_with) {
37+
case self::HASHED_WITH_PHP:
38+
return password_verify($this->global_salt . $password, $hash);
39+
case self::HASHED_WITH_PBKDF2:
40+
case self::HASHED_WITH_SHA1:
41+
return $this->hash($password, $hashed_with) === $hash;
42+
default:
43+
throw new InvalidArgumentException("Hashing mechanism '$hashed_with' is not supported");
44+
}
45+
}
46+
47+
/**
48+
* {@inheritdoc}
49+
*/
50+
public function hash($password, $hash_with = self::HASHED_WITH_PHP)
51+
{
52+
switch ($hash_with) {
53+
case self::HASHED_WITH_PHP:
54+
return password_hash($this->global_salt . $password, PASSWORD_DEFAULT);
55+
case self::HASHED_WITH_PBKDF2:
56+
return base64_encode($this->pbkdf2($password, $this->global_salt, 1000, 40));
57+
case self::HASHED_WITH_SHA1:
58+
return sha1($this->global_salt . $password);
59+
default:
60+
throw new InvalidArgumentException("Hashing mechanism '$hash_with' is not supported");
61+
}
62+
}
63+
64+
/**
65+
* {@inheritdoc}
66+
*/
67+
public function needsRehash($hash, $hashed_with)
68+
{
69+
if ($hashed_with === self::HASHED_WITH_PHP) {
70+
return password_needs_rehash($hash, PASSWORD_DEFAULT);
71+
} else {
72+
return true;
73+
}
74+
}
75+
76+
/**
77+
* PBKDF2 Implementation (described in RFC 2898).
78+
*
79+
* Source: http://www.itnewb.com/tutorial/Encrypting-Passwords-with-PHP-for-Storage-Using-the-RSA-PBKDF2-Standard
80+
*
81+
* @param string $p password
82+
* @param string $s salt
83+
* @param int $c iteration count (use 1000 or higher)
84+
* @param int $kl derived key length
85+
* @param string $a hash algorithm
86+
* @return string derived key
87+
*/
88+
private function pbkdf2($p, $s, $c, $kl, $a = 'sha256')
89+
{
90+
$hl = strlen(hash($a, null, true)); # Hash length
91+
$kb = ceil($kl / $hl); # Key blocks to compute
92+
$dk = ''; # Derived key
93+
94+
# Create key
95+
for ($block = 1; $block <= $kb; ++$block) {
96+
97+
# Initial hash for this block
98+
$ib = $b = hash_hmac($a, $s . pack('N', $block), $p, true);
99+
100+
# Perform block iterations
101+
for ($i = 1; $i < $c; ++$i) {
102+
# XOR each iterate
103+
104+
$ib ^= ($b = hash_hmac($a, $b, $p, true));
105+
}
106+
107+
$dk .= $ib; # Append iterated block
108+
}
109+
110+
# Return derived key of correct length
111+
112+
return substr($dk, 0, $kl);
113+
}
114+
}

0 commit comments

Comments
 (0)