Passed
Pull Request — master (#6)
by Tim
02:45
created

PasswordVerify::__construct()   A

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