Issues (1)

src/Auth/Source/SQL2.php (1 issue)

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
            throw new Exception('Missing required attribute \'databases\' for authentication source ' . $this->authId);
65
        } else {
66
            if (!is_array($config['databases'])) {
67
                throw new Exception('Required parameter \'databases\' for authentication source ' .
68
                    $this->authId . ' was provided and is expected to be an array. Instead it was: ' .
69
                    var_export($config['databases'], true));
70
            }
71
72
            if (empty($config['databases'])) {
73
                throw new Exception('Required parameter \'databases\' for authentication source ' .
74
                    $this->authId . ' was provided but is an empty array.');
75
            }
76
77
            foreach ($config['databases'] as $dbname => $dbConfig) {
78
                if (!is_array($dbConfig)) {
79
                    throw new Exception('Each entry in the ' .
80
                        $dbname . ' \'databases\' parameter for authentication source ' .
81
                        $this->authId . ' is expected to be an array. Instead it was: ' .
82
                        var_export($dbConfig, true));
83
                }
84
                foreach (['dsn', 'username', 'password'] as $param) {
85
                    if (!array_key_exists($param, $dbConfig)) {
86
                        throw new Exception('Database ' .
87
                            $dbname . ' is missing required attribute \'' .
88
                            $param . '\' for authentication source ' .
89
                            $this->authId);
90
                    }
91
                    if (!is_string($dbConfig[$param])) {
92
                        throw new Exception('Expected parameter \'' . $param .
93
                            '\' for authentication source ' . $this->authId .
94
                            ' to be a string. Instead it was: ' .
95
                            var_export($config[$param], true));
96
                    }
97
                }
98
99
                if (array_key_exists('options', $dbConfig) && !is_array($dbConfig['options'])) {
100
                    throw new Exception('Optional parameter \'options\' for authentication source ' .
101
                        $this->authId . ' was provided and is expected to be an array. Instead it was: ' .
102
                        var_export($dbConfig['options'], true));
103
                }
104
105
                $this->databases[$dbname] = [
106
                    '_pdo' => null, // Will hold the PDO connection when connected
107
                    'dsn' => $dbConfig['dsn'],
108
                    'username' => $dbConfig['username'],
109
                    'password' => $dbConfig['password'],
110
                    'options' => $dbConfig['options'] ?? [],
111
                ];
112
            }
113
        }
114
115
        // Check auth_queries configuration that all required parameters are present
