Passed
Pull Request — master (#20)
by
unknown
02:37
created

SQL2::connect()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 47
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

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