Database   A
last analyzed

Complexity

Total Complexity 25

Size/Duplication

Total Lines 311
Duplicated Lines 0 %

Importance

Changes 9
Bugs 4 Features 0
Metric Value
wmc 25
eloc 105
c 9
b 4
f 0
dl 0
loc 311
rs 10

10 Methods

Rating   Name   Duplication   Size   Complexity  
A getUsernameByHashedId() 0 13 2
A deleteTokenData() 0 10 1
A is2FAEnabled() 0 31 6
A __sleep() 0 4 1
B __construct() 0 42 7
A doesCredentialExist() 0 14 2
A getTokenData() 0 18 3
A storeTokenData() 0 35 1
A __wakeup() 0 3 1
A updateSignCount() 0 10 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\webauthn\WebAuthn\Store;
6
7
use PDO;
8
use SimpleSAML\Configuration;
9
use SimpleSAML\Database as SSP_Database;
10
use SimpleSAML\Logger;
11
use SimpleSAML\Module\webauthn\Store;
12
13
/**
14
 * Store FIDO2 information in database.
15
 *
16
 * This class implements a store which stores the FIDO2 information in a
17
 * database. It is tested with MySQL, others might work, too.
18
 *
19
 * It has the following options:
20
 * - dsn: The DSN which should be used to connect to the database server. See
21
 *   the PHP Manual for supported drivers and DSN formats.
22
 * - username: The username used for database connection.
23
 * - password: The password used for database connection.
24
 *
25
 * @author Stefan Winter <[email protected]>
26
 * @package SimpleSAMLphp
27
 */
28
29
class Database extends Store
30
{
31
    /**
32
     * Database handle.
33
     *
34
     * This variable can't be serialized.
35
     *
36
     * @var \SimpleSAML\Database
37
     */
38
    private SSP_Database $db;
39
40
    /**
41
     * The configuration for our database store.
42
     *
43
     * @var array
44
     */
45
    private array $config;
46
47
48
    /**
49
     * Parse configuration.
50
     *
51
     * This constructor parses the configuration.
52
     *
53
     * @param array $config Configuration for database consent store.
54
     *
55
     * @throws \Exception in case of a configuration error.
56
     */
57
    public function __construct(array $config)
58
    {
59
        parent::__construct($config);
60
        $this->config = $config;
61
        $this->db = SSP_Database::getInstance(Configuration::loadFromArray($config));
62
        $driver = $this->db->getDriver();
63
        // phpcs:disable Generic.Files.LineLength.TooLong
64
        try {
65
            $this->db->read("SELECT COUNT(*) FROM credentials");
66
        } catch (\Exception $e) {
67
            $this->db->write("
68
                CREATE TABLE IF NOT EXISTS credentials (
69
                    creation_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
70
                    user_id VARCHAR(80) NOT NULL,
71
                    credentialId VARCHAR(1024) " . ($driver === 'mysql' ? "CHARACTER SET 'binary'" : '') . " NOT NULL,
72
                    credential " . ($driver === 'pgsql' ? 'BYTEA' : 'MEDIUMBLOB') . " NOT NULL,
73
                    algo INT DEFAULT NULL,
74
                    presenceLevel INT DEFAULT NULL,
75
                    isResidentKey BOOLEAN DEFAULT NULL,
76
                    signCounter INT NOT NULL,
77
                    friendlyName VARCHAR(100) DEFAULT 'Unnamed Token',
78
                    hashedId VARCHAR(128) DEFAULT '---',
79
                    aaguid VARCHAR(64) DEFAULT NULL,
80
                    " . ($driver === 'pgsql'
81
                        ? "attLevel VARCHAR(6) NOT NULL DEFAULT 'None' CHECK (attLevel IN ('None','Basic','Self','AttCA')),"
82
                        : "attLevel ENUM('None','Basic','Self','AttCA') NOT NULL DEFAULT 'None',")
83
                    . "
84
                    lastUsedTime TIMESTAMP DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(),
85
                    lastUsedIp VARCHAR(64) DEFAULT NULL,
86
                    CONSTRAINT credentials_user_id_credentialId_key UNIQUE (user_id, credentialId)
87
                )
88
            ");
89
        }
90
        try {
91
            $this->db->read("SELECT COUNT(*) FROM userstatus");
92
        } catch (\Exception $e) {
93
            $this->db->write("CREATE TABLE IF NOT EXISTS userstatus (
94
            user_id VARCHAR(80) NOT NULL,
95
            " . ($driver === 'pgsql'
96
                ? "fido2Status VARCHAR(14) NOT NULL DEFAULT 'FIDO2Disabled' CHECK (fido2Status IN ('FIDO2Disabled', 'FIDO2Enabled')),"
97
                : "fido2Status ENUM('FIDO2Disabled','FIDO2Enabled') NOT NULL DEFAULT 'FIDO2Disabled',")
98
            . "
99
            CONSTRAINT userstatus_user_id_key UNIQUE (user_id)
100
            )");
101
        }
102
        // phpcs:enable Generic.Files.LineLength.TooLong
103
    }
