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