SQL2::__construct()   F
last analyzed

Complexity

Conditions 39
Paths 992

Size

Total Lines 277
Code Lines 181

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 39
eloc 181
c 4
b 0
f 0
nc 992
nop 2
dl 0
loc 277
rs 0.0088

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 count;
17
use function explode;
18
use function implode;
19
use function in_array;
20
use function is_array;
21
use function is_null;
22
use function is_string;
23
use function password_verify;
24
use function preg_match;
25
use function preg_replace;
26
use function sprintf;
27
use function strlen;
28
use function strtolower;
29
use function var_export;
30
31
/**
32
 * An authentication source source that uses (potentially multiple) SQL databases.
33
 *
34
 * This class is an example authentication source which authenticates an user
35
 * against a SQL database.
36
 *
37
 * @package SimpleSAMLphp
38
 */
39
40
class SQL2 extends UserPassBase
41
{
42
    /**
43
     * List of one or more databases that are used by auth and attribute queries.
44
     * Each database must have a unique name, and the name is used to refer to
45
     * the database in auth and attribute queries.
46
     *
47
     * @var array
48
     */
49
    private array $databases = [];
50
51
    /**
52
     * List of one or more authentication queries. The first query that returns a result
53
     * is considered to have authenticated the user (and termed "winning").
54
     *
55
     * @var array
56
     */
57
    private array $authQueries = [];
58
59
    /**
60
     * List of zero or more attribute queries, which can optionally be limited to run only
61
     * for certain "winning" authentication queries.
62
     *
63
     * @var array
64
     */
65
    private array $attributesQueries = [];
66
67
68
    /**
69
     * Constructor for this authentication source.
70
     *
71
     * @param array $info  Information about this authentication source.
72
     * @param array $config  Configuration.
73
     */
74
    public function __construct(array $info, array $config)
75
    {
76
        // Call the parent constructor first, as required by the interface
77
        parent::__construct($info, $config);
78
79
        // Check databases configuration that all required parameters are present
80
        if (!array_key_exists('databases', $config)) {
81
            throw new Exception(sprintf(
82
                'Missing required attribute \'databases\' for authentication source \'%s\'',
83
                $this->authId,
84
            ));
85
        } else {
86
            if (!is_array($config['databases'])) {
87
                throw new Exception(sprintf(
88
                    'Required parameter \'databases\' for authentication source \'%s\''
89
                    . ' was provided and is expected to be an array. Instead it was: %s',
90
                    $this->authId,
91
                    var_export($config['databases'], true),
92
                ));
93
            }
94
95
            if (empty($config['databases'])) {
96
                throw new Exception(sprintf(
97
                    'Required parameter \'databases\' for authentication source \'%s\''
98
                    . ' was provided but is an empty array.',
99
                    $this->authId,
100
                ));
101
            }
102
103
            foreach ($config['databases'] as $dbname => $dbConfig) {
104
                if (!is_array($dbConfig)) {
105
                    throw new Exception(sprintf(
106
                        'Each entry in the %s \'databases\' parameter for authentication source \'%s\''
107
                        . ' is expected to be an array. Instead it was: %s',
108
                        $dbname,
109
                        $this->authId,
110
                        var_export($dbConfig, true),
111
                    ));
112
                }
113
                foreach (['dsn', 'username', 'password'] as $param) {
114
                    if (!array_key_exists($param, $dbConfig)) {
115
                        throw new Exception(sprintf(
116
                            'Database %s is missing required attribute \'%s\' for authentication source \'%s\'',
117
                            $dbname,
118
                            $param,
119
                            $this->authId,
120
                        ));
121
                    }
122
                    if (!is_string($dbConfig[$param])) {
123
                        throw new Exception(sprintf(
124
                            'Expected parameter \'%s\' for authentication source %s to be a string. '
125
                            . 'Instead it was: %s',
126
                            $dbname,
127
                            $this->authId,
128
                            var_export($config[$param], true),
129
                        ));
130
                    }
131
                }
132
133
                if (array_key_exists('options', $dbConfig) && !is_array($dbConfig['options'])) {
134
                    throw new Exception(sprintf(
135
                        'Optional parameter \'options\' for authentication source \'%s\''
136
                        . ' was provided and is expected to be an array. Instead it was: %s',
137
                        $this->authId,
138
                        var_export($dbConfig['options'], true),
139
                    ));
140
                }
141
142
                $this->databases[$dbname] = [
143
                    '_pdo' => null, // Will hold the PDO connection when connected
144
                    'dsn' => $dbConfig['dsn'],
145
                    'username' => $dbConfig['username'],
146
                    'password' => $dbConfig['password'],
147
                    'options' => $dbConfig['options'] ?? [],
148
                ];
149
            }
150
        }
151
152
        // Check auth_queries configuration that all required parameters are present
153
        if (!array_key_exists('auth_queries', $config)) {
154
            throw new Exception(sprintf(
155
                'Missing required attribute \'auth_queries\' for authentication source \'%s\'',
156
                $this->authId,
157
            ));
158
        } else {
159
            if (!is_array($config['auth_queries'])) {
160
                throw new Exception(sprintf(
161
                    'Required parameter \'auth_queries\' for authentication source \'%s\''
162
                    . ' was provided and is expected to be an array. Instead it was: %s',
163
                    $this->authId,
164
                    var_export($config['auth_queries'], true),
165
                ));
166
            }
167
168
            if (empty($config['auth_queries'])) {
169
                throw new Exception(sprintf(
170
                    'Required parameter \'auth_queries\' for authentication source \'%s\''
171
                    . ' was provided but is an empty array.',
172
                    $this->authId,
173
                ));
174
            }
175
176
            foreach ($config['auth_queries'] as $authQueryName => $authQueryConfig) {
177
                if (!is_array($authQueryConfig)) {
178
                    throw new Exception(sprintf(
179
                        'Each entry in the %s \'auth_queries\' parameter for authentication source '
180
                        . '\'%s\' is expected to be an array. Instead it was: %s' .
181
                        $authQueryName,
182
                        $this->authId,
183
                        var_export($authQueryConfig, true),
184
                    ));
185
                }
186
187
                foreach (['database', 'query'] as $param) {
188
                    if (!array_key_exists($param, $authQueryConfig)) {
189
                        throw new Exception(sprintf(
190
                            'Auth query %s is missing required attribute \'%s\' for authentication source \'%s\'',
191
                            $param,
192
                            $authQueryName,
193
                            $this->authId,
194
                        ));
195
                    }
196
                    if (!is_string($authQueryConfig[$param])) {
197
                        throw new Exception(sprintf(
198
                            'Expected parameter \'%s\' for authentication source \'%s\' to be a string.'
199
                            . ' Instead it was: %s',
200
                            $param,
201
                            $this->authId,
202
                            var_export($authQueryConfig[$param], true),
203
                        ));
204
                    }
205
                }
206
207
                if (!array_key_exists($authQueryConfig['database'], $this->databases)) {
208
                    throw new Exception(sprintf(
209
                        'Auth query %s references unknown database \'%s\' for authentication source \'%s\'',
210
                        $authQueryName,
211
                        $authQueryConfig['database'],
212
                        $this->authId,
213
                    ));
214
                }
215
216
                $this->authQueries[$authQueryName] = [
217
                    // Will be set to true for the query that successfully authenticated the user
218
                    '_winning_auth_query' => false,
219
220
                    // Will hold the value of the attribute named by 'extract_userid_from'
221
                    // if specified and authentication succeeds
222
                    '_extracted_userid' => null,
223
224
                    'database' => $authQueryConfig['database'],
225
                    'query' => $authQueryConfig['query'],
226
                ];
227
228
                if (array_key_exists('username_regex', $authQueryConfig)) {
229
                    if (!is_string($authQueryConfig['username_regex'])) {
230
                        throw new Exception(sprintf(
231
                            'Optional parameter \'username_regex\' for authentication source \'%s\''
232
                            . ' was provided and is expected to be a string. Instead it was: %s',
233
                            $this->authId,
234
                            var_export($authQueryConfig['username_regex'], true),
235
                        ));
236
                    }
237
                    $this->authQueries[$authQueryName]['username_regex'] = $authQueryConfig['username_regex'];
238
                }
239
240
                if (array_key_exists('extract_userid_from', $authQueryConfig)) {
241
                    if (!is_string($authQueryConfig['extract_userid_from'])) {
242
                        throw new Exception(sprintf(
243
                            'Optional parameter \'extract_userid_from\' for authentication source \'%s\''
244
                            . ' was provided and is expected to be a string. Instead it was: %s',
245
                            $this->authId,
246
                            var_export($authQueryConfig['extract_userid_from'], true),
247
                        ));
248
                    }
249
                    $this->authQueries[$authQueryName]['extract_userid_from'] = $authQueryConfig['extract_userid_from'];
250
                }
251
252
                if (array_key_exists('password_verify_hash_column', $authQueryConfig)) {
253
                    if (!is_string($authQueryConfig['password_verify_hash_column'])) {
254
                        throw new Exception(sprintf(
255
                            'Optional parameter \'password_verify_hash_column\' for authentication source \'%s\''
256
                            . ' was provided and is expected to be a string. Instead it was: %s',
257
                            $this->authId,
258
                            var_export($authQueryConfig['password_verify_hash_column'], true),
259
                        ));
260
                    }
261
                    $this->authQueries[$authQueryName]['password_verify_hash_column'] =
262
                        $authQueryConfig['password_verify_hash_column'];
263
                }
264
            }
265
        }
266
267
        // attr_queries is optional, but if specified, we need to check the parameters
268
        if (array_key_exists('attr_queries', $config)) {
269
            if (!is_array($config['attr_queries'])) {
270
                throw new Exception(sprintf(
271
                    'Optional parameter \'attr_queries\' for authentication source \'%s\''
272
                    . ' was provided and is expected to be an array. Instead it was: %s',
273
                    $this->authId,
274
                    var_export($config['attr_queries'], true),
275
                ));
276
            }
277
278
            foreach ($config['attr_queries'] as $attrQueryConfig) {
279
                if (!is_array($attrQueryConfig)) {
280
                    throw new Exception(sprintf(
281
                        '\'attr_queries\' parameter for authentication source \'%s\''
282
                        . ' is expected to be an array. Instead it was: %s' .
283
                        $this->authId,
284
                        var_export($attrQueryConfig, true),
285
                    ));
286
                }
287
288
                foreach (['database', 'query'] as $param) {
289
                    if (!array_key_exists($param, $attrQueryConfig)) {
290
                        throw new Exception(sprintf(
291
                            'Attribute query is missing required attribute \'%s\' for authentication source %s',
292
                            $param,
293
                            $this->authId,
294
                        ));
295
                    }
296
297
                    if (!is_string($attrQueryConfig[$param])) {
298
                        throw new Exception(sprintf(
299
                            'Expected parameter \'%s\' for authentication source \'%s\''
300
                            . ' to be a string. Instead it was: %s',
301
                            $param,
302
                            $this->authId,
303
                            var_export($attrQueryConfig[$param], true),
304
                        ));
305
                    }
306
                }
307
308
                $currentAttributeQuery = [
309
                    'database' => $attrQueryConfig['database'],
310
                    'query' => $attrQueryConfig['query'],
311
                ];
312
313
                if (!array_key_exists($attrQueryConfig['database'], $this->databases)) {
314
                    throw new Exception(sprintf(
315
                        'Attribute query references unknown database \'%s\' for authentication source \'%s\'',
316
                        $attrQueryConfig['database'],
317
                        $this->authId,
318
                    ));
319
                }
320
321
                if (array_key_exists('only_for_auth', $attrQueryConfig)) {
322
                    if (!is_array($attrQueryConfig['only_for_auth'])) {
323
                        throw new Exception(sprintf(
324
                            'Optional parameter \'only_for_auth\' for authentication source \'%s\''
325
                            . ' was provided and is expected to be an array. Instead it was: %s',
326
                            $this->authId,
327
                            var_export($attrQueryConfig['only_for_auth'], true),
328
                        ));
329
                    }
330
                    foreach ($attrQueryConfig['only_for_auth'] as $authQueryName) {
331
                        if (!is_string($authQueryName)) {
332
                            throw new Exception(sprintf(
333
                                'Each entry in the \'only_for_auth\' array for authentication source \'%s\''
334
                                . ' is expected to be a string. Instead it was: %s',
335
                                $this->authId,
336
                                var_export($authQueryName, true),
337
                            ));
338
                        }
339
                        if (!array_key_exists($authQueryName, $this->authQueries)) {
340
                            throw new Exception(sprintf(
341
                                'Attribute query references unknown auth query \'%s\' for authentication source %s',
342
                                $authQueryName,
343
                                $this->authId,
344
                            ));
345
                        }
346
                    }
347
                    $currentAttributeQuery['only_for_auth'] = $attrQueryConfig['only_for_auth'];
348
                }
349
350
                $this->attributesQueries[] = $currentAttributeQuery;
351
            }
352
        }
353
    }
354
355
356
    /**
357
     * Create a database connection.
358
     *
359
     * @return \PDO  The database connection.
360
     */
361
    protected function connect(string $dbname): PDO
362
    {
363
        if (!array_key_exists($dbname, $this->databases)) {
364
            throw new Exception(sprintf(
365
                "sqlauth:%s: Attempt to connect to unknown database '%s'",
366
                $this->authId,
367
                $dbname,
368
            ));
369
        }
370
        if ($this->databases[$dbname]['_pdo'] !== null) {
371
            // Already connected
372
            return $this->databases[$dbname]['_pdo'];
373
        }
374
375
        try {
376
            $db = new PDO(
377
                $this->databases[$dbname]['dsn'],
378
                $this->databases[$dbname]['username'],
379
                $this->databases[$dbname]['password'],
380
                $this->databases[$dbname]['options'],
381
            );
382
        } catch (PDOException $e) {
383
            // Obfuscate the password if it's part of the dsn
384
            throw new Exception(sprintf(
385
                "sqlauth:%s: - Failed to connect to '%s': %s",
386
                $this->authId,
387
                preg_replace('/(user|password)=(.*?([;]|$))/', '${1}=***', $this->databases[$dbname]['dsn']),
388
                $e->getMessage(),
389
            ));
390
        }
391
392
        $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
393
394
        $driver = explode(':', $this->databases[$dbname]['dsn'], 2);
395
        $driver = strtolower($driver[0]);
396
397
        // Driver specific initialization
398
        switch ($driver) {
399
            case 'mysql':
400
                // Use UTF-8
401
                $db->exec("SET NAMES 'utf8mb4'");
402
                break;
403
            case 'pgsql':
404
                // Use UTF-8
405
                $db->exec("SET NAMES 'UTF8'");
406
                break;
407
        }
408
409
        Logger::debug(sprintf('sqlauth:%s: Connected to database %s', $this->authId, $dbname));
410
        $this->databases[$dbname]['_pdo'] = $db;
411
        return $db;
412
    }
413
414
415
    /**
416
     * Extract SQL columns into SAML attribute array
417
     *
418
     * @param array $attributes output place to store extracted attributes
419
     * @param array  $data  Associative array from database in the format of PDO fetchAll
420
     * @param array  $forbiddenAttributes An array of attributes to never return
421
     * @return array &$attributes
422
     */
423
    protected function extractAttributes(array &$attributes, array $data, array $forbiddenAttributes = []): array
424
    {
425
        foreach ($data as $row) {
426
            foreach ($row as $name => $value) {
427
                if ($value === null) {
428
                    continue;
429
                }
430
                if (in_array($name, $forbiddenAttributes)) {
431
                    continue;
432
                }
433
434
                $value = (string) $value;
435
436
                if (!array_key_exists($name, $attributes)) {
437
                    $attributes[$name] = [];
438
                }
439
440
                if (in_array($value, $attributes[$name], true)) {
441
                    // Value already exists in attribute
442
                    continue;
443
                }
444
445
                $attributes[$name][] = $value;
446
            }
447
        }
448
        return $attributes;
449
    }
450
451
452
    /**
453
     * Execute the query with given parameters and return the tuples that result.
454
     *
455
     * @param string $query  SQL to execute
456
     * @param array $params parameters to the SQL query
457
     * @return array tuples that result
458
     */
459
    protected function executeQuery(PDO $db, string $query, array $params): array
460
    {
461
        try {
462
            $sth = $db->prepare($query);
463
        } catch (PDOException $e) {
464
            throw new Exception(sprintf(
465
                'sqlauth:%s: - Failed to prepare query: %s',
466
                $this->authId,
467
                $e->getMessage(),
468
            ));
469
        }
470
471
        try {
472
            $sth->execute($params);
473
        } catch (PDOException $e) {
474
            throw new Exception(sprintf(
475
                'sqlauth:%s: - Failed to execute query: %s',
476
                $this->authId,
477
                $e->getMessage(),
478
            ));
479
        }
480
481
        try {
482
            $data = $sth->fetchAll(PDO::FETCH_ASSOC);
483
            return $data;
484
        } catch (PDOException $e) {
485
            throw new Exception(sprintf(
486
                'sqlauth:%s: - Failed to fetch result set: %s',
487
                $this->authId,
488
                $e->getMessage(),
489
            ));
490
        } finally {
491
            unset($sth);
492
        }
493
    }
494
495
496
    /**
497
     * Authenticate using the optional password_verify() support against a hash retrieved from the database.
498
     *
499
     * @param string $queryname   Name of the auth query being processed
500
     * @param array $queryConfig  Configuration from authsources.php for this auth query
501
     * @param array $data         Result data from the database query
502
     * @param string $password    Password to verify with password_verify()
503
     * @return bool  True if password_verify() password verification succeeded, false otherwise
504
     */
505
    protected function authenticatePasswordVerifyHash(
506
        string $queryname,
507
        array $queryConfig,
508
        array $data,
509
        string $password,
510
    ): bool {
511
        // If password_verify_hash_column is not set, we are not using password_verify()
512
        if (!array_key_exists('password_verify_hash_column', $queryConfig)) {
513
            Logger::error(sprintf(
514
                'sqlauth:%s: authenticatePasswordVerifyHash() called but configuration for ' .
515
                '"password_verify_hash_column" not found in query config for query %s.',
516
                $this->authId,
517
                $queryname,
518
            ));
519
            throw new Error\Error('WRONGUSERPASS');
520
        } elseif (count($data) < 1) {
521
            // No rows returned, password_verify() cannot succeed
522
            return false;
523
        }
524
525
        /* This is where we need to run password_verify() if we are using password_verify() to
526
            * authenticate hashed passwords that are only stored in the database. */
527
        $hashColumn = $queryConfig['password_verify_hash_column'];
528
        if (!array_key_exists($hashColumn, $data[0])) {
529
            Logger::error(sprintf(
530
                'sqlauth:%s: Auth query %s did not return expected hash column \'' . $hashColumn . '\'',
531
                $this->authId,
532
                $queryname,
533
            ));
534
            throw new Error\Error('WRONGUSERPASS');
535
        }
536
537
        $passwordHash = null;
538
        foreach ($data as $row) {
539
            if ((!array_key_exists($hashColumn, $row)) || is_null($row[$hashColumn])) {
540
                Logger::error(sprintf(
541
                    'sqlauth:%s: column `%s` must be in every result tuple.',
542
                    $this->authId,
543
                    $hashColumn,
544
                ));
545
                throw new Error\Error('WRONGUSERPASS');
546
            }
547
548
            if (strlen($row[$hashColumn]) === 0) {
549
                Logger::error(sprintf(
550
                    'sqlauth:%s: column `%s` must contain a valid password hash.',
551
                    $this->authId,
552
                    $hashColumn,
553
                ));
554
                throw new Error\Error('WRONGUSERPASS');
555
            } elseif ($passwordHash === null) {
556
                $passwordHash = $row[$hashColumn];
557
            } elseif ($passwordHash != $row[$hashColumn]) {
558
                Logger::error(sprintf(
559
                    'sqlauth:%s: column %s must be THE SAME in every result tuple.',
560
                    $this->authId,
561
                    $hashColumn,
562
                ));
563
                throw new Error\Error('WRONGUSERPASS');
564
            }
565
        }
566
567
        if (($passwordHash == null) || (!password_verify($password, $passwordHash))) {
568
            Logger::error(sprintf(
569
                'sqlauth:%s: Auth query %s password verification failed',
570
                $this->authId,
571
                $queryname,
572
            ));
573
            /* Authentication with verify_password() failed, however that only means that
574
                * this auth query did not succeed. We should try the next auth query if any. */
575
            return false;
576
        }
577
578
        Logger::debug(sprintf(
579
            'sqlauth:%s: Auth query %s password verification using password_verify() succeeded',
580
            $this->authId,
581
            $queryname,
582
        ));
583
        return true;
584
    }
585
586
587
    /**
588
     * Attempt to log in using the given username and password.
589
     *
590
     * On a successful login, this function should return the users attributes. On failure,
591
     * it should throw an exception. If the error was caused by the user entering the wrong
592
     * username or password, a \SimpleSAML\Error\Error('WRONGUSERPASS') should be thrown.
593
     *
594
     * Note that both the username and the password are UTF-8 encoded.
595
     *
596
     * @param string $username  The username the user wrote.
597
     * @param string $password  The password the user wrote.
598
     * @return array  Associative array with the users attributes.
599
     */
600
    protected function login(
601
        string $username,
602
        #[\SensitiveParameter]
603
        string $password,
604
    ): array {
605
606
        $attributes = [];
607
        $winningAuthQuery = null;
608
609
        // Run authentication queries in order until one succeeds.
610
        foreach ($this->authQueries as $queryname => &$queryConfig) {
611
            // Check if the username matches the username_regex for this query
612
            if (
613
                array_key_exists('username_regex', $queryConfig) &&
614
                !preg_match($queryConfig['username_regex'], $username)
615
            ) {
616
                Logger::debug(sprintf(
617
                    'sqlauth:%s: Skipping auth query %s because username %s does not match username_regex %s',
618
                    $this->authId,
619
                    $queryname,
620
                    $username,
621
                    $queryConfig['username_regex'],
622
                ));
623
                continue;
624
            }
625
626
            Logger::debug('sqlauth:' . $this->authId . ': Trying auth query ' . $queryname);
627
628
            $db = $this->connect($queryConfig['database']);
629
630
            try {
631
                $sqlParams = ['username' => $username];
632
                if (!array_key_exists('password_verify_hash_column', $queryConfig)) {
633
                    // If we are not using password_verify(), pass the password to the query
634
                    $sqlParams['password'] = $password;
635
                }
636
                $data = $this->executeQuery($db, $queryConfig['query'], $sqlParams);
637
            } catch (PDOException $e) {
638
                Logger::error(sprintf(
639
                    'sqlauth:%s: Auth query %s failed with error: %s',
640
                    $this->authId,
641
                    $queryname,
642
                    $e->getMessage(),
643
                ));
644
                continue;
645
            }
646
647
            // If we got any rows, the authentication succeeded. If not, try the next query.
648
            if (
649
                (count($data) > 0) &&
650
                ((array_key_exists('password_verify_hash_column', $queryConfig) === false) ||
651
                    $this->authenticatePasswordVerifyHash($queryname, $queryConfig, $data, $password))
652
            ) {
653
                Logger::debug(sprintf(
654
                    'sqlauth:%s: Auth query %s succeeded with %d rows',
655
                    $this->authId,
656
                    $queryname,
657
                    count($data),
658
                ));
659
                $queryConfig['_winning_auth_query'] = true;
660
661
                if (array_key_exists('extract_userid_from', $queryConfig)) {
662
                    $queryConfig['_extracted_userid'] = $data[0][$queryConfig['extract_userid_from']];
663
                }
664
                $winningAuthQuery = $queryname;
665
666
                $forbiddenAttributes = [];
667
                if (array_key_exists('password_verify_hash_column', $queryConfig)) {
668
                    $forbiddenAttributes[] = $queryConfig['password_verify_hash_column'];
669
                }
670
                $this->extractAttributes($attributes, $data, $forbiddenAttributes);
671
672
                // The first auth query that succeeds is the winning one, so we can stop here.
673
                break;
674
            } else {
675
                Logger::debug(sprintf(
676
                    'sqlauth:%s: Auth query %s returned no rows, trying next auth query if any',
677
                    $this->authId,
678
                    $queryname,
679
                ));
680
            }
681
        }
682
683
        if (empty($attributes)) {
684
            // No auth query succeeded
685
            Logger::error(sprintf(
686
                'sqlauth:%s: No auth query succeeded. Probably wrong username/password.',
687
                $this->authId,
688
            ));
689
            throw new Error\Error('WRONGUSERPASS');
690
        }
691
692
        // Run attribute queries. Each attribute query can specify which auth queries it applies to.
693
        foreach ($this->attributesQueries as $attrQueryConfig) {
694
            // If the attribute query is limited to certain auth queries, check if the winning auth query
695
            // is one of those.
696
            Logger::debug(sprintf(
697
                'sqlauth:%s: Considering attribute query \'%s\' for winning auth query \'%s\''
698
                . ' with only_for_auth \'%s\'',
699
                $this->authId,
700
                $attrQueryConfig['query'],
701
                $winningAuthQuery,
702
                implode(',', $attrQueryConfig['only_for_auth'] ?? []),
703
            ));
704
705
            if (
706
                (!array_key_exists('only_for_auth', $attrQueryConfig)) ||
707
                in_array($winningAuthQuery, $attrQueryConfig['only_for_auth'], true)
708
            ) {
709
                Logger::debug(sprintf(
710
                    'sqlauth:%s: Running attribute query \'%s\' for winning auth query \'%s\'',
711
                    $this->authId,
712
                    $attrQueryConfig['query'],
713
                    $winningAuthQuery,
714
                ));
715
716
                $db = $this->connect($attrQueryConfig['database']);
717
718
                try {
719
                    $params = ($this->authQueries[$winningAuthQuery]['_extracted_userid'] !== null) ?
720
                        ['userid' => $this->authQueries[$winningAuthQuery]['_extracted_userid']] :
721
                        ['username' => $username];
722
                    $data = $this->executeQuery($db, $attrQueryConfig['query'], $params);
723
                } catch (PDOException $e) {
724
                    Logger::error(sprintf(
725
                        'sqlauth:%s: Attribute query \'%s\' failed with error: %s',
726
                        $this->authId,
727
                        $attrQueryConfig['query'],
728
                        $e->getMessage(),
729
                    ));
730
                    continue;
731
                }
732
733
                Logger::debug(sprintf(
734
                    'sqlauth:%s: Attribute query \'%s\' returned %d rows',
735
                    $this->authId,
736
                    $attrQueryConfig['query'],
737
                    count($data),
738
                ));
739
740
                $this->extractAttributes($attributes, $data, []);
741
            } else {
742
                Logger::debug(sprintf(
743
                    'sqlauth:%s: Skipping attribute query \'%s\' because it does not apply'
744
                    . ' to winning auth query \'%s\'',
745
                    $this->authId,
746
                    $attrQueryConfig['query'],
747
                    $winningAuthQuery,
748
                ));
749
            }
750
        }
751
752
        // At the end, disconnect from all databases
753
        unset($db);
754
755
        foreach ($this->databases as $dbname => $dbConfig) {
756
            if ($dbConfig['_pdo'] !== null) {
757
                $this->databases[$dbname]['_pdo'] = null;
758
                Logger::debug(sprintf(
759
                    'sqlauth:%s: Disconnected from database %s',
760
                    $this->authId,
761
                    $dbname,
762
                ));
763
            }
764
        }
765
766
        Logger::info(sprintf(
767
            'sqlauth:%s: Attributes: %s',
768
            $this->authId,
769
            implode(',', array_keys($attributes)),
770
        ));
771
772
        return $attributes;
773
    }
774
}
775