104
105
106
    /**
107
     * Called before serialization.
108
     *
109
     * @return array The variables which should be serialized.
110
     */
111
    public function __sleep(): array
112
    {
113
        return [
114
            'config',
115
        ];
116
    }
117
118
119
    /**
120
     * Called after unserialization.
121
     */
122
    public function __wakeup(): void
123
    {
124
        $this->db = SSP_Database::getInstance(Configuration::loadFromArray($this->config));
125
    }
126
127
128
    /**
129
     * is the user subject to 2nd factor at all?
130
     *
131
     * This function checks whether a given user has been enabled for WebAuthn.
132
     *
133
     * @param string $userId The hash identifying the user at an IdP.
134
     * @param bool $defaultIfNx if not found in the DB, should the user be considered enabled (true)
135
     *                              or disabled(false)
136
     * @param bool $useDatabase a bool that determines whether to use local database or not
137
     * @param bool $toggle variable which is associated with $force because it determines its meaning, it either
138
     *                     simply means whether to trigger webauthn authentication or switch the default settings,
139
     * @param bool $force switch that determines how $toggle will be used, if true then value of $toggle
140
     *                    will mean whether to trigger (true) or not (false) the webauthn authentication,
141
     *                    if false then $toggle means whether to switch the value of $defaultEnabled and then use that
142
     *
143
     * @return bool True if the user is enabled for 2FA, false if not
144
     */
145
    public function is2FAEnabled(
146
        string $userId,
147
        bool $defaultIfNx,
148
        bool $useDatabase = true,
149
        bool $toggle = false,
150
        bool $force = true,
151
    ): bool {
152
        if (!$useDatabase) {
153
            if ($force) {
154
                return $toggle;
155
            } else {
156
                return $toggle ? !$defaultIfNx : $defaultIfNx;
157
            }
158
        }
159
        $st = $this->db->read('SELECT COUNT(*) FROM userstatus WHERE user_id = :userId', ['userId' => $userId]);
160
161
        $c = $st->fetchColumn();
162
        if ($c == 0) {
163
            Logger::debug('User does not exist in DB, returning desired default.');
164
            return $defaultIfNx;
165
        } else {
166
            $st2 = $this->db->read(
167
                "SELECT COUNT(*) FROM userstatus WHERE user_id = :userId AND fido2Status = 'FIDO2Disabled'",
168
                ['userId' => $userId],
169
            );
170
            $rowCount2 = $st2->fetchColumn();
171
            if ($rowCount2 === 1 /* explicitly disabled user in DB */) {
172
                return false;
173
            }
174
            Logger::debug('User exists and is not disabled -> enabled.');
175
            return true;
176
        }
177
    }
178
179
180
    /**
181
     * does a given credentialID already exist?
182
     *
183
     * This function checks whether a given credential ID already exists in the database
184
     *
185
     * @param string $credIdHex The hex representation of the credentialID to look for.
186
     *
187
     * @return bool True if the credential exists, false if not
188
     */
189
    public function doesCredentialExist(string $credIdHex): bool
190
    {
191
        $st = $this->db->read(
192
            'SELECT COUNT(*) FROM credentials WHERE credentialId = :credentialId',
193
            ['credentialId' => $credIdHex],
194
        );
195
196
        $rowCount = $st->fetchColumn();
197
        if (!$rowCount) {
198
            Logger::debug('Credential does not exist yet.');
199
            return false;
200
        } else {
201
            Logger::debug('Credential exists.');
202
            return true;
203
        }
204
    }
205
206
207
    /**
208
     * store newly enrolled token data
209
     *
210
     * @param string $userId        The user.
211
     * @param string $credentialId  The id identifying the credential.
212
     * @param string $credential    The credential.
213
     * @param int    $algo          The algorithm used.
214
     * @param int    $presenceLevel UV or UP?
215
     * @param int    $signCounter   The signature counter for this credential.
216
     * @param string $friendlyName  A user-supplied name for this token.
217
     * @param string $hashedId      hashed user ID
218
     */
