AuthModel::createUserJWT()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 46
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 22
nc 1
nop 17
dl 0
loc 46
rs 9.568
c 0
b 0
f 0

How to fix   Many Parameters   

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-2025 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 = bin2hex(random_bytes(16));
102
                /*DB::update(
103
                    prefixTable('users'),
104
                    [
105
                        'key_tempo' => $keyTempo,
106
                    ],
107
                    'id = %i',
108
                    $userInfo['id']
109
                );*/
110
111
                // Generate a unique session key for this API session (256 bits / 32 bytes)
112
                // This key will be stored in the JWT and used to decrypt the private key
113
                $sessionKey = random_bytes(32);
114
                $sessionKeySalt = bin2hex(random_bytes(16));
115
116
                // Encrypt the decrypted private key with the session key
117
                // This allows us to store it securely in the database without exposing it
118
                require_once API_ROOT_PATH . '/inc/encryption_utils.php';
119
                $encryptedPrivateKey = encrypt_with_session_key($privateKeyClear, $sessionKey);
120
121
                if ($encryptedPrivateKey === false) {
122
                    return ["error" => "Login failed.", "info" => "Failed to encrypt private key"];
123
                }
124
125
                // Store the ENCRYPTED private key in the API table
126
                // Even if the database is compromised, the key cannot be used without the session_key from the JWT
127
                DB::update(
128
                    prefixTable('api'),
129
                    [
130
                        'encrypted_private_key' => $encryptedPrivateKey,
131
                        'session_key_salt' => $sessionKeySalt,
132
                        'session_key' => $keyTempo,
133
                        'timestamp' => time(),
134
                    ],
135
                    'user_id = %i',
136
                    $userInfo['id']
137
                );
138
139
                // get user folders list
140
                $ret = $this->buildUserFoldersList($userInfo);
141
142
                // Load config
143
                $configManager = new ConfigManager();
144
                $SETTINGS = $configManager->getAllSettings();
145
146
                // Log user
147
                logEvents($SETTINGS, 'api', 'user_connection', (string) $userInfo['id'], stripslashes($userInfo['login']));
148
149
                // create JWT with session key
150
                return $this->createUserJWT(
151
                    (int) $userInfo['id'],
152
                    (string) $inputData['login'],
153
                    (int) $userInfo['personal_folder'],
154
                    (string) implode(",", $ret['folders']),
155
                    (string) implode(",", $ret['items']),
156
                    (string) $keyTempo,
157
                    (string) base64_encode($sessionKey), // Session key for decrypting private key
158
                    (int) $userInfo['admin'],
159
                    (int) $userInfo['gestionnaire'],
160
                    (int) $userInfo['can_create_root_folder'],
161
                    (int) $userInfo['can_manage_all_users'],
162
                    (string) $userInfo['fonction_id'],
163
                    (string) $userInfo['api_allowed_folders'],
164
                    (int) $userInfo['api_allowed_to_create'],
165
                    (int) $userInfo['api_allowed_to_read'],
166
                    (int) $userInfo['api_allowed_to_update'],
167
                    (int) $userInfo['api_allowed_to_delete'],
168
                );
169
            } else {
170
                return ["error" => "Login failed.", "info" => "Credentials not valid"];
171
            }
172
        }
173
    }
174
    //end getUserAuth
175
176
    /**
177
     * Create a JWT
178
     *
179
     * Note: User encryption keys (public_key, private_key) are no longer stored in the JWT
180
     * to reduce token size. Instead, the private key is encrypted with a session key and
181
     * stored in the database. The session key is stored in the JWT (~44 bytes) which is
182
     * used to decrypt the private key when needed.
183
     *
184
     * Security approach (defense in depth):
185
     * - Encrypted private key in DB is useless without session_key
186
     * - session_key in JWT is useless without encrypted private key from DB
187
     * - Both are required together to access the decrypted private key
188
     *
189
     * @param integer $id
190
     * @param string $login
191
     * @param integer $pf_enabled
192
     * @param string $folders
193
     * @param string $items
194
     * @param string $keyTempo
195
     * @param string $sessionKey Session key (base64) for decrypting private key from DB
196
     * @param integer $admin
197
     * @param integer $manager
198
     * @param integer $can_create_root_folder
199
     * @param integer $can_manage_all_users
200
     * @param string $roles
201
     * @param string $allowed_folders
202
     * @param integer $allowed_to_create
203
     * @param integer $allowed_to_read
204
     * @param integer $allowed_to_update
205
     * @param integer $allowed_to_delete
206
     * @return array
207
     */
208
    private function createUserJWT(
209
        int $id,
210
        string $login,
211
        int $pf_enabled,
212
        string $folders,
213
        string $items,
214
        string $keyTempo,
215
        string $sessionKey,
216
        int $admin,
217
        int $manager,
218
        int $can_create_root_folder,
219
        int $can_manage_all_users,
220
        string $roles,
221
        string $allowed_folders,
222
        int $allowed_to_create,
223
        int $allowed_to_read,
224
        int $allowed_to_update,
225
        int $allowed_to_delete,
226
    ): array
