Skip to content

Commit 052851d

Browse files
committed
Tests checked in for password-strength checker.
Adding password-strength checker functionality. Modified docs to support option.
1 parent dabc284 commit 052851d

File tree

9 files changed

+123
-17
lines changed

9 files changed

+123
-17
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ configure it like so:
6969
],
7070

7171
Writing your own should be very simple, see provided tests.
72+
73+
## Pluggable Password Strength Checker
74+
75+
You can use the built-in support for [paragonie/passwdqc](https://github.com/paragonie/passwdqc) by uncommenting the password_strength_checker config key. You can also roll your own if you have more complex needs; uncomment the key and specify your own implementation of [PasswordCheckerInterface](src/CirclicalUser/Provider/PasswordCheckerInterface.php). This will cause the
76+
password input routines to throw WeakPasswordExceptions when weak input is received.
77+
7278
7379
7480
## Creating Access For Your Users

bundle/Spec/CirclicalUser/Service/AuthenticationServiceSpec.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use CirclicalUser\Entity\Authentication;
66
use CirclicalUser\Exception\PersistedUserRequiredException;
7+
use CirclicalUser\Exception\WeakPasswordException;
78
use CirclicalUser\Provider\AuthenticationRecordInterface;
89
use CirclicalUser\Provider\UserInterface as User;
910
use CirclicalUser\Exception\BadPasswordException;
@@ -14,6 +15,7 @@
1415
use CirclicalUser\Mapper\AuthenticationMapper;
1516
use CirclicalUser\Mapper\UserMapper;
1617
use CirclicalUser\Service\AuthenticationService;
18+
use CirclicalUser\Service\PasswordChecker\Passwdqc;
1719
use ParagonIE\Halite\KeyFactory;
1820
use ParagonIE\Halite\Symmetric\Crypto;
1921
use ParagonIE\Halite\Symmetric\EncryptionKey;
@@ -415,4 +417,30 @@ public function it_requires_that_users_have_id_during_creation(User $otherUser)
415417
$otherUser->getId()->willReturn(null);
416418
$this->shouldThrow(PersistedUserRequiredException::class)->during('create', [$otherUser, 'whoami', 'nobody']);
417419
}
420+
421+
public function it_will_create_new_auth_records_with_strong_passwords($authenticationMapper, User $user5, AuthenticationRecordInterface $newAuth, $userMapper)
422+
{
423+
$this->beConstructedWith($authenticationMapper, $userMapper, $this->systemEncryptionKey->getRawKeyMaterial(), false, false, new Passwdqc());
424+
425+
$newAuth->getSessionKey()->willReturn(KeyFactory::generateEncryptionKey()->getRawKeyMaterial());
426+
$newAuth->getUsername()->willReturn('email');
427+
$newAuth->getUserId()->willReturn(5);
428+
$user5->getId()->willReturn(5);
429+
430+
$authenticationMapper->save(Argument::type(AuthenticationRecordInterface::class))->shouldBeCalled();
431+
$authenticationMapper->create(Argument::type('integer'), Argument::type('string'), Argument::type('string'), Argument::type('string'))->willReturn($newAuth);
432+
$this->create($user5, 'userC', 'beestring')->shouldBeAnInstanceOf(AuthenticationRecordInterface::class);
433+
}
434+
435+
public function it_wont_create_new_auth_records_with_weak_passwords($authenticationMapper, User $user5, AuthenticationRecordInterface $newAuth, $userMapper)
436+
{
437+
$this->beConstructedWith($authenticationMapper, $userMapper, $this->systemEncryptionKey->getRawKeyMaterial(), false, false, new Passwdqc());
438+
439+
$newAuth->getSessionKey()->willReturn(KeyFactory::generateEncryptionKey()->getRawKeyMaterial());
440+
$newAuth->getUsername()->willReturn('email');
441+
$newAuth->getUserId()->willReturn(5);
442+
$user5->getId()->willReturn(5);
443+
444+
$this->shouldThrow(WeakPasswordException::class)->during('create', [$user5, 'userC', '123456']);
445+
}
418446
}