116
        if (!array_key_exists('auth_queries', $config)) {
117
            throw new Exception(
118
                'Missing required attribute \'auth_queries\' for authentication source ' .
119
                $this->authId,
120
            );
121
        } else {
122
            if (!is_array($config['auth_queries'])) {
123
                throw new Exception('Required parameter \'auth_queries\' for authentication source ' .
124
                    $this->authId . ' was provided and is expected to be an array. Instead it was: ' .
125
                    var_export($config['auth_queries'], true));
126
            }
127
128
            if (empty($config['auth_queries'])) {
129
                throw new Exception('Required parameter \'auth_queries\' for authentication source ' .
130
                    $this->authId . ' was provided but is an empty array.');
131
            }
132
133
            foreach ($config['auth_queries'] as $authQueryName => $authQueryConfig) {
134
                if (!is_array($authQueryConfig)) {
135
                    throw new Exception('Each entry in the ' .
136
                        $authQueryName . ' \'auth_queries\' parameter for authentication source ' .
137
                        $this->authId . ' is expected to be an array. Instead it was: ' .
138
                        var_export($authQueryConfig, true));
139
                }
140
141
                foreach (['database', 'query'] as $param) {
142
                    if (!array_key_exists($param, $authQueryConfig)) {
143
                        throw new Exception('Auth query ' .
144
                            $authQueryName . ' is missing required attribute \'' .
145
                            $param . '\' for authentication source ' .
146
                            $this->authId);
147
                    }
148
                    if (!is_string($authQueryConfig[$param])) {
149
                        throw new Exception('Expected parameter \'' . $param .
150
                            '\' for authentication source \'' . $this->authId . '\'' .
151
                            ' to be a string. Instead it was: ' .
152
                            var_export($authQueryConfig[$param], true));
153
                    }
154
                }
155
156
                if (!array_key_exists($authQueryConfig['database'], $this->databases)) {
157
                    throw new Exception('Auth query ' .
158
                        $authQueryName . ' references unknown database \'' .
159
                        $authQueryConfig['database'] . '\' for authentication source ' .
160
                        $this->authId);
161
                }
162
163
                $this->authQueries[$authQueryName] = [
164
                    // Will be set to true for the query that successfully authenticated the user
165
                    '_winning_auth_query' => false,
166
167
                    // Will hold the value of the attribute named by 'extract_userid_from'
168
                    // if specified and authentication succeeds
169
                    '_extracted_userid' => null,
170
171
                    'database' => $authQueryConfig['database'],
172
                    'query' => $authQueryConfig['query'],
173
                ];
174
175
                if (array_key_exists('username_regex', $authQueryConfig)) {
176
                    if (!is_string($authQueryConfig['username_regex'])) {
177
                        throw new Exception('Optional parameter \'username_regex\' for authentication source ' .
178
                            $this->authId . ' was provided and is expected to be a string. Instead it was: ' .
179
                            var_export($authQueryConfig['username_regex'], true));
180
                    }
181
                    $this->authQueries[$authQueryName]['username_regex'] = $authQueryConfig['username_regex'];
182
                }
183
184
                if (array_key_exists('extract_userid_from', $authQueryConfig)) {
185
                    if (!is_string($authQueryConfig['extract_userid_from'])) {
186
                        throw new Exception('Optional parameter \'extract_userid_from\' for authentication source ' .
187
                            $this->authId . ' was provided and is expected to be a string. Instead it was: ' .
188
                            var_export($authQueryConfig['extract_userid_from'], true));
189
                    }
190
                    $this->authQueries[$authQueryName]['extract_userid_from'] = $authQueryConfig['extract_userid_from'];
191
                }
192
193
                if (array_key_exists('password_verify_hash_column', $authQueryConfig)) {
194
                    if (!is_string($authQueryConfig['password_verify_hash_column'])) {
195
                        throw new Exception(
196
                            'Optional parameter \'password_verify_hash_column\' for authentication source ' .
197
                            $this->authId . ' was provided and is expected to be a string. Instead it was: ' .
198
                            var_export($authQueryConfig['password_verify_hash_column'], true),
199
                        );
200
                    }
201
                    $this->authQueries[$authQueryName]['password_verify_hash_column'] =
202
                        $authQueryConfig['password_verify_hash_column'];
203
                }
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
        $passwordHash = null;
438
        foreach ($data as $row) {
439
            if ((!array_key_exists($hashColumn, $row)) || is_null($row[$hashColumn])) {
440
                Logger::error(sprintf(
441
                    'sqlauth:%s: column `%s` must be in every result tuple.',
442
                    $this->authId,
443
                    $hashColumn,
444
                ));
445
                throw new Error\Error('WRONGUSERPASS');
446
            }
447
448
            if (strlen($row[$hashColumn]) === 0) {
449
                Logger::error(sprintf(
450
                    'sqlauth:%s: column `%s` must contain a valid password hash.',
451
                    $this->authId,
452
                    $hashColumn,
453
                ));
454
                throw new Error\Error('WRONGUSERPASS');
455
            } elseif ($passwordHash === null) {
456
                $passwordHash = $row[$hashColumn];
457
            } elseif ($passwordHash != $row[$hashColumn]) {
458
                Logger::error(sprintf(
459
                    'sqlauth:%s: column %s must be THE SAME in every result tuple.',
460
                    $this->authId,
461
                    $hashColumn,
462
                ));
463
                throw new Error\Error('WRONGUSERPASS');
464
            }
465
        }
466
467
        if (($passwordHash == null) || (!password_verify($password, $passwordHash))) {
468
            Logger::error('sqlauth:' . $this->authId . ': Auth query ' . $queryname .
469
                            ' password verification failed');
470
            /* Authentication with verify_password() failed, however that only means that
471
                * this auth query did not succeed. We should try the next auth query if any. */
472
            return false;
473
        }
474
475
        Logger::debug('sqlauth:' . $this->authId . ': Auth query ' . $queryname .
476
                        ' password verification using password_verify() succeeded');
477
        return true;
478
    }
479
480
481
    /**
482
     * Attempt to log in using the given username and password.
483
     *
484
     * On a successful login, this function should return the users attributes. On failure,
485
     * it should throw an exception. If the error was caused by the user entering the wrong
486
     * username or password, a \SimpleSAML\Error\Error('WRONGUSERPASS') should be thrown.
487
     *
488
     * Note that both the username and the password are UTF-8 encoded.
489
     *
490
     * @param string $username  The username the user wrote.
491
     * @param string $password  The password the user wrote.
492
     * @return array  Associative array with the users attributes.
493
     */
494
    protected function login(
495
        string $username,
496
        #[\SensitiveParameter]
497
        string $password,
498
    ): array {
499
500
        $attributes = [];
501
        $winningAuthQuery = null;
502
503
        // Run authentication queries in order until one succeeds.
504
        foreach ($this->authQueries as $queryname => &$queryConfig) {
505
            // Check if the username matches the username_regex for this query
506
            if (
507
                array_key_exists('username_regex', $queryConfig) &&
508
                !preg_match($queryConfig['username_regex'], $username)
509
            ) {
510
                Logger::debug('sqlauth:' . $this->authId . ': Skipping auth query ' . $queryname .
511
                             ' because username ' . $username . ' does not match username_regex ' .
512
                             $queryConfig['username_regex']);
513
                continue;
514
            }
515
516
            Logger::debug('sqlauth:' . $this->authId . ': Trying auth query ' . $queryname);
517
518
            $db = $this->connect($queryConfig['database']);
519
520
            try {
521
                $sqlParams = ['username' => $username];
522
                if (!array_key_exists('password_verify_hash_column', $queryConfig)) {
523
                    // If we are not using password_verify(), pass the password to the query
524
                    $sqlParams['password'] = $password;
525
                }
526
                $data = $this->executeQuery($db, $queryConfig['query'], $sqlParams);
527
            } catch (PDOException $e) {
528
                Logger::error('sqlauth:' . $this->authId . ': Auth query ' . $queryname .
529
                              ' failed with error: ' . $e->getMessage());
530
                continue;
531
            }
532
533
            // If we got any rows, the authentication succeeded. If not, try the next query.
534
            if (
535
                (count($data) > 0) &&
536
                ((array_key_exists('password_verify_hash_column', $queryConfig) === false) ||
537
                    $this->authenticatePasswordVerifyHash($queryname, $queryConfig, $data, $password))
538
            ) {
539
                Logger::debug('sqlauth:' . $this->authId . ': Auth query ' . $queryname .
540
                             ' succeeded with ' . count($data) . ' rows');
541
                $queryConfig['_winning_auth_query'] = true;
542
543
                if (array_key_exists('extract_userid_from', $queryConfig)) {
544
                    $queryConfig['_extracted_userid'] = $data[0][$queryConfig['extract_userid_from']];
545
                }
546
                $winningAuthQuery = $queryname;
547
548
                $forbiddenAttributes = [];
549
                if (array_key_exists('password_verify_hash_column', $queryConfig)) {
550
                    $forbiddenAttributes[] = $queryConfig['password_verify_hash_column'];
551
                }
552
                $this->extractAttributes($attributes, $data, $forbiddenAttributes);
553
554
                // The first auth query that succeeds is the winning one, so we can stop here.
555
                break;
556
            } else {
557
                Logger::debug('sqlauth:' . $this->authId . ': Auth query ' . $queryname .
558
                             ' returned no rows, trying next auth query if any');
559
            }
560
        }
561
562
        if (empty($attributes)) {
563
            // No auth query succeeded
564
            Logger::error('sqlauth:' . $this->authId . ': No auth query succeeded. Probably wrong username/password.');
565
            throw new Error\Error('WRONGUSERPASS');
566
        }
567
568
        // Run attribute queries. Each attribute query can specify which auth queries it applies to.
569
        foreach ($this->attributesQueries as $attrQueryConfig) {
570
            // If the attribute query is limited to certain auth queries, check if the winning auth query
571
            // is one of those.
572
            Logger::debug(
573
                'sqlauth:' . $this->authId . ': ' .
574
                'Considering attribute query ' . $attrQueryConfig['query'] .
575
                ' for winning auth query ' . $winningAuthQuery .
576
                ' with only_for_auth ' . implode(',', $attrQueryConfig['only_for_auth'] ?? []),
577
            );
578
579
            if (
580
                (!array_key_exists('only_for_auth', $attrQueryConfig)) ||
581
                in_array($winningAuthQuery, $attrQueryConfig['only_for_auth'], true)
582
            ) {
583
                Logger::debug('sqlauth:' . $this->authId . ': Running attribute query ' . $attrQueryConfig['query'] .
584
                             ' for winning auth query ' . $winningAuthQuery);
585
586
                $db = $this->connect($attrQueryConfig['database']);
587
588
                try {
589
                    $params = ($this->authQueries[$winningAuthQuery]['_extracted_userid'] !== null) ?
590
                        ['userid' => $this->authQueries[$winningAuthQuery]['_extracted_userid']] :
591
                        ['username' => $username];
592
                    $data = $this->executeQuery($db, $attrQueryConfig['query'], $params);
593
                } catch (PDOException $e) {
594
                    Logger::error('sqlauth:' . $this->authId . ': Attribute query ' . $attrQueryConfig['query'] .
595
                                  ' failed with error: ' . $e->getMessage());
596
                    continue;
597
                }
598
599
                Logger::debug('sqlauth:' . $this->authId . ': Attribute query ' . $attrQueryConfig['query'] .
600
                             ' returned ' . count($data) . ' rows');
601
602
                $this->extractAttributes($attributes, $data, []);
603
            } else {
604
                Logger::debug('sqlauth:' . $this->authId . ': Skipping attribute query ' . $attrQueryConfig['query'] .
605
                             ' because it does not apply to winning auth query ' . $winningAuthQuery);
606
            }
607
        }
608
609
        // At the end, disconnect from all databases
610
        $db = null;
0 ignored issues
show
The assignment to $db is dead and can be removed.
Loading history...
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