Failed Conditions
Push — newinternal-releasecandidate ( 2e1778...b14046 )
by Simon
15:26 queued 05:35
created

PasswordCredentialProvider::authenticate()   B

Complexity

Conditions 9
Paths 9

Size

Total Lines 45
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 18
c 1
b 0
f 0
dl 0
loc 45
rs 8.0555
cc 9
nc 9
nop 2
1
<?php
2
/******************************************************************************
3
 * Wikipedia Account Creation Assistance tool                                 *
4
 *                                                                            *
5
 * All code in this file is released into the public domain by the ACC        *
6
 * Development Team. Please see team.json for a list of contributors.         *
7
 ******************************************************************************/
8
9
namespace Waca\Security\CredentialProviders;
10
11
use Waca\DataObjects\User;
12
use Waca\Exceptions\ApplicationLogicException;
13
use Waca\Exceptions\OptimisticLockFailedException;
14
use Waca\PdoDatabase;
15
use Waca\SessionAlert;
16
use Waca\SiteConfiguration;
0 ignored issues
show
Bug introduced by
The type Waca\SiteConfiguration was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use Wikimedia\PasswordBlacklist\PasswordBlacklist;
18
use ZxcvbnPhp\Zxcvbn;
19
20
class PasswordCredentialProvider extends CredentialProviderBase
21
{
22
    const PASSWORD_COST = 10;
23
    const PASSWORD_ALGO = PASSWORD_BCRYPT;
24
25
    public function __construct(PdoDatabase $database, SiteConfiguration $configuration)
26
    {
27
        parent::__construct($database, $configuration, 'password');
28
    }
29
30
    public function authenticate(User $user, $data)
31
    {
32
        $storedData = $this->getCredentialData($user->getId());
33
        if($storedData === null)
34
        {
35
            // No available credential matching these parameters
36
            return false;
37
        }
38
39
        if($storedData->getVersion() !== 2) {
40
            // Non-2 versions are not supported.
41
            return false;
42
        }
43
44
        if (!password_verify($data, $storedData->getData())) {
45
            return false;
46
        }
47
48
        if (password_needs_rehash($storedData->getData(), self::PASSWORD_ALGO,
49
            array('cost' => self::PASSWORD_COST))) {
50
            try {
51
                $this->reallySetCredential($user, $storedData->getFactor(), $data);
52
            }
53
            catch (OptimisticLockFailedException $e) {
54
                // optimistic lock failed, but no biggie. We'll catch it on the next login.
55
            }
56
        }
57
58
        $strengthTester = new Zxcvbn();
59
        $strength = $strengthTester->passwordStrength($data, [$user->getUsername(), $user->getOnWikiName(), $user->getEmail()]);
60
61
        /*  0 means the password is extremely guessable (within 10^3 guesses), dictionary words like 'password' or 'mother' score a 0
62
            1 is still very guessable (guesses < 10^6), an extra character on a dictionary word can score a 1
63
            2 is somewhat guessable (guesses < 10^8), provides some protection from unthrottled online attacks
64
            3 is safely unguessable (guesses < 10^10), offers moderate protection from offline slow-hash scenario
65
            4 is very unguessable (guesses >= 10^10) and provides strong protection from offline slow-hash scenario         */
66
67
        if ($strength['score'] <= 1 || PasswordBlacklist::isBlacklisted($data) || mb_strlen($data) < 8) {
68
            // prevent login for extremely weak passwords
69
            // at this point the user has authenticated via password, so they *know* it's weak.
70
            SessionAlert::error('Your password is too weak to permit login. Please choose the "forgotten your password" option below and set a new one.', null);
71
            return false;
72
        }
73
74
        return true;
75
    }
76
77
    /**
78
     * @param User   $user
79
     * @param int    $factor
80
     * @param string $password
81
     *
82
     * @throws OptimisticLockFailedException
83
     */
84
    private function reallySetCredential(User $user, int $factor, string $password) : void {
85
        $storedData = $this->getCredentialData($user->getId());
86
87
        if ($storedData === null) {
88
            $storedData = $this->createNewCredential($user);
89
        }
90
91
        $storedData->setData(password_hash($password, self::PASSWORD_ALGO, array('cost' => self::PASSWORD_COST)));
92
        $storedData->setFactor($factor);
93
        $storedData->setVersion(2);
94
95
        $storedData->save();
96
    }
97
98
    /**
99
     * @param User   $user
100
     * @param int    $factor
101
     * @param string $password
102
     *
103
     * @throws ApplicationLogicException
104
     * @throws OptimisticLockFailedException
105
     */
106
    public function setCredential(User $user, $factor, $password)
107
    {
108
        if (PasswordBlacklist::isBlacklisted($password)) {
109
            throw new ApplicationLogicException("Your new password is listed in the top 100,000 passwords. Please choose a stronger one.", null);
110
        }
111
112
        $strengthTester = new Zxcvbn();
113
        $strength = $strengthTester->passwordStrength($password, [$user->getUsername(), $user->getOnWikiName(), $user->getEmail()]);
114
115
        /*  0 means the password is extremely guessable (within 10^3 guesses), dictionary words like 'password' or 'mother' score a 0
116
            1 is still very guessable (guesses < 10^6), an extra character on a dictionary word can score a 1
117
            2 is somewhat guessable (guesses < 10^8), provides some protection from unthrottled online attacks
118
            3 is safely unguessable (guesses < 10^10), offers moderate protection from offline slow-hash scenario
119
            4 is very unguessable (guesses >= 10^10) and provides strong protection from offline slow-hash scenario         */
120
121
        if ($strength['score'] <= 2 || mb_strlen($password) < 8) {
122
            throw new ApplicationLogicException("Your new password is too weak. Please choose a stronger one.", null);
123
        }
124
125
        if ($strength['score'] <= 3) {
126
            SessionAlert::warning("Your new password is not as strong as it could be. Consider replacing it with a stronger password.", null);
127
        }
128
129
        $this->reallySetCredential($user, $factor, $password);
130
    }
131
132
    /**
133
     * @param User $user
134
     *
135
     * @throws ApplicationLogicException
136
     */
137
    public function deleteCredential(User $user)
138
    {
139
        throw new ApplicationLogicException('Deletion of password credential is not allowed.');
140
    }
141
}
142