composer.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@
3737
"require-dev": {
3838
"phpspec/phpspec": "^3.1",
3939
"henrikbjorn/phpspec-code-coverage": "^3.0",
40-
"codacy/coverage": "^1.0"
40+
"codacy/coverage": "^1.0",
41+
"paragonie/passwdqc": "dev-master"
42+
},
43+
"suggest":{
44+
"paragonie/passwdqc": "Add built-in support for password-strength checking! See README for configuration details."
4145
},
4246
"autoload": {
4347
"psr-4": {

config/module.config.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use CirclicalUser\Service\AccessService;
2222
use CirclicalUser\Service\AuthenticationService;
2323
use CirclicalUser\Factory\Service\AuthenticationServiceFactory;
24+
use CirclicalUser\Service\PasswordChecker\Passwdqc;
2425
use CirclicalUser\Strategy\RedirectStrategy;
2526

2627
return [
@@ -38,6 +39,7 @@
3839
'user' => UserPermissionMapper::class,
3940
],
4041
],
42+
//'password_strength_checker' => Passwdqc::class,
4143
],
4244
],
4345

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace CirclicalUser\Exception;
4+
5+
class WeakPasswordException extends \Exception
6+
{
7+
8+
}

src/CirclicalUser/Factory/Service/AuthenticationServiceFactory.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace CirclicalUser\Factory\Service;
44

5+
use CirclicalUser\Provider\PasswordCheckerInterface;
56
use Interop\Container\ContainerInterface;
67
use Interop\Container\Exception\ContainerException;
78
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
@@ -31,15 +32,25 @@ public function __invoke(ContainerInterface $container, $requestedName, array $o
3132
$config = $container->get('config');
3233
$userConfig = $config['circlical']['user'];
3334

34-
$userProvider = isset($userConfig['providers']['user']) ? $userConfig['providers']['user'] : UserMapper::class;
35-
$authMapper = isset($userConfig['providers']['auth']) ? $userConfig['providers']['auth'] : AuthenticationMapper::class;
35+
$userProvider = $userConfig['providers']['user'] ?? UserMapper::class;
36+
$authMapper = $userConfig['providers']['auth'] ?? AuthenticationMapper::class;
37+
$passwordChecker = null;
38+
39+
if( !empty( $userConfig['password_strength_checker'] ) ){
40+
$checkerImplementation = new $userConfig['password_strength_checker'];
41+
if( $checkerImplementation instanceof PasswordCheckerInterface ){
42+
$passwordChecker = $checkerImplementation;
43+
}
44+
}
45+
3646

3747
return new AuthenticationService(
3848
$container->get($authMapper),
3949
$container->get($userProvider),
4050
base64_decode($userConfig['auth']['crypto_key']),
4151
$userConfig['auth']['transient'],
42-
false
52+
false,
53+
$passwordChecker
4354
);
4455
}
4556
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace CirclicalUser\Provider;
4+
5+
interface PasswordCheckerInterface
6+
{
7+
public function isStrongPassword(string $clearPassword): bool;
8+
}

src/CirclicalUser/Service/AuthenticationService.php

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55

66
use CirclicalUser\Exception\PersistedUserRequiredException;
7+
use CirclicalUser\Exception\WeakPasswordException;
78
use CirclicalUser\Provider\AuthenticationProviderInterface;
89
use CirclicalUser\Provider\AuthenticationRecordInterface;
10+
use CirclicalUser\Provider\PasswordCheckerInterface;
911
use CirclicalUser\Provider\UserInterface as User;
1012
use CirclicalUser\Exception\BadPasswordException;
1113
use CirclicalUser\Exception\EmailUsernameTakenException;
@@ -82,6 +84,11 @@ class AuthenticationService
8284
private $secure;
8385

8486

