Passed
Pull Request — master (#6)
by
unknown
07:22
created

PasswordVerify::__construct()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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