PasswordVerify::__construct()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 3
b 0
f 0
nc 2
nop 2
dl 0
loc 7
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\sqlauth\Auth\Source;
6
7
use SimpleSAML\Error;
8
use SimpleSAML\Logger;
9
use SimpleSAML\Module\sqlauth\Auth\Source\SQL;
10
11
use function array_key_exists;
12
use function array_keys;
13
use function count;
14
use function implode;
15
use function is_null;
16
use function password_verify;
17
use function sprintf;
18
19
/**
20
 * Simple SQL authentication source
21
 *
22
 * This class is very much like the SQL class. The major difference is that
23
 * instead of using SHA2 and other functions in the database we use the PHP
24
 * password_verify() function to allow for example PASSWORD_ARGON2ID to be used
25
 * for verification.
26
 *
27
 * While this class has a query parameter as the SQL class does the meaning
28
 * is different. The query for this class should return at least a column
29
 * called passwordhash containing the hashed password which was generated
30
 * for example using
31
 *    password_hash('hello', PASSWORD_ARGON2ID );
32
 *
33
 * Auth only passes if the PHP code below returns true.
34
 *   password_verify($password, row['passwordhash'] );
35
 *
36
 * Unlike the SQL class the username is the only parameter passed to the SQL query,
37
 * the query can not perform password checks, they are performed by the PHP code
38
 * in this class using password_verify().
39
 *
40
 * If there are other columns in the returned data they are assumed to be attributes
41
 * you would like to be returned through SAML.
42
 *
43
 * @package SimpleSAMLphp
44
 */
45
46
class PasswordVerify extends SQL
47
{
48
    /**
49
     * The column in the result set containing the passwordhash.
50
     */
51
    protected string $passwordhashcolumn = 'passwordhash';
52
53
    /**
54
     * Constructor for this authentication source.
55
     *
56
     * @param array $info  Information about this authentication source.
57
     * @param array $config  Configuration.
58
     */
59
    public function __construct(array $info, array $config)
60
    {
61
        // Call the parent constructor first, as required by the interface
62
        parent::__construct($info, $config);
63
64
        if (array_key_exists('passwordhashcolumn', $config)) {
65
            $this->passwordhashcolumn = $config['passwordhashcolumn'];
66
        }
67
    }
68
69
70
71
    /**
72
     * Attempt to log in using the given username and password.
73
     *
74
     * On a successful login, this function should return the users attributes. On failure,
75
     * it should throw an exception. If the error was caused by the user entering the wrong
76
     * username or password, a \SimpleSAML\Error\Error('WRONGUSERPASS') should be thrown.
77
     *
78
     * Note that both the username and the password are UTF-8 encoded.
79
     *
80
     * @param string $username  The username the user wrote.
81
     * @param string $password  The password the user wrote.
82
     * @return array  Associative array with the users attributes.
83
     */
84
    protected function login(string $username, string $password): array
85
    {
86
        $this->verifyUserNameWithRegex($username);
87
88
        $db = $this->connect();
89
        $params = ['username' => $username];
90
        $attributes = [];
91
92
        $numQueries = count($this->query);
93
        for ($x = 0; $x < $numQueries; $x++) {
94
            $data = $this->executeQuery($db, $this->query[$x], $params);
95
96
            Logger::info('sqlauth:' . $this->authId . ': Got ' . count($data) .
97
                         ' rows from database');
98
99
            /**
100
             * Sanity check, passwordhash must be in each resulting tuple and must have
101
             * the same value in every tuple.
102
             *
103
             * Note that $pwhash will contain the passwordhash value after this loop.
104
             */
105
            $pwhash = null;
106
            if ($x === 0) {
107
                if (count($data) === 0) {
108
                    // No rows returned - invalid username/password
109
                    Logger::error(sprintf(
110
                        'sqlauth:%s: No rows in result set. Probably wrong username/password.',
111
                        $this->authId,
112
                    ));
113
                    throw new Error\Error('WRONGUSERPASS');
114
                }
115
116
                foreach ($data as $row) {
117
                    if (
118
                        !array_key_exists($this->passwordhashcolumn, $row)
119
                        || is_null($row[$this->passwordhashcolumn])
120
                    ) {
121
                        Logger::error(sprintf(
122
                            'sqlauth:%s: column `%s` must be in every result tuple.',
123
                            $this->authId,
124
                            $this->passwordhashcolumn,
125
                        ));
126
                        throw new Error\Error('WRONGUSERPASS');
127
                    }
128
                    if ($pwhash) {
129
                        if ($pwhash != $row[$this->passwordhashcolumn]) {
130
                            Logger::error(sprintf(
131
                                'sqlauth:%s: column %s must be THE SAME in every result tuple.',
132
                                $this->authId,
133
                                $this->passwordhashcolumn,
134
                            ));
135
                            throw new Error\Error('WRONGUSERPASS');
136
                        }
137
                    }
138
                    $pwhash = $row[$this->passwordhashcolumn];
139
                }
140
141
                /**
142
                 * This should never happen as the count(data) test above would have already thrown.
143
                 * But checking twice doesn't hurt.
144
                 */
145
                if ($pwhash === null) {
146
                    if ($pwhash != $row[$this->passwordhashcolumn]) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $row does not seem to be defined for all execution paths leading up to this point.
Loading history...
147
                        Logger::error(sprintf(
148
                            'sqlauth:%s: column `%s` does not contain a password hash.',
149
                            $this->authId,
150
                            $this->passwordhashcolumn,
151
                        ));
152
                        throw new Error\Error('WRONGUSERPASS');
153
                    }
154
                }
155
156
                /**
157
                 * VERIFICATION!
158
                 * Now to check if the password the user supplied is actually valid
159
                 */
160
                if (!password_verify($password, $pwhash)) {
161
                    Logger::error(sprintf(
162
                        'sqlauth:%s: password is incorrect.',
163
                        $this->authId,
164
                    ));
165
                    throw new Error\Error('WRONGUSERPASS');
166
                }
167
            }
168
169
            $this->extractAttributes($attributes, $data, [$this->passwordhashcolumn]);
170
        }
171
172
        Logger::info(sprintf(
173
            'sqlauth:%s: Attributes: %s',
174
            $this->authId,
175
            implode(',', array_keys($attributes)),
176
        ));
177
178
        return $attributes;
179
    }
180
}
181