Passed
Push — master ( 4409eb...4c494f )
by Tim
02:28 queued 20s
created

SQL::__construct()   B

Complexity

Conditions 9
Paths 14

Size

Total Lines 35
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 22
nc 14
nop 2
dl 0
loc 35
rs 8.0555
c 0
b 0
f 0
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\Error;
11
use SimpleSAML\Logger;
12
use SimpleSAML\Module\core\Auth\UserPassBase;
13
14
use function array_key_exists;
15
use function array_keys;
16
use function explode;
17
use function implode;
18
use function in_array;
19
use function is_string;
20
use function preg_replace;
21
use function strtolower;
22
use function var_export;
23
24
/**
25
 * Simple SQL authentication source
26
 *
27
 * This class is an example authentication source which authenticates an user
28
 * against a SQL database.
29
 *
30
 * @package SimpleSAMLphp
31
 */
32
33
class SQL extends UserPassBase
34
{
35
    /**
36
     * The DSN we should connect to.
37
     * @var string
38
     */
39
    private string $dsn;
40
41
    /**
42
     * The username we should connect to the database with.
43
     * @var string
44
     */
45
    private string $username;
46
47
    /**
48
     * The password we should connect to the database with.
49
     * @var string
50
     */
51
    private string $password;
52
53
    /**
54
     * The options that we should connect to the database with.
55
     * @var array
56
     */
57
    private array $options = [];
58
59
    /**
60
     * The query or queries we should use to retrieve the attributes for the user.
61
     *
62
     * The username and password will be available as :username and :password.
63
     * @var array
64
     */
65
    private array $query;
66
67
    /**
68
     * Constructor for this authentication source.
69
     *
70
     * @param array $info  Information about this authentication source.
71
     * @param array $config  Configuration.
72
     */
73
    public function __construct(array $info, array $config)
74
    {
75
        // Call the parent constructor first, as required by the interface
76
        parent::__construct($info, $config);
77
78
        // Make sure that all required parameters are present.
79
        foreach (['dsn', 'username', 'password'] as $param) {
80
            if (!array_key_exists($param, $config)) {
81
                throw new Exception('Missing required attribute \'' . $param .
82
                    '\' for authentication source ' . $this->authId);
83
            }
84
85
            if (!is_string($config[$param])) {
86
                throw new Exception('Expected parameter \'' . $param .
87
                    '\' for authentication source ' . $this->authId .
88
                    ' to be a string. Instead it was: ' .
89
                    var_export($config[$param], true));
90
            }
91
        }
92
93
        // Query can be a single query or an array of queries.
94
        if (!array_key_exists('query', $config)) {
95
            throw new Exception('Missing required attribute \'query\' ' .
96
                'for authentication source ' . $this->authId);
97
        } elseif (is_array($config['query']) && (count($config['query']) < 1)) {
98
            throw new Exception('Required attribute \'query\' is an empty ' .
99
                'list of queries for authentication source ' . $this->authId);
100
        }
101
102
        $this->dsn = $config['dsn'];
103
        $this->username = $config['username'];
104
        $this->password = $config['password'];
105
        $this->query = is_string($config['query']) ? [$config['query']] : $config['query'];
106
        if (isset($config['options'])) {
107
            $this->options = $config['options'];
108
        }
109
    }
110
111
112
    /**
113
     * Create a database connection.
114
     *
115
     * @return \PDO  The database connection.
116
     */
117
    private function connect(): PDO
118
    {
119
        try {
120
            $db = new PDO($this->dsn, $this->username, $this->password, $this->options);
121
        } catch (PDOException $e) {
122
            // Obfuscate the password if it's part of the dsn
123
            $obfuscated_dsn =  preg_replace('/(user|password)=(.*?([;]|$))/', '${1}=***', $this->dsn);
124
125
            throw new Exception('sqlauth:' . $this->authId . ': - Failed to connect to \'' .
126
                $obfuscated_dsn . '\': ' . $e->getMessage());
127
        }
128
129
        $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
130
131
        $driver = explode(':', $this->dsn, 2);
132
        $driver = strtolower($driver[0]);
133
134
        // Driver specific initialization
135
        switch ($driver) {
136
            case 'mysql':
137
                // Use UTF-8
138
                $db->exec("SET NAMES 'utf8mb4'");
139
                break;
140
            case 'pgsql':
141
                // Use UTF-8
142
                $db->exec("SET NAMES 'UTF8'");
143
                break;
144
        }
145
146
        return $db;
147
    }
148
149
150
    /**
151
     * Attempt to log in using the given username and password.
152
     *
153
     * On a successful login, this function should return the users attributes. On failure,
154
     * it should throw an exception. If the error was caused by the user entering the wrong
155
     * username or password, a \SimpleSAML\Error\Error('WRONGUSERPASS') should be thrown.
156
     *
157
     * Note that both the username and the password are UTF-8 encoded.
158
     *
159
     * @param string $username  The username the user wrote.
160
     * @param string $password  The password the user wrote.
161
     * @return array  Associative array with the users attributes.
162
     */
163
    protected function login(string $username, string $password): array
164
    {
165
        $db = $this->connect();
166
        $params = ['username' => $username, 'password' => $password];
167
        $attributes = [];
168
169
        $numQueries = count($this->query);
170
        for ($x = 0; $x < $numQueries; $x++) {
171
            try {
172
                $sth = $db->prepare($this->query[$x]);
173
            } catch (PDOException $e) {
174
                throw new Exception('sqlauth:' . $this->authId .
175
                    ': - Failed to prepare query: ' . $e->getMessage());
176
            }
177
178
            try {
179
                $sth->execute($params);
180
            } catch (PDOException $e) {
181
                throw new Exception('sqlauth:' . $this->authId .
182
                    ': - Failed to execute query: ' . $e->getMessage());
183
            }
184
185
            try {
186
                $data = $sth->fetchAll(PDO::FETCH_ASSOC);
187
            } catch (PDOException $e) {
188
                throw new Exception('sqlauth:' . $this->authId .
189
                    ': - Failed to fetch result set: ' . $e->getMessage());
190
            }
191
192
            Logger::info('sqlauth:' . $this->authId . ': Got ' . count($data) .
193
                ' rows from database');
194
195
            if ($x === 0) {
196
                if (count($data) === 0) {
197
                    // No rows returned from first query - invalid username/password
198
                    Logger::error('sqlauth:' . $this->authId .
199
                        ': No rows in result set. Probably wrong username/password.');
200
                    throw new Error\Error('WRONGUSERPASS');
201
                }
202
                /* Only the first query should be passed the password, as that is the only
203
                 * one used for authentication. Subsequent queries are only used for
204
                 * getting attribute lists, so only need the username. */
205
                unset($params['password']);
206
            }
207
208
            /* Extract attributes. We allow the resultset to consist of multiple rows. Attributes
209
            * which are present in more than one row will become multivalued. null values and
210
            * duplicate values will be skipped. All values will be converted to strings.
211
            */
212
            foreach ($data as $row) {
213
                foreach ($row as $name => $value) {
214
                    if ($value === null) {
215
                        continue;
216
                    }
217
218
                    $value = (string) $value;
219
220
                    if (!array_key_exists($name, $attributes)) {
221
                        $attributes[$name] = [];
222
                    }
223
224
                    if (in_array($value, $attributes[$name], true)) {
225
                        // Value already exists in attribute
226
                        continue;
227
                    }
228
229
                    $attributes[$name][] = $value;
230
                }
231
            }
232
        }
233
234
        Logger::info('sqlauth:' . $this->authId . ': Attributes: ' . implode(',', array_keys($attributes)));
235
236
        return $attributes;
237
    }
238
}
239