227
    {
228
        // Load config
229
        $configManager = new ConfigManager();
230
        $SETTINGS = $configManager->getAllSettings();
231
232
		$payload = [
233
            'username' => $login,
234
            'id' => $id,
235
            'exp' => (time() + $SETTINGS['api_token_duration'] + 600),
236
            'pf_enabled' => $pf_enabled,
237
            'folders_list' => $folders,
238
            'restricted_items_list' => $items,
239
            'key_tempo' => $keyTempo,
240
            'session_key' => $sessionKey, // Session key for decrypting private key from database
241
            'is_admin' => $admin,
242
            'is_manager' => $manager,
243
            'user_can_create_root_folder' => $can_create_root_folder,
244
            'user_can_manage_all_users' => $can_manage_all_users,
245
            'roles' => $roles,
246
            'allowed_folders' => $allowed_folders,
247
            'allowed_to_create' => $allowed_to_create,
248
            'allowed_to_read' => $allowed_to_read,
249
            'allowed_to_update' => $allowed_to_update,
250
            'allowed_to_delete' => $allowed_to_delete,
251
        ];
252
253
        return ['token' => JWT::encode($payload, DB_PASSWD, 'HS256')];
254
    }
255
256
    //end createUserJWT
257
258
259
    /**
260
     * Permit to build the list of folders the user can access
261
     *
262
     * @param array $userInfo
263
     * @return array
264
     */
265
    private function buildUserFoldersList(array $userInfo): array
266
    {
267
        //Build tree
268
        $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
269
        
270
        // Start by adding the manually added folders
271
        $allowedFolders = array_map('intval', explode(";", $userInfo['groupes_visibles']));
272
        $readOnlyFolders = [];
273
        $allowedFoldersByRoles = [];
274
        $restrictedFoldersForItems = [];
275
        $foldersLimited = [];
276
        $foldersLimitedFull = [];
277
        $restrictedItems = [];
278
        $personalFolders = [];
279
280
        $userFunctionId = explode(";", $userInfo['fonction_id']);
281
282
        // Get folders from the roles
283
        if (count($userFunctionId) > 0) {
284
            $rows = DB::query(
285
                'SELECT * 
286
                FROM ' . prefixTable('roles_values') . '
287
                WHERE role_id IN %li  AND type IN ("W", "ND", "NE", "NDNE", "R")',
288
                $userFunctionId
289
            );
290
            foreach ($rows as $record) {
291
                if ($record['type'] === 'R') {
292
                    array_push($readOnlyFolders, $record['folder_id']);
293
                } elseif (in_array($record['folder_id'], $allowedFolders) === false) {
294
                    array_push($allowedFoldersByRoles, $record['folder_id']);
295
                }
296
            }
297
            $allowedFoldersByRoles = array_unique($allowedFoldersByRoles);
298
            $readOnlyFolders = array_unique($readOnlyFolders);
299
            // Clean arrays
300
            foreach ($allowedFoldersByRoles as $value) {
301
                $key = array_search($value, $readOnlyFolders);
302
                if ($key !== false) {
303
                    unset($readOnlyFolders[$key]);
304
                }
305
            }
306
        }
307
        
308
        // Does this user is allowed to see other items
309
        $inc = 0;
310
        $rows = DB::query(
311
            'SELECT id, id_tree 
312
            FROM ' . prefixTable('items') . '
313
            WHERE restricted_to LIKE %s'.
314
            (count($userFunctionId) > 0 ? ' AND id_tree NOT IN %li' : ''),
315
            $userInfo['id'],
316
            count($userFunctionId) > 0 ? $userFunctionId : DB::sqleval('0')
317
        );
318
        foreach ($rows as $record) {
319
            // Exclude restriction on item if folder is fully accessible
320
            $restrictedFoldersForItems[$inc] = $record['id_tree'];
321
            ++$inc;
322
        }
323
324
        // Check for the users roles if some specific rights exist on items
325
        $rows = DB::query(
326
            'SELECT i.id_tree, r.item_id
327
            FROM ' . prefixTable('items') . ' AS i
328
            INNER JOIN ' . prefixTable('restriction_to_roles') . ' AS r ON (r.item_id=i.id)
329
            WHERE '.(count($userFunctionId) > 0 ? ' id_tree NOT IN %li AND ' : '').' i.id_tree != ""
330
            ORDER BY i.id_tree ASC',
331
            count($userFunctionId) > 0 ? $userFunctionId : DB::sqleval('0')
332
        );
333
        foreach ($rows as $record) {
334
            $foldersLimited[$record['id_tree']][$inc] = $record['item_id'];
335
            //array_push($foldersLimitedFull, $record['item_id']);
336
            array_push($restrictedItems, $record['item_id']);
337
            array_push($foldersLimitedFull, $record['id_tree']);
338
            ++$inc;
339
        }
340
341
        // Add all personal folders
342
        $rows = DB::queryFirstRow(
343
            'SELECT id 
344
            FROM ' . prefixTable('nested_tree') . '
345
            WHERE title = %i AND personal_folder = 1'.
346
            (count($userFunctionId) > 0 ? ' AND id NOT IN %li' : ''),
347
            $userInfo['id'],
348
            count($userFunctionId) > 0 ? $userFunctionId : DB::sqleval('0')
349
        );
350
        if (empty($rows['id']) === false) {
351
            array_push($personalFolders, $rows['id']);
352
            // get all descendants
353
            $ids = $tree->getDescendants($rows['id'], false, false, true);
354
            foreach ($ids as $id) {
355
                array_push($personalFolders, $id);
356
            }
357
        }
358
359
        // All folders visibles
360
        return [
361
            'folders' => array_unique(
362
            array_filter(
363
                array_merge(
364
                    $allowedFolders,
365
                    $foldersLimitedFull,
366
                    $allowedFoldersByRoles,
367
                    $restrictedFoldersForItems,
368
                    $readOnlyFolders,
369
                    $personalFolders
370
                )
371
                )
372
            ),
373
            'items' => array_unique($restrictedItems),
374
        ];
375
    }
376
    //end buildUserFoldersList
377
}