AuthModel::createUserJWT()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 54
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 26
nc 1
nop 21
dl 0
loc 54
rs 9.504
c 0
b 0
f 0

How to fix   Long Method    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/**
3
 * Teampass - a collaborative passwords manager.
4
 * ---
5
 * This file is part of the TeamPass project.
6
 * 
7
 * TeamPass is free software: you can redistribute it and/or modify it
8
 * under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation, version 3 of the License.
10
 * 
11
 * TeamPass is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 * 
16
 * You should have received a copy of the GNU General Public License
17
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
 * 
19
 * Certain components of this file may be under different licenses. For
20
 * details, see the `licenses` directory or individual file headers.
21
 * ---
22
 * @version    API
23
 *
24
 * @file      AuthModel.php
25
 * @author    Nils Laumaillé ([email protected])
26
 * @copyright 2009-2026 Teampass.net
27
 * @license   GPL-3.0
28
 * @see       https://www.teampass.net
29
 */
30
31
use TeampassClasses\PasswordManager\PasswordManager;
32
use TeampassClasses\NestedTree\NestedTree;
33
use TeampassClasses\ConfigManager\ConfigManager;
34
use Firebase\JWT\JWT;
35
use Firebase\JWT\Key;
36
37
class AuthModel
38
{
39
40
41
    /**
42
     * Is the user allowed
43
     *
44
     * @param string $login
45
     * @param string $password
46
     * @param string $apikey
47
     * @return array
48
     */
49
    public function getUserAuth(string $login, string $password, string $apikey): array
50
    {
51
        // Sanitize
52
        // IMPORTANT: Password should NOT be escaped/sanitized - treat as opaque binary data
53
        // Only trim whitespace which is safe and expected (fix 3.1.5.10)
54
        include_once API_ROOT_PATH . '/../sources/main.functions.php';
55
        $inputData = dataSanitizer(
56
            [
57
                'login' => isset($login) === true ? $login : '',
58
                'password' => isset($password) === true ? $password : '',
59
                'apikey' => isset($apikey) === true ? $apikey : '',
60
            ],
61
            [
62
                'login' => 'trim|escape|strip_tags',
63
                'password' => 'trim', // Only trim, NO escape/sanitization
64
                'apikey' => 'trim|escape|strip_tags',
65
            ]
66
        );
67
        
68
        // Check apikey and credentials
69
        if (empty($inputData['login']) === true || empty($inputData['apikey']) === true || empty($inputData['password']) === true) {
70
            // case where it is a generic key
71
            // Not allowed to use this API
72
73
            return ["error" => "Login failed.", "info" => "User password is requested"];
74
        } else {
75
            // case where it is a user api key
76
            // Check if user exists
77
            $userInfo = getUserCompleteData($inputData['login']);
78
79
            if ($userInfo === null) {
80
                return ["error" => "Login failed.", "info" => "Credentials not valid"];
81
            }
82
83
            // Check if user is enabled
84
            if ((int) $userInfo['api_enabled'] === 0) {
85
                return ["error" => "Login failed.", "info" => "User not allowed to use API"];
86
            }
87
            
88
            // Check password
89
            $passwordManager = new PasswordManager();
90
            if ($passwordManager->verifyPassword($userInfo['pw'], $inputData['password']) === true) {
91
                // Correct credentials
92
                // get user keys
93
                $privateKeyClear = decryptPrivateKey($inputData['password'], (string) $userInfo['private_key']);
94
95
                // check API key
96
                if ($inputData['apikey'] !== base64_decode(decryptUserObjectKey($userInfo['api_key'], $privateKeyClear))) {
97
                    return ["error" => "Login failed.", "info" => "API Key not valid"];
98
                }
99
100
                // Update user's key_tempo
101
                $keyTempo = getOrRotateKeyTempo($userInfo['id'], 3600);
102
103
                // Generate a unique session key for this API session (256 bits / 32 bytes)
104
                // This key will be stored in the JWT and used to decrypt the private key
105
                $sessionKey = random_bytes(32);
106
                $sessionKeySalt = bin2hex(random_bytes(16));
107
108
                // Encrypt the decrypted private key with the session key
109
                // This allows us to store it securely in the database without exposing it
110
                require_once API_ROOT_PATH . '/inc/encryption_utils.php';
111
                $encryptedPrivateKey = encrypt_with_session_key($privateKeyClear, $sessionKey);
112
113
                if ($encryptedPrivateKey === false) {
114
                    return ["error" => "Login failed.", "info" => "Failed to encrypt private key"];
115
                }
116
117
                // Store the ENCRYPTED private key in the API table
118
                // Even if the database is compromised, the key cannot be used without the session_key from the JWT
119
                DB::update(
120
                    prefixTable('api'),
121
                    [
122
                        'encrypted_private_key' => $encryptedPrivateKey,
123
                        'session_key_salt' => $sessionKeySalt,
124
                        'session_key' => $keyTempo,
125
                        'timestamp' => time(),
126
                    ],
127
                    'user_id = %i',
128
                    $userInfo['id']
129
                );
130
131
                // get user folders list
132
                $ret = $this->buildUserFoldersList($userInfo);
133
134
                // Load config
135
                $configManager = new ConfigManager();
136
                $SETTINGS = $configManager->getAllSettings();
137
138
                // Log user
139
                logEvents($SETTINGS, 'api', 'user_connection', (string) $userInfo['id'], stripslashes($userInfo['login']));
140
141
                // create JWT with session key
142
                return $this->createUserJWT(
143
                    (int) $userInfo['id'],
144
                    (string) $inputData['login'],
145
                    (string) $userInfo['email'],
146
                    (int) $userInfo['personal_folder'],
147
                    (string) implode(",", $ret['folders']),
148
                    (string) implode(",", $ret['items']),
149
                    (string) $keyTempo,
150
                    (string) base64_encode($sessionKey), // Session key for decrypting private key
151
                    (int) $userInfo['admin'],
152
                    (int) $userInfo['gestionnaire'],
153
                    (int) $userInfo['can_create_root_folder'],
154
                    (int) $userInfo['can_manage_all_users'],
155
                    (string) $userInfo['fonction_id'],
156
                    (string) $userInfo['api_allowed_folders'],
157
                    (int) $userInfo['api_allowed_to_create'],
158
                    (int) $userInfo['api_allowed_to_read'],
159
                    (int) $userInfo['api_allowed_to_update'],
160
                    (int) $userInfo['api_allowed_to_delete'],
161
                    (int) $SETTINGS['api_token_duration'] ?? 60,
162
                    (int) $SETTINGS['pwd_maximum_length'] ?? 60,
163
                    (int) $SETTINGS['maintenance_mode'] ?? 0,
164
                );
165
            } else {
166
                return ["error" => "Login failed.", "info" => "Credentials not valid"];
167
            }
168
        }
169
    }
170
    //end getUserAuth
171
172
    /**
173
     * Create a JWT
174
     *
175
     * Note: User encryption keys (public_key, private_key) are no longer stored in the JWT
176
     * to reduce token size. Instead, the private key is encrypted with a session key and
177
     * stored in the database. The session key is stored in the JWT (~44 bytes) which is
178
     * used to decrypt the private key when needed.
179
     *
180
     * Security approach (defense in depth):
181
     * - Encrypted private key in DB is useless without session_key
182
     * - session_key in JWT is useless without encrypted private key from DB
183
     * - Both are required together to access the decrypted private key
184
     *
185
     * @param integer $id
186
     * @param string $login
187
     * @param string $email
188
     * @param integer $pf_enabled
189
     * @param string $folders
190
     * @param string $items
191
     * @param string $keyTempo
192
     * @param string $sessionKey Session key (base64) for decrypting private key from DB
193
     * @param integer $admin
194
     * @param integer $manager
195
     * @param integer $can_create_root_folder
196
     * @param integer $can_manage_all_users
197
     * @param string $roles
198
     * @param string $allowed_folders
199
     * @param integer $allowed_to_create
200
     * @param integer $allowed_to_read
201
     * @param integer $allowed_to_update
202
     * @param integer $allowed_to_delete
203
     * @param integer $api_token_duration
204
     * @param integer $pwd_maximum_length
205
     * @param integer $maintenance_mode
206
     * @return array
207
     */
208
    private function createUserJWT(
209
        int $id,
210
        string $login,
211
        string $email,
212
        int $pf_enabled,
213
        string $folders,
214
        string $items,
215
        string $keyTempo,
216
        string $sessionKey,
217
        int $admin,
218
        int $manager,
219
        int $can_create_root_folder,
220
        int $can_manage_all_users,
221
        string $roles,
222
        string $allowed_folders,
223
        int $allowed_to_create,
224
        int $allowed_to_read,
225
        int $allowed_to_update,
226
        int $allowed_to_delete,
227
        int $api_token_duration,
228
        int $pwd_maximum_length,
229
        int $maintenance_mode
230
    ): array
231
    {
232
        // Load config
233
        $configManager = new ConfigManager();
234
        $SETTINGS = $configManager->getAllSettings();
235
236
		$payload = [
237
            'username' => $login,
238
            'id' => $id,
239
            'exp' => (time() + $SETTINGS['api_token_duration'] + 600),
240
            'pf_enabled' => $pf_enabled,
241
            'folders_list' => $folders,
242
            'restricted_items_list' => $items,
243
            'key_tempo' => $keyTempo,
244
            'session_key' => $sessionKey, // Session key for decrypting private key from database
245
            'is_admin' => $admin,
246
            'is_manager' => $manager,
247
            'user_can_create_root_folder' => $can_create_root_folder,
248
            'user_can_manage_all_users' => $can_manage_all_users,
249
            'roles' => $roles,
250
            'allowed_folders' => $allowed_folders,
251
            'allowed_to_create' => $allowed_to_create,
252
            'allowed_to_read' => $allowed_to_read,
253
            'allowed_to_update' => $allowed_to_update,
254
            'allowed_to_delete' => $allowed_to_delete,
255
            'email' => $email,
256
            'api_token_duration' => $api_token_duration,
257
            'pwd_maximum_length' => $pwd_maximum_length,
258
            'maintenance_mode' => $maintenance_mode,
259
        ];
260
261
        return ['token' => JWT::encode($payload, DB_PASSWD, 'HS256')];
262
    }
263
264
    //end createUserJWT
265
266
267
    /**
268
     * Permit to build the list of folders the user can access
269
     *
270
     * @param array $userInfo
271
     * @return array
272
     */
273
    private function buildUserFoldersList(array $userInfo): array
274
    {
275
        //Build tree
276
        $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
277
        
278
        // Start by adding the manually added folders
279
        $allowedFolders = array_map('intval', explode(";", $userInfo['groupes_visibles']));
280
        $readOnlyFolders = [];
281
        $allowedFoldersByRoles = [];
282
        $restrictedFoldersForItems = [];
283
        $foldersLimited = [];
284
        $foldersLimitedFull = [];
285
        $restrictedItems = [];
286
        $personalFolders = [];
287
288
        $userFunctionId = explode(";", $userInfo['fonction_id']);
289
290
        // Get folders from the roles
291
        if (count($userFunctionId) > 0) {
292
            $rows = DB::query(
293
                'SELECT * 
294
                FROM ' . prefixTable('roles_values') . '
295
                WHERE role_id IN %li  AND type IN ("W", "ND", "NE", "NDNE", "R")',
296
                $userFunctionId
297
            );
298
            foreach ($rows as $record) {
299
                if ($record['type'] === 'R') {
300
                    array_push($readOnlyFolders, $record['folder_id']);
301
                } elseif (in_array($record['folder_id'], $allowedFolders) === false) {
302
                    array_push($allowedFoldersByRoles, $record['folder_id']);
303
                }
304
            }
305
            $allowedFoldersByRoles = array_unique($allowedFoldersByRoles);
306
            $readOnlyFolders = array_unique($readOnlyFolders);
307
            // Clean arrays
308
            foreach ($allowedFoldersByRoles as $value) {
309
                $key = array_search($value, $readOnlyFolders);
310
                if ($key !== false) {
311
                    unset($readOnlyFolders[$key]);
312
                }
313
            }
314
        }
315
        
316
        // Does this user is allowed to see other items
317
        $inc = 0;
318
        $rows = DB::query(
319
            'SELECT id, id_tree 
320
            FROM ' . prefixTable('items') . '
321
            WHERE restricted_to LIKE %s'.
322
            (count($userFunctionId) > 0 ? ' AND id_tree NOT IN %li' : ''),
323
            $userInfo['id'],
324
            count($userFunctionId) > 0 ? $userFunctionId : DB::sqleval('0')
325
        );
326
        foreach ($rows as $record) {
327
            // Exclude restriction on item if folder is fully accessible
328
            $restrictedFoldersForItems[$inc] = $record['id_tree'];
329
            ++$inc;
330
        }
331
332
        // Check for the users roles if some specific rights exist on items
333
        $rows = DB::query(
334
            'SELECT i.id_tree, r.item_id
335
            FROM ' . prefixTable('items') . ' AS i
336
            INNER JOIN ' . prefixTable('restriction_to_roles') . ' AS r ON (r.item_id=i.id)
337
            WHERE '.(count($userFunctionId) > 0 ? ' id_tree NOT IN %li AND ' : '').' i.id_tree != ""
338
            ORDER BY i.id_tree ASC',
339
            count($userFunctionId) > 0 ? $userFunctionId : DB::sqleval('0')
340
        );
341
        foreach ($rows as $record) {
342
            $foldersLimited[$record['id_tree']][$inc] = $record['item_id'];
343
            //array_push($foldersLimitedFull, $record['item_id']);
344
            array_push($restrictedItems, $record['item_id']);
345
            array_push($foldersLimitedFull, $record['id_tree']);
346
            ++$inc;
347
        }
348
349
        // Add all personal folders
350
        $rows = DB::queryFirstRow(
351
            'SELECT id 
352
            FROM ' . prefixTable('nested_tree') . '
353
            WHERE title = %i AND personal_folder = 1'.
354
            (count($userFunctionId) > 0 ? ' AND id NOT IN %li' : ''),
355
            $userInfo['id'],
356
            count($userFunctionId) > 0 ? $userFunctionId : DB::sqleval('0')
357
        );
358
        if (empty($rows['id']) === false) {
359
            array_push($personalFolders, $rows['id']);
360
            // get all descendants
361
            $ids = $tree->getDescendants($rows['id'], false, false, true);
362
            foreach ($ids as $id) {
363
                array_push($personalFolders, $id);
364
            }
365
        }
366
367
        // All folders visibles
368
        return [
369
            'folders' => array_unique(
370
            array_filter(
371
                array_merge(
372
                    $allowedFolders,
373
                    $foldersLimitedFull,
374
                    $allowedFoldersByRoles,
375
                    $restrictedFoldersForItems,
376
                    $readOnlyFolders,
377
                    $personalFolders
378
                )
379
                )
380
            ),
381
            'items' => array_unique($restrictedItems),
382
        ];
383
    }
384
    //end buildUserFoldersList
385
}