87+
/**
88+
* @var PasswordCheckerInterface
89+
*/
90+
private $passwordChecker;
91+
8592
/**
8693
* AuthenticationService constructor.
8794
*
@@ -90,15 +97,16 @@ class AuthenticationService
9097
* @param string $systemEncryptionKey The raw material of a Halite-generated encryption key, stored in config.
9198
* @param bool $transient True if cookies should expire at the end of the session (zero value, for expiry)
9299
* @param bool $secure True if cookies should be marked as 'Secure', enforced as 'true' in production by this service's Factory
100+
* @param PasswordCheckerInterface $passwordChecker Optional, a password checker implementation
93101
*/
94-
public function __construct(AuthenticationProviderInterface $authenticationProvider, UserProviderInterface $userProvider, $systemEncryptionKey, $transient, $secure)
102+
public function __construct(AuthenticationProviderInterface $authenticationProvider, UserProviderInterface $userProvider, string $systemEncryptionKey, bool $transient, bool $secure, $passwordChecker = null)
95103
{
96104
$this->authenticationProvider = $authenticationProvider;
97105
$this->userProvider = $userProvider;
98106
$this->systemEncryptionKey = $systemEncryptionKey;
99107
$this->transient = $transient;
100108
$this->secure = $secure;
101-
$this->identity = null;
109+
$this->passwordChecker = $passwordChecker;
102110
}
103111

104112
/**
@@ -107,7 +115,7 @@ public function __construct(AuthenticationProviderInterface $authenticationProvi
107115
*/
108116
public function hasIdentity(): bool
109117
{
110-
return $this->getIdentity() != null;
118+
return $this->getIdentity() !== null;
111119
}
112120