219
    public function storeTokenData(
220
        string $userId,
221
        string $credentialId,
222
        string $credential,
223
        int $algo,
224
        int $presenceLevel,
225
        int $isResidentKey,
226
        int $signCounter,
227
        string $friendlyName,
228
        string $hashedId,
229
        string $aaguid,
230
        string $attLevel,
231
    ): true {
0 ignored issues
show
Bug introduced by
The type SimpleSAML\Module\webauthn\WebAuthn\Store\true was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
232
        // phpcs:disable Generic.Files.LineLength.TooLong
233
        $this->db->write(
234
            'INSERT INTO credentials ' .
235
            '(user_id, credentialId, credential, algo, presenceLevel, isResidentKey, signCounter, friendlyName, hashedId, aaguid, attLevel) VALUES ' .
236
            '(:userId,:credentialId,:credential,:algo,:presenceLevel,:isResidentKey,:signCounter,:friendlyName,:hashedId,:aaguid,:attLevel)',
237
            [
238
                'userId' => $userId,
239
                'credentialId' => $credentialId,
240
                'credential' => $credential,
241
                'algo' => $algo,
242
                'presenceLevel' => $presenceLevel,
243
                'isResidentKey' => $isResidentKey,
244
                'signCounter' => $signCounter,
245
                'friendlyName' => $friendlyName,
246
                'hashedId' => $hashedId,
247
                'aaguid' => $aaguid,
248
                'attLevel' => $attLevel,
249
            ],
250
        );
251
        // phpcs:enable Generic.Files.LineLength.TooLong
252
253
        return true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return true returns the type true which is incompatible with the type-hinted return SimpleSAML\Module\webauthn\WebAuthn\Store\true.
Loading history...
254
    }
255
256
257
    /**
258
     * remove an existing credential from the database
259
     *
260
     * @param string $credentialId the credential
261
     */
262
    public function deleteTokenData(string $credentialId): true
263
    {
264
        $this->db->write(
265
            'DELETE FROM credentials WHERE credentialId = :credentialId',
266
            ['credentialId' => $credentialId],
267
        );
268
269
        Logger::debug('webauthn:Database - DELETED credential.');
270
271
        return true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return true returns the type true which is incompatible with the type-hinted return SimpleSAML\Module\webauthn\WebAuthn\Store\true.
Loading history...
272
    }
273
274
275
    /**
276
     * increment the signature counter after a successful authentication
277
     *
278
     * @param string $credentialId the credential
279
     * @param int    $signCounter  the new counter value
280
     */
281
    public function updateSignCount(string $credentialId, int $signCounter): true
282
    {
283
        $this->db->write(
284
            'UPDATE credentials SET signCounter = :signCounter WHERE credentialId = :credentialId',
285
            ['signCounter' => $signCounter, 'credentialId' => $credentialId],
286
        );
287
288
        Logger::debug('webauthn:Database - UPDATED signature counter.');
289
290
        return true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return true returns the type true which is incompatible with the type-hinted return SimpleSAML\Module\webauthn\WebAuthn\Store\true.
Loading history...
291
    }
292
293
294
    /**
295
     * Retrieve existing token data
296
     *
297
     * @param string $userId the username
298
     * @return array Array of all crypto data we have on file.
299
     */
300
    public function getTokenData(string $userId): array
301
    {
302
        $ret = [];
303
304
        $st = $this->db->read(
305
        // phpcs:ignore Generic.Files.LineLength.TooLong
306
            'SELECT credentialId, credential, signCounter, friendlyName, algo, presenceLevel, isResidentKey FROM credentials WHERE user_id = :userId',
307
            ['userId' => $userId],
308
        );
309
310
        while ($row = $st->fetch(PDO::FETCH_NUM)) {
311
            if (is_resource($row[1])) {
312
                $row[1] = stream_get_contents($row[1]);
313
            }
314
            $ret[] = $row;
315
        }
316
317
        return $ret;
318
    }
319
320
321
    /**
322
     * Retrieve username, given a credential ID
323
     *
324
     * @param string $hashedId the credential ID
325
     * @return string the username, if found (otherwise, empty string)
326
     */
327
    public function getUsernameByHashedId(string $hashedId): string
328
    {
329
        $st = $this->db->read(
330
            'SELECT user_id FROM credentials WHERE hashedId = :hashId',
331
            ['hashId' => $hashedId],
332
        );
333
334
        // return on first match, credential IDs are unique
335
        while ($row = $st->fetch(PDO::FETCH_NUM)) {
336
            return $row[0];
337
        }
338
339
        return "";
340
    }
341
}
342