113121
/**
@@ -133,7 +141,7 @@ private function setIdentity(User $user)
133141
* @throws BadPasswordException Thrown when the password doesn't work
134142
* @throws NoSuchUserException Thrown when the user can't be identified
135143
*/
136-
public function authenticate($username, $password): User
144+
public function authenticate(string $username, string $password): User
137145
{
138146
$auth = $this->authenticationProvider->findByUsername($username);
139147
$user = null;
@@ -185,7 +193,7 @@ public function authenticate($username, $password): User
185193
* @throws NoSuchUserException Thrown when the user's authentication records couldn't be found
186194
* @throws UsernameTakenException
187195
*/
188-
public function changeUsername(User $user, $newUsername): AuthenticationRecordInterface
196+
public function changeUsername(User $user, string $newUsername): AuthenticationRecordInterface
189197
{
190198
/** @var AuthenticationRecordInterface $auth */
191199
$auth = $this->authenticationProvider->findByUserId($user->getId());
@@ -220,7 +228,7 @@ private function setSessionCookies(AuthenticationRecordInterface $authentication
220228
$systemKey = new EncryptionKey($this->systemEncryptionKey);
221229
$userKey = new EncryptionKey($authentication->getSessionKey());
222230
$hashCookieName = hash_hmac('sha256', $authentication->getSessionKey() . $authentication->getUsername(), $systemKey);
223-
$userTuple = base64_encode(Crypto::encrypt($authentication->getUserId() . ":" . $hashCookieName, $systemKey));
231+
$userTuple = base64_encode(Crypto::encrypt($authentication->getUserId() . ':' . $hashCookieName, $systemKey));
224232
$hashCookieContents = base64_encode(Crypto::encrypt(time() . ':' . $authentication->getUserId() . ':' . $authentication->getUsername(), $userKey));
225233

226234
//
@@ -262,7 +270,7 @@ private function setSessionCookies(AuthenticationRecordInterface $authentication
262270
* @param $name
263271
* @param $value
264272
*/
265-
private function setCookie($name, $value)
273+
private function setCookie(string $name, $value)
266274
{
267275
$expiry = $this->transient ? 0 : (time() + 2629743);
268276
$sessionParameters = session_get_cookie_params();
@@ -332,7 +340,7 @@ public function getIdentity()
332340

333341
// paranoid, make sure we have everything we need
334342
@list($cookieUserId, $hashCookieSuffix) = @explode(":", $userTuple, 2);
335-
if (!isset($cookieUserId) || !isset($hashCookieSuffix) || !is_numeric($cookieUserId) || !trim($hashCookieSuffix)) {
343+
if (!isset($cookieUserId, $hashCookieSuffix) || !is_numeric($cookieUserId) || !trim($hashCookieSuffix)) {
336344
throw new \Exception();
337345
}
338346

@@ -364,7 +372,7 @@ public function getIdentity()
364372
// 3. Decrypt the hash cookie with the user key
365373
//
366374
$hashedCookieContents = Crypto::decrypt(base64_decode($_COOKIE[$hashCookieName]), $userKey);
367-
if (!substr_count($hashedCookieContents, ':') == 2) {
375+
if (!substr_count($hashedCookieContents, ':') === 2) {
368376
throw new \Exception();
369377
}
370378

@@ -405,12 +413,19 @@ private function purgeHashCookies(string $skipCookie = null)
405413
{
406414
$sp = session_get_cookie_params();
407415
foreach ($_COOKIE as $cookieName => $value) {
408-
if ($cookieName != $skipCookie && strpos($cookieName, self::COOKIE_HASH_PREFIX) !== false) {
416+
if ($cookieName !== $skipCookie && strpos($cookieName, self::COOKIE_HASH_PREFIX) !== false) {
409417
setcookie($cookieName, null, null, '/', $sp['domain'], false, true);
410418
}
411419
}
412420
}
413421

422+
private function enforcePasswordStrength(string $password)
423+
{
424+
if ($this->passwordChecker && !$this->passwordChecker->isStrongPassword($password)) {
425+
throw new WeakPasswordException();
426+
}
427+
}
428+
414429

415430
/**
416431
* Reset this user's password
@@ -419,9 +434,12 @@ private function purgeHashCookies(string $skipCookie = null)
419434
* @param string $newPassword Cleartext password that's being hashed
420435
*
421436
* @throws NoSuchUserException
437+
* @throws WeakPasswordException
422438
*/
423-
public function resetPassword(User $user, $newPassword)
439+
public function resetPassword(User $user, string $newPassword)
424440
{
441+
$this->enforcePasswordStrength($newPassword);
442+
425443
$auth = $this->authenticationProvider->findByUserId($user->getId());
426444
if (!$auth) {
427445
throw new NoSuchUserException();
@@ -442,9 +460,12 @@ public function resetPassword(User $user, $newPassword)
442460
* @return bool
443461
*
444462
* @throws NoSuchUserException
463+
* @throws WeakPasswordException
445464
*/
446465
public function verifyPassword(User $user, string $password): bool
447466
{
467+
$this->enforcePasswordStrength($password);
468+
448469
$auth = $this->authenticationProvider->findByUserId($user->getId());
449470
if (!$auth) {
450471
throw new NoSuchUserException();
@@ -467,6 +488,8 @@ public function verifyPassword(User $user, string $password): bool
467488
*/
468489
public function create(User $user, string $username, string $password): AuthenticationRecordInterface
469490
{
491+
$this->enforcePasswordStrength($password);
492+
470493
$auth = $this->registerAuthenticationRecord($user, $username, $password);
471494
$this->setSessionCookies($auth);
472495
$this->setIdentity($user);
@@ -500,12 +523,12 @@ public function registerAuthenticationRecord(User $user, string $username, strin
500523
}
501524

502525
if (filter_var($username, FILTER_VALIDATE_EMAIL)) {
503-
if ($user->getEmail() != $username) {
526+
if ($user->getEmail() !== $username) {
504527
throw new MismatchedEmailsException();
505528
}
506529

507530
if ($emailUser = $this->userProvider->findByEmail($username)) {
508-
if ($emailUser != $user) {
531+
if ($emailUser !== $user) {
509532
throw new EmailUsernameTakenException();
510533
}
511534
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace CirclicalUser\Service\PasswordChecker;
4+
5+
use CirclicalUser\Provider\PasswordCheckerInterface;
6+
7+
class Passwdqc implements PasswordCheckerInterface
8+
{
9+
10+
public function isStrongPassword(string $clearPassword): bool
11+
{
12+
$implementation = new \ParagonIE\Passwdqc\Passwdqc();
13+
14+
return $implementation->check($clearPassword);
15+
}
16+
}

0 commit comments

Comments
 (0)