Passed
Push — master ( e7d28b...261004 )
by Nils
06:20
created

prepareUserEncryptionKeys()   C

Complexity

Conditions 16
Paths 64

Size

Total Lines 102
Code Lines 60

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 16
eloc 60
c 1
b 1
f 0
nc 64
nop 3
dl 0
loc 102
rs 5.5666

How to fix   Long Method    Complexity   

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:

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Teampass - a collaborative passwords manager.
7
 * ---
8
 * This file is part of the TeamPass project.
9
 * 
10
 * TeamPass is free software: you can redistribute it and/or modify it
11
 * under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, version 3 of the License.
13
 * 
14
 * TeamPass is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 * 
19
 * You should have received a copy of the GNU General Public License
20
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21
 * 
22
 * Certain components of this file may be under different licenses. For
23
 * details, see the `licenses` directory or individual file headers.
24
 * ---
25
 * @file      identify.php
26
 * @author    Nils Laumaillé ([email protected])
27
 * @copyright 2009-2025 Teampass.net
28
 * @license   GPL-3.0
29
 * @see       https://www.teampass.net
30
 */
31
32
use voku\helper\AntiXSS;
33
use TeampassClasses\SessionManager\SessionManager;
34
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
35
use TeampassClasses\Language\Language;
36
use TeampassClasses\PerformChecks\PerformChecks;
37
use TeampassClasses\ConfigManager\ConfigManager;
38
use TeampassClasses\NestedTree\NestedTree;
39
use TeampassClasses\PasswordManager\PasswordManager;
40
use Duo\DuoUniversal\Client;
41
use Duo\DuoUniversal\DuoException;
42
use RobThree\Auth\TwoFactorAuth;
43
use TeampassClasses\LdapExtra\LdapExtra;
44
use TeampassClasses\LdapExtra\OpenLdapExtra;
45
use TeampassClasses\LdapExtra\ActiveDirectoryExtra;
46
use TeampassClasses\OAuth2Controller\OAuth2Controller;
47
48
// Load functions
49
require_once 'main.functions.php';
50
51
// init
52
loadClasses('DB');
53
$session = SessionManager::getSession();
54
$request = SymfonyRequest::createFromGlobals();
55
$lang = new Language($session->get('user-language') ?? 'english');
56
57
// Load config
58
$configManager = new ConfigManager();
59
$SETTINGS = $configManager->getAllSettings();
60
61
// Define Timezone
62
date_default_timezone_set($SETTINGS['timezone'] ?? 'UTC');
63
64
// Set header properties
65
header('Content-type: text/html; charset=utf-8');
66
header('Cache-Control: no-cache, no-store, must-revalidate');
67
error_reporting(E_ERROR);
68
69
// --------------------------------- //
70
71
// Prepare POST variables
72
$post_type = filter_input(INPUT_POST, 'type', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
73
$post_login = filter_input(INPUT_POST, 'login', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
74
$post_data = filter_input(INPUT_POST, 'data', FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_FLAG_NO_ENCODE_QUOTES);
75
76
if ($post_type === 'identify_user') {
77
    //--------
78
    // NORMAL IDENTICATION STEP
79
    //--------
80
81
    // Ensure Complexity levels are translated
82
    defineComplexity();
83
84
    // Identify the user through Teampass process
85
    identifyUser($post_data, $SETTINGS);
86
87
    // ---
88
    // ---
89
    // ---
90
} elseif ($post_type === 'get2FAMethods') {
91
    //--------
92
    // Get MFA methods
93
    //--------
94
    //
95
96
    // Encrypt data to return
97
    echo json_encode([
98
        'ret' => prepareExchangedData(
99
            [
100
                'agses' => isKeyExistingAndEqual('agses_authentication_enabled', 1, $SETTINGS) === true ? true : false,
101
                'google' => isKeyExistingAndEqual('google_authentication', 1, $SETTINGS) === true ? true : false,
102
                'yubico' => isKeyExistingAndEqual('yubico_authentication', 1, $SETTINGS) === true ? true : false,
103
                'duo' => isKeyExistingAndEqual('duo', 1, $SETTINGS) === true ? true : false,
104
            ],
105
            'encode'
106
        ),
107
        'key' => $session->get('key'),
108
    ]);
109
    return false;
110
} elseif ($post_type === 'initiateSSOLogin') {
111
    //--------
112
    // Do initiateSSOLogin
113
    //--------
114
    //
115
116
    // Création d'une instance du contrôleur
117
    $OAuth2 = new OAuth2Controller($SETTINGS);
118
119
    // Redirection vers Azure pour l'authentification
120
    $OAuth2->redirect();
121
122
    // Encrypt data to return
123
    echo json_encode([
124
        'key' => $session->get('key'),
125
    ]);
126
    return false;
127
}
128
129
/**
130
 * Complete authentication of user through Teampass
131
 *
132
 * @param string $sentData Credentials
133
 * @param array $SETTINGS Teampass settings
134
 *
135
 * @return bool
136
 */
137
function identifyUser(string $sentData, array $SETTINGS): bool
138
{
139
    $antiXss = new AntiXSS();
140
    $session = SessionManager::getSession();
141
    $request = SymfonyRequest::createFromGlobals();
142
    $lang = new Language($session->get('user-language') ?? 'english');
143
    $session = SessionManager::getSession();
144
145
    // Prepare GET variables
146
    $sessionAdmin = $session->get('user-admin');
147
    $sessionPwdAttempts = $session->get('pwd_attempts');
148
    $sessionUrl = $session->get('user-initial_url');
149
    $server = [];
150
    $server['PHP_AUTH_USER'] =  $request->getUser();
151
    $server['PHP_AUTH_PW'] = $request->getPassword();
152
    
153
    // decrypt and retreive data in JSON format
154
    if ($session->get('key') === null) {
155
        $dataReceived = $sentData;
156
    } else {
157
        $dataReceived = prepareExchangedData(
158
            $sentData,
159
            'decode',
160
            $session->get('key')
161
        );
162
    }
163
164
    // Base64 decode sensitive data
165
    if (isset($dataReceived['pw'])) {
166
        $dataReceived['pw'] = base64_decode($dataReceived['pw']);
167
    }
168
169
    // Check if Duo auth is in progress and pass the pw and login back to the standard login process
170
    if(
171
        isKeyExistingAndEqual('duo', 1, $SETTINGS) === true
172
        && $dataReceived['user_2fa_selection'] === 'duo'
173
        && $session->get('user-duo_status') === 'IN_PROGRESS'
174
        && !empty($dataReceived['duo_state'])
175
    ){
176
        $key = hash('sha256', $dataReceived['duo_state']);
177
        $iv = substr(hash('sha256', $dataReceived['duo_state']), 0, 16);
178
        $duo_data_dec = openssl_decrypt(base64_decode($session->get('user-duo_data')), 'AES-256-CBC', $key, 0, $iv);
179
        // Clear the data from the Duo process to continue clean with the standard login process
180
        $session->set('user-duo_data','');
181
        if($duo_data_dec === false) {
182
            // Add failed authentication log
183
            addFailedAuthentication(filter_var($dataReceived['login'], FILTER_SANITIZE_FULL_SPECIAL_CHARS), getClientIpServer());
184
185
            echo prepareExchangedData(
186
                [
187
                    'error' => true,
188
                    'message' => $lang->get('duo_error_decrypt'),
189
                ],
190
                'encode'
191
            );
192
            return false;
193
        }
194
        $duo_data = unserialize($duo_data_dec);
195
        $dataReceived['pw'] = $duo_data['duo_pwd'];
196
        $dataReceived['login'] = $duo_data['duo_login'];
197
    }
198
199
    if(isset($dataReceived['pw']) === false || isset($dataReceived['login']) === false) {
200
        echo json_encode([
201
            'data' => prepareExchangedData(
202
                [
203
                    'error' => true,
204
                    'message' => $lang->get('ga_enter_credentials'),
205
                ],
206
                'encode'
207
            ),
208
            'key' => $session->get('key')
209
        ]);
210
        return false;
211
    }
212
213
    // prepare variables    
214
    $userCredentials = identifyGetUserCredentials(
215
        $SETTINGS,
216
        (string) $server['PHP_AUTH_USER'],
217
        (string) $server['PHP_AUTH_PW'],
218
        (string) filter_var($dataReceived['pw'], FILTER_SANITIZE_FULL_SPECIAL_CHARS),
219
        (string) filter_var($dataReceived['login'], FILTER_SANITIZE_FULL_SPECIAL_CHARS)
220
    );
221
    $username = $userCredentials['username'];
222
    $passwordClear = $userCredentials['passwordClear'];
223
224
    // DO initial checks
225
    $userInitialData = identifyDoInitialChecks(
226
        $SETTINGS,
227
        (int) $sessionPwdAttempts,
228
        (string) $username,
229
        (int) $sessionAdmin,
230
        (string) $sessionUrl,
231
        (string) filter_var($dataReceived['user_2fa_selection'], FILTER_SANITIZE_FULL_SPECIAL_CHARS)
232
    );
233
234
    // if user doesn't exist in Teampass then return error
235
    if ($userInitialData['error'] === true) {
236
        // Add log on error unless skip_anti_bruteforce flag is set to true
237
        if (empty($userInitialData['skip_anti_bruteforce'])
238
            || !$userInitialData['skip_anti_bruteforce']) {
239
240
            // Add failed authentication log
241
            addFailedAuthentication($username, getClientIpServer());
242
        }
243
244
        echo prepareExchangedData(
245
            $userInitialData['array'],
246
            'encode'
247
        );
248
        return false;
249
    }
250
251
    $userInfo = $userInitialData['userInfo'] + $dataReceived;
252
    $return = '';
253
254
    // Check if LDAP is enabled and user is in AD
255
    $userLdap = identifyDoLDAPChecks(
256
        $SETTINGS,
257
        $userInfo,
258
        (string) $username,
259
        (string) $passwordClear,
260
        (int) $sessionAdmin,
261
        (string) $sessionUrl,
262
        (int) $sessionPwdAttempts
263
    );
264
    if ($userLdap['error'] === true) {
265
        // Add failed authentication log
266
        addFailedAuthentication($username, getClientIpServer());
267
268
        // deepcode ignore ServerLeak: File and path are secured directly inside the function decryptFile()
269
        echo prepareExchangedData(
270
            $userLdap['array'],
271
            'encode'
272
        );
273
        return false;
274
    }
275
    if (isset($userLdap['user_info']) === true && (int) $userLdap['user_info']['has_been_created'] === 1) {
276
        // Add failed authentication log
277
        addFailedAuthentication($username, getClientIpServer());
278
279
        echo json_encode([
280
            'data' => prepareExchangedData(
281
                [
282
                    'error' => true,
283
                    'message' => '',
284
                    'extra' => 'ad_user_created',
285
                ],
286
                'encode'
287
            ),
288
            'key' => $session->get('key')
289
        ]);
290
        return false;
291
    }
292
293
    // Is oauth2 user exists?
294
    $userOauth2 = checkOauth2User(
295
        (array) $SETTINGS,
296
        (array) $userInfo,
297
        (string) $username,
298
        (string) $passwordClear,
299
        (int) $userLdap['user_info']['has_been_created']
300
    );
301
    if ($userOauth2['error'] === true) {
302
        $session->set('userOauth2Info', '');
303
304
        // Add failed authentication log
305
        if ($userOauth2['no_log_event'] !== true) {
306
            addFailedAuthentication($username, getClientIpServer());
307
        }
308
309
        // deepcode ignore ServerLeak: File and path are secured directly inside the function decryptFile()        
310
        echo prepareExchangedData(
311
            [
312
                'error' => true,
313
                'message' => $lang->get($userOauth2['message']),
314
                'extra' => 'oauth2_user_not_found',
315
            ],
316
            'encode'
317
        );
318
        return false;
319
    }
320
321
    // Check user and password
322
    $authResult = checkCredentials($passwordClear, $userInfo);
323
    if ($userLdap['userPasswordVerified'] === false && $userOauth2['userPasswordVerified'] === false && $authResult['authenticated'] !== true) {
324
        // Add failed authentication log
325
        addFailedAuthentication($username, getClientIpServer());
326
        echo prepareExchangedData(
327
            [
328
                'value' => '',
329
                'error' => true,
330
                'message' => $lang->get('error_bad_credentials'),
331
            ],
332
            'encode'
333
        );
334
        return false;
335
    }
336
337
    // If user was migrated, then return error to force user to wait
338
    if ($authResult['migrated'] === true && (int) $userInfo['admin'] !== 1) {
339
        echo prepareExchangedData(
340
            [
341
                'value' => '',
342
                'error' => true,
343
                'message' => $lang->get('user_encryption_ongoing'),
344
            ],
345
            'encode'
346
        );
347
        return false;
348
    }
349
350
    // Check if MFA is required
351
    if ((isOneVarOfArrayEqualToValue(
352
                [
353
                    (int) $SETTINGS['yubico_authentication'],
354
                    (int) $SETTINGS['google_authentication'],
355
                    (int) $SETTINGS['duo']
356
                ],
357
                1
358
            ) === true)
359
        && (((int) $userInfo['admin'] !== 1 && (int) $userInfo['mfa_enabled'] === 1 && $userInfo['mfa_auth_requested_roles'] === true)
360
        || ((int) $SETTINGS['admin_2fa_required'] === 1 && (int) $userInfo['admin'] === 1))
361
    ) {
362
        // Check user against MFA method if selected
363
        $userMfa = identifyDoMFAChecks(
364
            $SETTINGS,
365
            $userInfo,
366
            $dataReceived,
367
            $userInitialData,
368
            (string) $username
369
        );
370
        if ($userMfa['error'] === true) {
371
            // Add failed authentication log
372
            addFailedAuthentication($username, getClientIpServer());
373
374
            echo prepareExchangedData(
375
                [
376
                    'error' => true,
377
                    'message' => $userMfa['mfaData']['message'],
378
                    'mfaStatus' => $userMfa['mfaData']['mfaStatus'],
379
                ],
380
                'encode'
381
            );
382
            return false;
383
        } elseif ($userMfa['mfaQRCodeInfos'] === true) {
384
            // Add failed authentication log
385
            addFailedAuthentication($username, getClientIpServer());
386
387
            // Case where user has initiated Google Auth
388
            // Return QR code
389
            echo prepareExchangedData(
390
                [
391
                    'value' => $userMfa['mfaData']['value'],
392
                    'user_admin' => isset($sessionAdmin) ? (int) $sessionAdmin : 0,
393
                    'initial_url' => isset($sessionUrl) === true ? $sessionUrl : '',
394
                    'pwd_attempts' => (int) $sessionPwdAttempts,
395
                    'error' => false,
396
                    'message' => $userMfa['mfaData']['message'],
397
                    'mfaStatus' => $userMfa['mfaData']['mfaStatus'],
398
                ],
399
                'encode'
400
            );
401
            return false;
402
        } elseif ($userMfa['duo_url_ready'] === true) {
403
            // Add failed authentication log
404
            addFailedAuthentication($username, getClientIpServer());
405
406
            // Case where user has initiated Duo Auth
407
            // Return the DUO redirect URL
408
            echo prepareExchangedData(
409
                [
410
                    'user_admin' => isset($sessionAdmin) ? (int) $sessionAdmin : 0,
411
                    'initial_url' => isset($sessionUrl) === true ? $sessionUrl : '',
412
                    'pwd_attempts' => (int) $sessionPwdAttempts,
413
                    'error' => false,
414
                    'message' => $userMfa['mfaData']['message'],
415
                    'duo_url_ready' => $userMfa['mfaData']['duo_url_ready'],
416
                    'duo_redirect_url' => $userMfa['mfaData']['duo_redirect_url'],
417
                    'mfaStatus' => $userMfa['mfaData']['mfaStatus'],
418
                ],
419
                'encode'
420
            );
421
            return false;
422
        }
423
    }
424
425
    // Can connect if
426
    // 1- no LDAP mode + user enabled + pw ok
427
    // 2- LDAP mode + user enabled + ldap connection ok + user is not admin
428
    // 3- LDAP mode + user enabled + pw ok + usre is admin
429
    // This in order to allow admin by default to connect even if LDAP is activated
430
    if (canUserGetLog(
431
            $SETTINGS,
432
            (int) $userInfo['disabled'],
433
            $username,
434
            $userLdap['ldapConnection']
435
        ) === true
436
    ) {
437
        $session->set('pwd_attempts', 0);
438
439
        // Check if any unsuccessfull login tries exist
440
        handleLoginAttempts(
441
            $userInfo['id'],
442
            $userInfo['login'],
443
            $userInfo['last_connexion'],
444
            $username,
445
            $SETTINGS,
446
        );
447
448
        // Avoid unlimited session.
449
        $max_time = isset($SETTINGS['maximum_session_expiration_time']) ? (int) $SETTINGS['maximum_session_expiration_time'] : 60;
450
        $session_time = min($dataReceived['duree_session'], $max_time);
451
        $lifetime = time() + ($session_time * 60);
452
453
        // Save old key
454
        $old_key = $session->get('key');
455
456
        // Good practice: reset PHPSESSID and key after successful authentication
457
        $session->migrate();
458
        $session->set('key', generateQuickPassword(30, false));
459
460
        // Save account in SESSION
461
        $session->set('user-login', stripslashes($username));
462
        $session->set('user-name', empty($userInfo['name']) === false ? stripslashes($userInfo['name']) : '');
463
        $session->set('user-lastname', empty($userInfo['lastname']) === false ? stripslashes($userInfo['lastname']) : '');
464
        $session->set('user-id', (int) $userInfo['id']);
465
        $session->set('user-admin', (int) $userInfo['admin']);
466
        $session->set('user-manager', (int) $userInfo['gestionnaire']);
467
        $session->set('user-can_manage_all_users', $userInfo['can_manage_all_users']);
468
        $session->set('user-read_only', $userInfo['read_only']);
469
        $session->set('user-last_pw_change', $userInfo['last_pw_change']);
470
        $session->set('user-last_pw', $userInfo['last_pw']);
471
        $session->set('user-force_relog', $userInfo['force-relog']);
472
        $session->set('user-can_create_root_folder', $userInfo['can_create_root_folder']);
473
        $session->set('user-email', $userInfo['email']);
474
        //$session->set('user-ga', $userInfo['ga']);
475
        $session->set('user-avatar', $userInfo['avatar']);
476
        $session->set('user-avatar_thumb', $userInfo['avatar_thumb']);
477
        $session->set('user-upgrade_needed', $userInfo['upgrade_needed']);
478
        $session->set('user-is_ready_for_usage', $userInfo['is_ready_for_usage']);
479
        $session->set('user-personal_folder_enabled', $userInfo['personal_folder']);
480
        $session->set(
481
            'user-tree_load_strategy',
482
            (isset($userInfo['treeloadstrategy']) === false || empty($userInfo['treeloadstrategy']) === true) ? 'full' : $userInfo['treeloadstrategy']
483
        );
484
        $session->set(
485
            'user-split_view_mode',
486
            (isset($userInfo['split_view_mode']) === false || empty($userInfo['split_view_mode']) === true) ? 0 : (int) $userInfo['split_view_mode']
487
        );
488
        $session->set(
489
            'user-show_subfolders',
490
            (isset($userInfo['show_subfolders']) === false || empty($userInfo['show_subfolders']) === true) ? 0 : (int) $userInfo['show_subfolders']
491
        );
492
        $session->set('user-language', $userInfo['user_language']);
493
        $session->set('user-timezone', $userInfo['usertimezone']);
494
        $session->set('user-keys_recovery_time', $userInfo['keys_recovery_time']);
495
        
496
        // manage session expiration
497
        $session->set('user-session_duration', (int) $lifetime);
498
499
        // User signature keys
500
        $returnKeys = prepareUserEncryptionKeys($userInfo, $passwordClear, $SETTINGS);
501
        $session->set('user-public_key', $returnKeys['public_key']);
502
        
503
        // Did user has his AD password changed
504
        if (null !== $session->get('user-private_key_recovered') && $session->get('user-private_key_recovered') === true) {
505
            $session->set('user-private_key', $session->get('user-private_key'));
506
        } else {
507
            $session->set('user-private_key', $returnKeys['private_key_clear']);
508
        }
509
510
        // Update keys in DB if needed (for transparent recovery migration)
511
        if (!empty($returnKeys['update_keys_in_db'])) {
512
            DB::update(
513
                prefixTable('users'),
514
                $returnKeys['update_keys_in_db'],
515
                'id = %i',
516
                $userInfo['id']
517
            );
518
        }
519
520
        // API key
521
        $session->set(
522
            'user-api_key',
523
            empty($userInfo['api_key']) === false ? base64_decode(decryptUserObjectKey($userInfo['api_key'], $returnKeys['private_key_clear'])) : '',
524
        );
525
        
526
        $session->set('user-special', $userInfo['special']);
527
        $session->set('user-auth_type', $userInfo['auth_type']);
528
529
        // check feedback regarding user password validity
530
        $return = checkUserPasswordValidity(
531
            $userInfo,
532
            (int) $session->get('user-num_days_before_exp'),
533
            (int) $session->get('user-last_pw_change'),
534
            $SETTINGS
535
        );
536
        $session->set('user-validite_pw', $return['validite_pw']);
537
        $session->set('user-last_pw_change', $return['last_pw_change']);
538
        $session->set('user-num_days_before_exp', $return['numDaysBeforePwExpiration']);
539
        $session->set('user-force_relog', $return['user_force_relog']);
540
        
541
        $session->set('user-last_connection', empty($userInfo['last_connexion']) === false ? (int) $userInfo['last_connexion'] : (int) time());
542
        $session->set('user-latest_items', empty($userInfo['latest_items']) === false ? explode(';', $userInfo['latest_items']) : []);
543
        $session->set('user-favorites', empty($userInfo['favourites']) === false ? explode(';', $userInfo['favourites']) : []);
544
        $session->set('user-accessible_folders', empty($userInfo['groupes_visibles']) === false ? explode(';', $userInfo['groupes_visibles']) : []);
545
        $session->set('user-no_access_folders', empty($userInfo['groupes_interdits']) === false ? explode(';', $userInfo['groupes_interdits']) : []);
546
        
547
        // User's roles
548
        if (strpos($userInfo['fonction_id'] !== NULL ? (string) $userInfo['fonction_id'] : '', ',') !== -1) {
549
            // Convert , to ;
550
            $userInfo['fonction_id'] = str_replace(',', ';', (string) $userInfo['fonction_id']);
551
            DB::update(
552
                prefixTable('users'),
553
                [
554
                    'fonction_id' => $userInfo['fonction_id'],
555
                ],
556
                'id = %i',
557
                $session->get('user-id')
558
            );
559
        }
560
        // Append with roles from AD groups
561
        if (is_null($userInfo['roles_from_ad_groups']) === false) {
562
            $userInfo['fonction_id'] = empty($userInfo['fonction_id'])  === true ? $userInfo['roles_from_ad_groups'] : $userInfo['fonction_id']. ';' . $userInfo['roles_from_ad_groups'];
563
        }
564
        // store
565
        $session->set('user-roles', $userInfo['fonction_id']);
566
        $session->set('user-roles_array', array_unique(array_filter(explode(';', $userInfo['fonction_id']))));
567
        
568
        // build array of roles
569
        $session->set('user-pw_complexity', 0);
570
        $session->set('system-array_roles', []);
571
        if (count($session->get('user-roles_array')) > 0) {
572
            $rolesList = DB::query(
573
                'SELECT id, title, complexity
574
                FROM ' . prefixTable('roles_title') . '
575
                WHERE id IN %li',
576
                $session->get('user-roles_array')
577
            );
578
            $excludeUser = isset($SETTINGS['exclude_user']) ? str_contains($session->get('user-login'), $SETTINGS['exclude_user']) : false;
579
            $adjustPermissions = ($session->get('user-id') >= 1000000 && !$excludeUser && (isset($SETTINGS['admin_needle']) || isset($SETTINGS['manager_needle']) || isset($SETTINGS['tp_manager_needle']) || isset($SETTINGS['read_only_needle'])));
580
            if ($adjustPermissions) {
581
                $userInfo['admin'] = $userInfo['gestionnaire'] = $userInfo['can_manage_all_users'] = $userInfo['read_only'] = 0;
582
            }
583
            foreach ($rolesList as $role) {
584
                SessionManager::addRemoveFromSessionAssociativeArray(
585
                    'system-array_roles',
586
                    [
587
                        'id' => $role['id'],
588
                        'title' => $role['title'],
589
                    ],
590
                    'add'
591
                );
592
                
593
                if ($adjustPermissions) {
594
                    if (isset($SETTINGS['admin_needle']) && str_contains($role['title'], $SETTINGS['admin_needle'])) {
595
                        $userInfo['gestionnaire'] = $userInfo['can_manage_all_users'] = $userInfo['read_only'] = 0;
596
                        $userInfo['admin'] = 1;
597
                    }    
598
                    if (isset($SETTINGS['manager_needle']) && str_contains($role['title'], $SETTINGS['manager_needle'])) {
599
                        $userInfo['admin'] = $userInfo['can_manage_all_users'] = $userInfo['read_only'] = 0;
600
                        $userInfo['gestionnaire'] = 1;
601
                    }
602
                    if (isset($SETTINGS['tp_manager_needle']) && str_contains($role['title'], $SETTINGS['tp_manager_needle'])) {
603
                        $userInfo['admin'] = $userInfo['gestionnaire'] = $userInfo['read_only'] = 0;
604
                        $userInfo['can_manage_all_users'] = 1;
605
                    }
606
                    if (isset($SETTINGS['read_only_needle']) && str_contains($role['title'], $SETTINGS['read_only_needle'])) {
607
                        $userInfo['admin'] = $userInfo['gestionnaire'] = $userInfo['can_manage_all_users'] = 0;
608
                        $userInfo['read_only'] = 1;
609
                    }
610
                }
611
612
                // get highest complexity
613
                if ($session->get('user-pw_complexity') < (int) $role['complexity']) {
614
                    $session->set('user-pw_complexity', (int) $role['complexity']);
615
                }
616
            }
617
            if ($adjustPermissions) {
618
                $session->set('user-admin', (int) $userInfo['admin']);
619
                $session->set('user-manager', (int) $userInfo['gestionnaire']);
620
                $session->set('user-can_manage_all_users',(int)  $userInfo['can_manage_all_users']);
621
                $session->set('user-read_only', (int) $userInfo['read_only']);
622
                DB::update(
623
                    prefixTable('users'),
624
                    [
625
                        'admin' => $userInfo['admin'],
626
                        'gestionnaire' => $userInfo['gestionnaire'],
627
                        'can_manage_all_users' => $userInfo['can_manage_all_users'],
628
                        'read_only' => $userInfo['read_only'],
629
                    ],
630
                    'id = %i',
631
                    $session->get('user-id')
632
                );
633
            }
634
        }
635
636
        // Set some settings
637
        $SETTINGS['update_needed'] = '';
638
639
        // Update table
640
        DB::update(
641
            prefixTable('users'),
642
            array_merge(
643
                [
644
                    'key_tempo' => $session->get('key'),
645
                    'last_connexion' => time(),
646
                    'timestamp' => time(),
647
                    'disabled' => 0,
648
                    'session_end' => $session->get('user-session_duration'),
649
                    'user_ip' => $dataReceived['client'],
650
                ],
651
                $returnKeys['update_keys_in_db']
652
            ),
653
            'id=%i',
654
            $userInfo['id']
655
        );
656
        
657
        // Get user's rights
658
        if ($userLdap['user_initial_creation_through_external_ad'] === true || $userOauth2['retExternalAD']['has_been_created'] === 1) {
659
            // is new LDAP user. Show only his personal folder
660
            if ($SETTINGS['enable_pf_feature'] === '1') {
661
                $session->set('user-personal_visible_folders', [$userInfo['id']]);
662
                $session->set('user-personal_folders', [$userInfo['id']]);
663
            } else {
664
                $session->set('user-personal_visible_folders', []);
665
                $session->set('user-personal_folders', []);
666
            }
667
            $session->set('user-roles_array', []);
668
            $session->set('user-read_only_folders', []);
669
            $session->set('user-list_folders_limited', []);
670
            $session->set('system-list_folders_editable_by_role', []);
671
            $session->set('system-list_restricted_folders_for_items', []);
672
            $session->set('user-nb_folders', 1);
673
            $session->set('user-nb_roles', 1);
674
        } else {
675
            identifyUserRights(
676
                $userInfo['groupes_visibles'],
677
                $session->get('user-no_access_folders'),
678
                $userInfo['admin'],
679
                $userInfo['fonction_id'],
680
                $SETTINGS
681
            );
682
        }
683
        // Get some more elements
684
        $session->set('system-screen_height', $dataReceived['screenHeight']);
685
686
        // Get last seen items
687
        $session->set('user-nb_roles', 0);
688
        foreach ($session->get('user-latest_items') as $item) {
689
            if (! empty($item)) {
690
                $dataLastItems = DB::queryFirstRow(
0 ignored issues
show
Unused Code introduced by
The assignment to $dataLastItems is dead and can be removed.
Loading history...
691
                    'SELECT id,label,id_tree
692
                    FROM ' . prefixTable('items') . '
693
                    WHERE id=%i',
694
                    $item
695
                );
696
            }
697
        }
698
699
        // Get cahce tree info
700
        $cacheTreeData = DB::queryFirstRow(
701
            'SELECT visible_folders
702
            FROM ' . prefixTable('cache_tree') . '
703
            WHERE user_id=%i',
704
            (int) $session->get('user-id')
705
        );
706
        if (DB::count() > 0 && empty($cacheTreeData['visible_folders']) === true) {
707
            $session->set('user-cache_tree', '');
708
            // Prepare new task
709
            DB::insert(
710
                prefixTable('background_tasks'),
711
                array(
712
                    'created_at' => time(),
713
                    'process_type' => 'user_build_cache_tree',
714
                    'arguments' => json_encode([
715
                        'user_id' => (int) $session->get('user-id'),
716
                    ], JSON_HEX_QUOT | JSON_HEX_TAG),
717
                    'updated_at' => null,
718
                    'finished_at' => null,
719
                    'output' => null,
720
                )
721
            );
722
        } else {
723
            $session->set('user-cache_tree', $cacheTreeData['visible_folders']);
724
        }
725
726
        // send back the random key
727
        $return = $dataReceived['randomstring'];
728
        // Send email
729
        if (
730
            isKeyExistingAndEqual('enable_send_email_on_user_login', 1, $SETTINGS) === true
731
            && (int) $sessionAdmin !== 1
732
        ) {
733
            // get all Admin users
734
            $val = DB::queryFirstRow('SELECT email FROM ' . prefixTable('users') . " WHERE admin = %i and email != ''", 1);
735
            if (DB::count() > 0) {
736
                // Add email to table
737
                prepareSendingEmail(
738
                    $lang->get('email_subject_on_user_login'),
739
                    str_replace(
740
                        [
741
                            '#tp_user#',
742
                            '#tp_date#',
743
                            '#tp_time#',
744
                        ],
745
                        [
746
                            ' ' . $session->get('user-login') . ' (IP: ' . getClientIpServer() . ')',
747
                            date($SETTINGS['date_format'], (int) $session->get('user-last_connection')),
748
                            date($SETTINGS['time_format'], (int) $session->get('user-last_connection')),
749
                        ],
750
                        $lang->get('email_body_on_user_login')
751
                    ),
752
                    $val['email'],
753
                    $lang->get('administrator')
754
                );
755
            }
756
        }
757
        
758
        // Ensure Complexity levels are translated
759
        defineComplexity();
760
        echo prepareExchangedData(
761
            [
762
                'value' => $return,
763
                'user_id' => $session->get('user-id') !== null ? $session->get('user-id') : '',
764
                'user_admin' => null !== $session->get('user-admin') ? $session->get('user-admin') : 0,
765
                'initial_url' => $antiXss->xss_clean($sessionUrl),
766
                'pwd_attempts' => 0,
767
                'error' => false,
768
                'message' => $session->has('user-upgrade_needed') && (int) $session->get('user-upgrade_needed') && (int) $session->get('user-upgrade_needed') === 1 ? 'ask_for_otc' : '',
769
                'first_connection' => $session->get('user-validite_pw') === 0 ? true : false,
770
                'password_complexity' => TP_PW_COMPLEXITY[$session->get('user-pw_complexity')][1],
771
                'password_change_expected' => $userInfo['special'] === 'password_change_expected' ? true : false,
772
                'private_key_conform' => $session->get('user-id') !== null
773
                    && empty($session->get('user-private_key')) === false
774
                    && $session->get('user-private_key') !== 'none' ? true : false,
775
                'session_key' => $session->get('key'),
776
                'can_create_root_folder' => null !== $session->get('user-can_create_root_folder') ? (int) $session->get('user-can_create_root_folder') : '',
777
                'upgrade_needed' => isset($userInfo['upgrade_needed']) === true ? (int) $userInfo['upgrade_needed'] : 0,
778
                'special' => isset($userInfo['special']) === true ? (int) $userInfo['special'] : 0,
779
                'split_view_mode' => isset($userInfo['split_view_mode']) === true ? (int) $userInfo['split_view_mode'] : 0,
780
                'show_subfolders' => isset($userInfo['show_subfolders']) === true ? (int) $userInfo['show_subfolders'] : 0,
781
                'validite_pw' => $session->get('user-validite_pw') !== null ? $session->get('user-validite_pw') : '',
782
                'num_days_before_exp' => $session->get('user-num_days_before_exp') !== null ? (int) $session->get('user-num_days_before_exp') : '',
783
            ],
784
            'encode',
785
            $old_key
786
        );
787
    
788
        return true;
789
790
    } elseif ((int) $userInfo['disabled'] === 1) {
791
        // User and password is okay but account is locked
792
        echo prepareExchangedData(
793
            [
794
                'value' => $return,
795
                'user_id' => $session->get('user-id') !== null ? (int) $session->get('user-id') : '',
796
                'user_admin' => null !== $session->get('user-admin') ? $session->get('user-admin') : 0,
797
                'initial_url' => isset($sessionUrl) === true ? $sessionUrl : '',
798
                'pwd_attempts' => 0,
799
                'error' => 'user_is_locked',
800
                'message' => $lang->get('account_is_locked'),
801
                'first_connection' => $session->get('user-validite_pw') === 0 ? true : false,
802
                'password_complexity' => TP_PW_COMPLEXITY[$session->get('user-pw_complexity')][1],
803
                'password_change_expected' => $userInfo['special'] === 'password_change_expected' ? true : false,
804
                'private_key_conform' => $session->has('user-private_key') && null !== $session->get('user-private_key')
805
                    && empty($session->get('user-private_key')) === false
806
                    && $session->get('user-private_key') !== 'none' ? true : false,
807
                'session_key' => $session->get('key'),
808
                'can_create_root_folder' => null !== $session->get('user-can_create_root_folder') ? (int) $session->get('user-can_create_root_folder') : '',
809
            ],
810
            'encode'
811
        );
812
        return false;
813
    }
814
815
    echo prepareExchangedData(
816
        [
817
            'value' => $return,
818
            'user_id' => $session->get('user-id') !== null ? (int) $session->get('user-id') : '',
819
            'user_admin' => null !== $session->get('user-admin') ? $session->get('user-admin') : 0,
820
            'initial_url' => isset($sessionUrl) === true ? $sessionUrl : '',
821
            'pwd_attempts' => (int) $sessionPwdAttempts,
822
            'error' => true,
823
            'message' => $lang->get('error_not_allowed_to_authenticate'),
824
            'first_connection' => $session->get('user-validite_pw') === 0 ? true : false,
825
            'password_complexity' => TP_PW_COMPLEXITY[$session->get('user-pw_complexity')][1],
826
            'password_change_expected' => $userInfo['special'] === 'password_change_expected' ? true : false,
827
            'private_key_conform' => $session->get('user-id') !== null
828
                    && empty($session->get('user-private_key')) === false
829
                    && $session->get('user-private_key') !== 'none' ? true : false,
830
            'session_key' => $session->get('key'),
831
            'can_create_root_folder' => null !== $session->get('user-can_create_root_folder') ? (int) $session->get('user-can_create_root_folder') : '',
832
        ],
833
        'encode'
834
    );
835
    return false;
836
}
837
838
/**
839
 * Check if any unsuccessfull login tries exist
840
 *
841
 * @param int       $userInfoId
842
 * @param string    $userInfoLogin
843
 * @param string    $userInfoLastConnection
844
 * @param string    $username
845
 * @param array     $SETTINGS
846
 * @return array
847
 */
848
function handleLoginAttempts(
849
    $userInfoId,
850
    $userInfoLogin,
851
    $userInfoLastConnection,
852
    $username,
853
    $SETTINGS
854
) : array
855
{
856
    $rows = DB::query(
857
        'SELECT date
858
        FROM ' . prefixTable('log_system') . "
859
        WHERE field_1 = %s
860
        AND type = 'failed_auth'
861
        AND label = 'password_is_not_correct'
862
        AND date >= %s AND date < %s",
863
        $userInfoLogin,
864
        $userInfoLastConnection,
865
        time()
866
    );
867
    $arrAttempts = [];
868
    if (DB::count() > 0) {
869
        foreach ($rows as $record) {
870
            array_push(
871
                $arrAttempts,
872
                date($SETTINGS['date_format'] . ' ' . $SETTINGS['time_format'], (int) $record['date'])
873
            );
874
        }
875
    }
876
    
877
878
    // Log into DB the user's connection
879
    if (isKeyExistingAndEqual('log_connections', 1, $SETTINGS) === true) {
880
        logEvents($SETTINGS, 'user_connection', 'connection', (string) $userInfoId, stripslashes($username));
881
    }
882
883
    return [
884
        'attemptsList' => $arrAttempts,
885
        'attemptsCount' => count($rows),
886
    ];
887
}
888
889
890
/**
891
 * Can you user get logged into main page
892
 *
893
 * @param array     $SETTINGS
894
 * @param int       $userInfoDisabled
895
 * @param string    $username
896
 * @param bool      $ldapConnection
897
 *
898
 * @return boolean
899
 */
900
function canUserGetLog(
901
    $SETTINGS,
902
    $userInfoDisabled,
903
    $username,
904
    $ldapConnection
905
) : bool
906
{
907
    include_once $SETTINGS['cpassman_dir'] . '/sources/main.functions.php';
908
909
    if ((int) $userInfoDisabled === 1) {
910
        return false;
911
    }
912
913
    if (isKeyExistingAndEqual('ldap_mode', 0, $SETTINGS) === true) {
914
        return true;
915
    }
916
    
917
    if (isKeyExistingAndEqual('ldap_mode', 1, $SETTINGS) === true 
918
        && (
919
            ($ldapConnection === true && $username !== 'admin')
920
            || $username === 'admin'
921
        )
922
    ) {
923
        return true;
924
    }
925
926
    if (isKeyExistingAndEqual('ldap_and_local_authentication', 1, $SETTINGS) === true
927
        && isset($SETTINGS['ldap_mode']) === true && in_array($SETTINGS['ldap_mode'], ['1', '2']) === true
928
    ) {
929
        return true;
930
    }
931
932
    return false;
933
}
934
935
/**
936
 * 
937
 * Prepare user keys
938
 * 
939
 * @param array $userInfo   User account information
940
 * @param string $passwordClear
941
 *
942
 * @return array
943
 */
944
function prepareUserEncryptionKeys($userInfo, $passwordClear, array $SETTINGS = []) : array
945
{
946
    if (is_null($userInfo['private_key']) === true || empty($userInfo['private_key']) === true || $userInfo['private_key'] === 'none') {
947
        // No keys have been generated yet
948
        // Create them
949
        $userKeys = generateUserKeys($passwordClear, $SETTINGS);
950
951
        $updateData = [
952
            'public_key' => $userKeys['public_key'],
953
            'private_key' => $userKeys['private_key'],
954
        ];
955
956
        // Add transparent recovery data if available
957
        if (isset($userKeys['user_seed'])) {
958
            $updateData['user_derivation_seed'] = $userKeys['user_seed'];
959
            $updateData['private_key_backup'] = $userKeys['private_key_backup'];
960
            $updateData['key_integrity_hash'] = $userKeys['key_integrity_hash'];
961
            $updateData['last_pw_change'] = time();
962
        }
963
964
        return [
965
            'public_key' => $userKeys['public_key'],
966
            'private_key_clear' => $userKeys['private_key_clear'],
967
            'update_keys_in_db' => $updateData,
968
        ];
969
    }
970
971
    if ($userInfo['special'] === 'generate-keys') {
972
        return [
973
            'public_key' => $userInfo['public_key'],
974
            'private_key_clear' => '',
975
            'update_keys_in_db' => [],
976
        ];
977
    }
978
979
    // Don't perform this in case of special login action
980
    if ($userInfo['special'] === 'otc_is_required_on_next_login' || $userInfo['special'] === 'user_added_from_ad') {
981
        return [
982
            'public_key' => $userInfo['public_key'],
983
            'private_key_clear' => '',
984
            'update_keys_in_db' => [],
985
        ];
986
    }
987
988
    // Try to uncrypt private key with current password
989
    try {
990
        $privateKeyClear = decryptPrivateKey($passwordClear, $userInfo['private_key']);
991
        
992
        // If user has seed but no backup, create it on first successful login
993
        if (!empty($userInfo['user_derivation_seed']) && empty($userInfo['private_key_backup'])) {
994
            if (defined('LOG_TO_SERVER') && LOG_TO_SERVER === true) {
995
                error_log('TEAMPASS Transparent Recovery - Creating backup for user ' . ($userInfo['login'] ?? 'unknown'));
996
            }
997
998
            $derivedKey = deriveBackupKey($userInfo['user_derivation_seed'], $userInfo['public_key'], $SETTINGS);
999
            $cipher = new Crypt_AES();
1000
            $cipher->setPassword($derivedKey);
1001
            $privateKeyBackup = base64_encode($cipher->encrypt(base64_decode($privateKeyClear)));
1002
1003
            // Generate integrity hash
1004
            $integrityHash = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $integrityHash is dead and can be removed.
Loading history...
1005
            $serverSecret = getServerSecret();
1006
            $integrityHash = generateKeyIntegrityHash($userInfo['user_derivation_seed'], $userInfo['public_key'], $serverSecret);
1007
1008
            return [
1009
                'public_key' => $userInfo['public_key'],
1010
                'private_key_clear' => $privateKeyClear,
1011
                'update_keys_in_db' => [
1012
                    'private_key_backup' => $privateKeyBackup,
1013
                    'key_integrity_hash' => $integrityHash,
1014
                    'last_pw_change' => time(),
1015
                ],
1016
            ];
1017
        }
1018
1019
        return [
1020
            'public_key' => $userInfo['public_key'],
1021
            'private_key_clear' => $privateKeyClear,
1022
            'update_keys_in_db' => [],
1023
        ];
1024
1025
    } catch (Exception $e) {
1026
        // Decryption failed - try transparent recovery
1027
        if (!empty($userInfo['user_derivation_seed'])
1028
            && !empty($userInfo['private_key_backup'])) {
1029
1030
            $recovery = attemptTransparentRecovery($userInfo, $passwordClear, $SETTINGS);
1031
1032
            if ($recovery['success']) {
1033
                return [
1034
                    'public_key' => $userInfo['public_key'],
1035
                    'private_key_clear' => $recovery['private_key_clear'],
1036
                    'update_keys_in_db' => [],
1037
                ];
1038
            }
1039
        }
1040
1041
        // Recovery failed or not available - return empty
1042
        return [
1043
            'public_key' => $userInfo['public_key'],
1044
            'private_key_clear' => '',
1045
            'update_keys_in_db' => [],
1046
        ];
1047
    }
1048
}
1049
1050
1051
/**
1052
 * CHECK PASSWORD VALIDITY
1053
 * Don't take into consideration if LDAP in use
1054
 * 
1055
 * @param array $userInfo User account information
1056
 * @param int $numDaysBeforePwExpiration Number of days before password expiration
1057
 * @param int $lastPwChange Last password change
1058
 * @param array $SETTINGS Teampass settings
1059
 *
1060
 * @return array
1061
 */
1062
function checkUserPasswordValidity(array $userInfo, int $numDaysBeforePwExpiration, int $lastPwChange, array $SETTINGS)
1063
{
1064
    if (isKeyExistingAndEqual('ldap_mode', 1, $SETTINGS) === true && $userInfo['auth_type'] !== 'local') {
1065
        return [
1066
            'validite_pw' => true,
1067
            'last_pw_change' => $userInfo['last_pw_change'],
1068
            'user_force_relog' => '',
1069
            'numDaysBeforePwExpiration' => '',
1070
        ];
1071
    }
1072
    
1073
    if (isset($userInfo['last_pw_change']) === true) {
1074
        if ((int) $SETTINGS['pw_life_duration'] === 0) {
1075
            return [
1076
                'validite_pw' => true,
1077
                'last_pw_change' => '',
1078
                'user_force_relog' => 'infinite',
1079
                'numDaysBeforePwExpiration' => '',
1080
            ];
1081
        } elseif ((int) $SETTINGS['pw_life_duration'] > 0) {
1082
            $numDaysBeforePwExpiration = (int) $SETTINGS['pw_life_duration'] - round(
1083
                (mktime(0, 0, 0, (int) date('m'), (int) date('d'), (int) date('y')) - $userInfo['last_pw_change']) / (24 * 60 * 60)
1084
            );
1085
            return [
1086
                'validite_pw' => $numDaysBeforePwExpiration <= 0 ? false : true,
1087
                'last_pw_change' => $userInfo['last_pw_change'],
1088
                'user_force_relog' => 'infinite',
1089
                'numDaysBeforePwExpiration' => (int) $numDaysBeforePwExpiration,
1090
            ];
1091
        } else {
1092
            return [
1093
                'validite_pw' => false,
1094
                'last_pw_change' => '',
1095
                'user_force_relog' => '',
1096
                'numDaysBeforePwExpiration' => '',
1097
            ];
1098
        }
1099
    } else {
1100
        return [
1101
            'validite_pw' => false,
1102
            'last_pw_change' => '',
1103
            'user_force_relog' => '',
1104
            'numDaysBeforePwExpiration' => '',
1105
        ];
1106
    }
1107
}
1108
1109
1110
/**
1111
 * Authenticate a user through AD/LDAP.
1112
 *
1113
 * @param string $username      Username
1114
 * @param array $userInfo       User account information
1115
 * @param string $passwordClear Password
1116
 * @param array $SETTINGS       Teampass settings
1117
 *
1118
 * @return array
1119
 */
1120
function authenticateThroughAD(string $username, array $userInfo, string $passwordClear, array $SETTINGS): array
1121
{
1122
    $session = SessionManager::getSession();
1123
    $lang = new Language($session->get('user-language') ?? 'english');
1124
    
1125
    try {
1126
        // Get LDAP connection and handler
1127
        $ldapHandler = initializeLdapConnection($SETTINGS);
1128
        
1129
        // Authenticate user
1130
        $authResult = authenticateUser($username, $passwordClear, $ldapHandler, $SETTINGS, $lang);
1131
        if ($authResult['error']) {
1132
            return $authResult;
1133
        }
1134
        
1135
        $userADInfos = $authResult['user_info'];
1136
        
1137
        // Verify account expiration
1138
        if (isAccountExpired($userADInfos)) {
1139
            return [
1140
                'error' => true,
1141
                'message' => $lang->get('error_ad_user_expired'),
1142
            ];
1143
        }
1144
        
1145
        // Handle user creation if needed
1146
        if ($userInfo['ldap_user_to_be_created']) {
1147
            $userInfo = handleNewUser($username, $passwordClear, $userADInfos, $userInfo, $SETTINGS, $lang);
1148
        }
1149
        
1150
        // Get and handle user groups
1151
        $userGroupsData = getUserGroups($userADInfos, $ldapHandler, $SETTINGS);
1152
        handleUserADGroups($username, $userInfo, $userGroupsData['userGroups'], $SETTINGS);
1153
        
1154
        // Finalize authentication
1155
        finalizeAuthentication($userInfo, $passwordClear, $SETTINGS);
1156
        
1157
        return [
1158
            'error' => false,
1159
            'message' => '',
1160
            'user_info' => $userInfo,
1161
        ];
1162
        
1163
    } catch (Exception $e) {
1164
        return [
1165
            'error' => true,
1166
            'message' => "Error: " . $e->getMessage(),
1167
        ];
1168
    }
1169
}
1170
1171
/**
1172
 * Initialize LDAP connection based on type
1173
 * 
1174
 * @param array $SETTINGS Teampass settings
1175
 * @return array Contains connection and type-specific handler
1176
 * @throws Exception
1177
 */
1178
function initializeLdapConnection(array $SETTINGS): array
1179
{
1180
    $ldapExtra = new LdapExtra($SETTINGS);
1181
    $ldapConnection = $ldapExtra->establishLdapConnection();
1182
    
1183
    switch ($SETTINGS['ldap_type']) {
1184
        case 'ActiveDirectory':
1185
            return [
1186
                'connection' => $ldapConnection,
1187
                'handler' => new ActiveDirectoryExtra(),
1188
                'type' => 'ActiveDirectory'
1189
            ];
1190
        case 'OpenLDAP':
1191
            return [
1192
                'connection' => $ldapConnection,
1193
                'handler' => new OpenLdapExtra(),
1194
                'type' => 'OpenLDAP'
1195
            ];
1196
        default:
1197
            throw new Exception("Unsupported LDAP type: " . $SETTINGS['ldap_type']);
1198
    }
1199
}
1200
1201
/**
1202
 * Authenticate user against LDAP
1203
 * 
1204
 * @param string $username Username
1205
 * @param string $passwordClear Password
1206
 * @param array $ldapHandler LDAP connection and handler
1207
 * @param array $SETTINGS Teampass settings
1208
 * @param Language $lang Language instance
1209
 * @return array Authentication result
1210
 */
1211
function authenticateUser(string $username, string $passwordClear, array $ldapHandler, array $SETTINGS, Language $lang): array
1212
{
1213
    try {
1214
        $userAttribute = $SETTINGS['ldap_user_attribute'] ?? 'samaccountname';
1215
        $userADInfos = $ldapHandler['connection']->query()
1216
            ->where($userAttribute, '=', $username)
1217
            ->firstOrFail();
1218
        
1219
        // Verify user status for ActiveDirectory
1220
        if ($ldapHandler['type'] === 'ActiveDirectory' && !$ldapHandler['handler']->userIsEnabled((string) $userADInfos['dn'], $ldapHandler['connection'])) {
1221
            return [
1222
                'error' => true,
1223
                'message' => "Error: User is not enabled"
1224
            ];
1225
        }
1226
        
1227
        // Attempt authentication
1228
        $authIdentifier = $ldapHandler['type'] === 'ActiveDirectory' 
1229
            ? $userADInfos['userprincipalname'][0] 
1230
            : $userADInfos['dn'];
1231
            
1232
        if (!$ldapHandler['connection']->auth()->attempt($authIdentifier, $passwordClear)) {
1233
            return [
1234
                'error' => true,
1235
                'message' => "Error: User is not authenticated"
1236
            ];
1237
        }
1238
        
1239
        return [
1240
            'error' => false,
1241
            'user_info' => $userADInfos
1242
        ];
1243
        
1244
    } catch (\LdapRecord\Query\ObjectNotFoundException $e) {
1245
        return [
1246
            'error' => true,
1247
            'message' => $lang->get('error_bad_credentials')
1248
        ];
1249
    }
1250
}
1251
1252
/**
1253
 * Check if user account is expired
1254
 * 
1255
 * @param array $userADInfos User AD information
1256
 * @return bool
1257
 */
1258
function isAccountExpired(array $userADInfos): bool
1259
{
1260
    return (isset($userADInfos['shadowexpire'][0]) && (int) $userADInfos['shadowexpire'][0] === 1)
1261
        || (isset($userADInfos['accountexpires'][0]) 
1262
            && (int) $userADInfos['accountexpires'][0] < time() 
1263
            && (int) $userADInfos['accountexpires'][0] !== 0);
1264
}
1265
1266
/**
1267
 * Handle creation of new user
1268
 * 
1269
 * @param string $username Username
1270
 * @param string $passwordClear Password
1271
 * @param array $userADInfos User AD information
1272
 * @param array $userInfo User information
1273
 * @param array $SETTINGS Teampass settings
1274
 * @param Language $lang Language instance
1275
 * @return array User information
1276
 */
1277
function handleNewUser(string $username, string $passwordClear, array $userADInfos, array $userInfo, array $SETTINGS, Language $lang): array
1278
{
1279
    $userInfo = externalAdCreateUser(
1280
        $username,
1281
        $passwordClear,
1282
        $userADInfos['mail'][0],
1283
        $userADInfos['givenname'][0],
1284
        $userADInfos['sn'][0],
1285
        'ldap',
1286
        [],
1287
        $SETTINGS
1288
    );
1289
1290
    handleUserKeys(
1291
        (int) $userInfo['id'],
1292
        $passwordClear,
1293
        (int) ($SETTINGS['maximum_number_of_items_to_treat'] ?? NUMBER_ITEMS_IN_BATCH),
1294
        uniqidReal(20),
1295
        true,
1296
        true,
1297
        true,
1298
        false,
1299
        $lang->get('email_body_user_config_2')
1300
    );
1301
1302
    $userInfo['has_been_created'] = 1;
1303
    return $userInfo;
1304
}
1305
1306
/**
1307
 * Get user groups based on LDAP type
1308
 * 
1309
 * @param array $userADInfos User AD information
1310
 * @param array $ldapHandler LDAP connection and handler
1311
 * @param array $SETTINGS Teampass settings
1312
 * @return array User groups
1313
 */
1314
function getUserGroups(array $userADInfos, array $ldapHandler, array $SETTINGS): array
1315
{
1316
    $dnAttribute = $SETTINGS['ldap_user_dn_attribute'] ?? 'distinguishedname';
1317
    
1318
    if ($ldapHandler['type'] === 'ActiveDirectory') {
1319
        return $ldapHandler['handler']->getUserADGroups(
1320
            $userADInfos[$dnAttribute][0],
1321
            $ldapHandler['connection'],
1322
            $SETTINGS
1323
        );
1324
    }
1325
    
1326
    if ($ldapHandler['type'] === 'OpenLDAP') {
1327
        return $ldapHandler['handler']->getUserADGroups(
1328
            $userADInfos['dn'],
1329
            $ldapHandler['connection'],
1330
            $SETTINGS
1331
        );
1332
    }
1333
    
1334
    throw new Exception("Unsupported LDAP type: " . $ldapHandler['type']);
1335
}
1336
1337
/**
1338
 * Permits to update the user's AD groups with mapping roles
1339
 *
1340
 * @param string $username
1341
 * @param array $userInfo
1342
 * @param array $groups
1343
 * @param array $SETTINGS
1344
 * @return void
1345
 */
1346
function handleUserADGroups(string $username, array $userInfo, array $groups, array $SETTINGS): void
1347
{
1348
    if (isset($SETTINGS['enable_ad_users_with_ad_groups']) === true && (int) $SETTINGS['enable_ad_users_with_ad_groups'] === 1) {
1349
        // Get user groups from AD
1350
        $user_ad_groups = [];
1351
        foreach($groups as $group) {
1352
            //print_r($group);
1353
            // get relation role id for AD group
1354
            $role = DB::queryFirstRow(
1355
                'SELECT lgr.role_id
1356
                FROM ' . prefixTable('ldap_groups_roles') . ' AS lgr
1357
                WHERE lgr.ldap_group_id = %s',
1358
                $group
1359
            );
1360
            if (DB::count() > 0) {
1361
                array_push($user_ad_groups, $role['role_id']); 
1362
            }
1363
        }
1364
        
1365
        // save
1366
        if (count($user_ad_groups) > 0) {
1367
            $user_ad_groups = implode(';', $user_ad_groups);
1368
            DB::update(
1369
                prefixTable('users'),
1370
                [
1371
                    'roles_from_ad_groups' => $user_ad_groups,
1372
                ],
1373
                'id = %i',
1374
                $userInfo['id']
1375
            );
1376
1377
            $userInfo['roles_from_ad_groups'] = $user_ad_groups;
1378
        } else {
1379
            DB::update(
1380
                prefixTable('users'),
1381
                [
1382
                    'roles_from_ad_groups' => null,
1383
                ],
1384
                'id = %i',
1385
                $userInfo['id']
1386
            );
1387
1388
            $userInfo['roles_from_ad_groups'] = [];
1389
        }
1390
    } else {
1391
        // Delete all user's AD groups
1392
        DB::update(
1393
            prefixTable('users'),
1394
            [
1395
                'roles_from_ad_groups' => null,
1396
            ],
1397
            'id = %i',
1398
            $userInfo['id']
1399
        );
1400
    }
1401
}
1402
1403
/**
1404
 * Permits to finalize the authentication process.
1405
 *
1406
 * @param array $userInfo
1407
 * @param string $passwordClear
1408
 * @param array $SETTINGS
1409
 */
1410
function finalizeAuthentication(
1411
    array $userInfo,
1412
    string $passwordClear,
1413
    array $SETTINGS
1414
): void
1415
{
1416
    $passwordManager = new PasswordManager();
1417
    
1418
    // Migrate password if needed
1419
    $result  = $passwordManager->migratePassword(
1420
        $userInfo['pw'],
1421
        $passwordClear,
1422
        (int) $userInfo['id']
1423
    );
1424
1425
    // Use the new hashed password if migration was successful
1426
    $hashedPassword = $result['hashedPassword'];
1427
    
1428
    if (empty($userInfo['pw']) === true || $userInfo['special'] === 'user_added_from_ad') {
1429
        // 2 cases are managed here:
1430
        // Case where user has never been connected then erase current pwd with the ldap's one
1431
        // Case where user has been added from LDAP and never being connected to TP
1432
        DB::update(
1433
            prefixTable('users'),
1434
            [
1435
                'pw' => $passwordManager->hashPassword($passwordClear),
1436
            ],
1437
            'id = %i',
1438
            $userInfo['id']
1439
        );
1440
    } elseif ($passwordManager->verifyPassword($hashedPassword, $passwordClear) === false) {
1441
        // Case where user is auth by LDAP but his password in Teampass is not synchronized
1442
        // For example when user has changed his password in AD.
1443
        // So we need to update it in Teampass and handle private key re-encryption
1444
        DB::update(
1445
            prefixTable('users'),
1446
            [
1447
                'pw' => $passwordManager->hashPassword($passwordClear),
1448
            ],
1449
            'id = %i',
1450
            $userInfo['id']
1451
        );
1452
1453
        // Try transparent recovery for automatic re-encryption
1454
        handleExternalPasswordChange((int) $userInfo['id'], $passwordClear, $userInfo, $SETTINGS);
1455
    }
1456
}
1457
1458
/**
1459
 * Undocumented function.
1460
 *
1461
 * @param string $username      User name
1462
 * @param string $passwordClear User password in clear
1463
 * @param array $retLDAP       Received data from LDAP
1464
 * @param array $SETTINGS      Teampass settings
1465
 *
1466
 * @return array
1467
 */
1468
function externalAdCreateUser(
1469
    string $login,
1470
    string $passwordClear,
1471
    string $userEmail,
1472
    string $userName,
1473
    string $userLastname,
1474
    string $authType,
1475
    array $userGroups,
1476
    array $SETTINGS
1477
): array
1478
{
1479
    // Generate user keys pair with transparent recovery support
1480
    $userKeys = generateUserKeys($passwordClear, $SETTINGS);
1481
1482
    // Create password hash
1483
    $passwordManager = new PasswordManager();
1484
    $hashedPassword = $passwordManager->hashPassword($passwordClear);
1485
    
1486
    // If any groups provided, add user to them
1487
    if (count($userGroups) > 0) {
1488
        $groupIds = [];
1489
        foreach ($userGroups as $group) {
1490
            // Check if exists in DB
1491
            $groupData = DB::queryFirstRow(
1492
                'SELECT id
1493
                FROM ' . prefixTable('roles_title') . '
1494
                WHERE title = %s',
1495
                $group["displayName"]
1496
            );
1497
1498
            if (DB::count() > 0) {
1499
                array_push($groupIds, $groupData['id']);
1500
            }
1501
        }
1502
        $userGroups = implode(';', $groupIds);
1503
    } else {
1504
        $userGroups = '';
1505
    }
1506
    
1507
    if (empty($userGroups) && !empty($SETTINGS['oauth_selfregistered_user_belongs_to_role'])) {
1508
        $userGroups = $SETTINGS['oauth_selfregistered_user_belongs_to_role'];
1509
    }
1510
1511
    // Prepare user data
1512
    $userData = [
1513
        'login' => (string) $login,
1514
        'pw' => (string) $hashedPassword,
1515
        'email' => (string) $userEmail,
1516
        'name' => (string) $userName,
1517
        'lastname' => (string) $userLastname,
1518
        'admin' => '0',
1519
        'gestionnaire' => '0',
1520
        'can_manage_all_users' => '0',
1521
        'personal_folder' => $SETTINGS['enable_pf_feature'] === '1' ? '1' : '0',
1522
        'groupes_interdits' => '',
1523
        'groupes_visibles' => '',
1524
        'fonction_id' => $userGroups,
1525
        'last_pw_change' => (int) time(),
1526
        'user_language' => (string) $SETTINGS['default_language'],
1527
        'encrypted_psk' => '',
1528
        'isAdministratedByRole' => $authType === 'ldap' ?
1529
            (isset($SETTINGS['ldap_new_user_is_administrated_by']) === true && empty($SETTINGS['ldap_new_user_is_administrated_by']) === false ? $SETTINGS['ldap_new_user_is_administrated_by'] : 0)
1530
            : (
1531
                $authType === 'oauth2' ?
1532
                (isset($SETTINGS['oauth_new_user_is_administrated_by']) === true && empty($SETTINGS['oauth_new_user_is_administrated_by']) === false ? $SETTINGS['oauth_new_user_is_administrated_by'] : 0)
1533
                : 0
1534
            ),
1535
        'public_key' => $userKeys['public_key'],
1536
        'private_key' => $userKeys['private_key'],
1537
        'special' => 'none',
1538
        'auth_type' => $authType,
1539
        'otp_provided' => '1',
1540
        'is_ready_for_usage' => '0',
1541
        'created_at' => time(),
1542
    ];
1543
1544
    // Add transparent recovery fields if available
1545
    if (isset($userKeys['user_seed'])) {
1546
        $userData['user_derivation_seed'] = $userKeys['user_seed'];
1547
        $userData['private_key_backup'] = $userKeys['private_key_backup'];
1548
        $userData['key_integrity_hash'] = $userKeys['key_integrity_hash'];
1549
        $userData['last_pw_change'] = time();
1550
    }
1551
1552
    // Insert user in DB
1553
    DB::insert(prefixTable('users'), $userData);
1554
    $newUserId = DB::insertId();
1555
1556
    // Create the API key
1557
    DB::insert(
1558
        prefixTable('api'),
1559
        array(
1560
            'type' => 'user',
1561
            'user_id' => $newUserId,
1562
            'value' => encryptUserObjectKey(base64_encode(base64_encode(uniqidReal(39))), $userKeys['public_key']),
1563
            'timestamp' => time(),
1564
            'allowed_to_read' => 1,
1565
            'allowed_folders' => '',
1566
            'enabled' => 0,
1567
        )
1568
    );
1569
1570
    // Create personnal folder
1571
    if (isKeyExistingAndEqual('enable_pf_feature', 1, $SETTINGS) === true) {
1572
        DB::insert(
1573
            prefixTable('nested_tree'),
1574
            [
1575
                'parent_id' => '0',
1576
                'title' => $newUserId,
1577
                'bloquer_creation' => '0',
1578
                'bloquer_modification' => '0',
1579
                'personal_folder' => '1',
1580
                'categories' => '',
1581
            ]
1582
        );
1583
        // Rebuild tree
1584
        $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
1585
        $tree->rebuild();
1586
    }
1587
1588
1589
    return [
1590
        'error' => false,
1591
        'message' => '',
1592
        'proceedIdentification' => true,
1593
        'user_initial_creation_through_external_ad' => true,
1594
        'id' => $newUserId,
1595
        'oauth2_login_ongoing' => true,
1596
    ];
1597
}
1598
1599
/**
1600
 * Undocumented function.
1601
 *
1602
 * @param string                $username     Username
1603
 * @param array                 $userInfo     Result of query
1604
 * @param string|array|resource $dataReceived DataReceived
1605
 * @param array                 $SETTINGS     Teampass settings
1606
 *
1607
 * @return array
1608
 */
1609
function googleMFACheck(string $username, array $userInfo, $dataReceived, array $SETTINGS): array
1610
{
1611
    $session = SessionManager::getSession();    
1612
    $lang = new Language($session->get('user-language') ?? 'english');
1613
1614
    if (
1615
        isset($dataReceived['GACode']) === true
1616
        && empty($dataReceived['GACode']) === false
1617
    ) {
1618
        $sessionAdmin = $session->get('user-admin');
1619
        $sessionUrl = $session->get('user-initial_url');
1620
        $sessionPwdAttempts = $session->get('pwd_attempts');
1621
        // create new instance
1622
        $tfa = new TwoFactorAuth($SETTINGS['ga_website_name']);
1623
        // Init
1624
        $firstTime = [];
1625
        // now check if it is the 1st time the user is using 2FA
1626
        if ($userInfo['ga_temporary_code'] !== 'none' && $userInfo['ga_temporary_code'] !== 'done') {
1627
            if ($userInfo['ga_temporary_code'] !== $dataReceived['GACode']) {
1628
                return [
1629
                    'error' => true,
1630
                    'message' => $lang->get('ga_bad_code'),
1631
                    'proceedIdentification' => false,
1632
                    'ga_bad_code' => true,
1633
                    'firstTime' => $firstTime,
1634
                ];
1635
            }
1636
1637
            // If first time with MFA code
1638
            $proceedIdentification = false;
1639
            
1640
            // generate new QR
1641
            $new_2fa_qr = $tfa->getQRCodeImageAsDataUri(
1642
                'Teampass - ' . $username,
1643
                $userInfo['ga']
1644
            );
1645
            // clear temporary code from DB
1646
            DB::update(
1647
                prefixTable('users'),
1648
                [
1649
                    'ga_temporary_code' => 'done',
1650
                ],
1651
                'id=%i',
1652
                $userInfo['id']
1653
            );
1654
            $firstTime = [
1655
                'value' => '<img src="' . $new_2fa_qr . '">',
1656
                'user_admin' => isset($sessionAdmin) ? (int) $sessionAdmin : '',
1657
                'initial_url' => isset($sessionUrl) === true ? $sessionUrl : '',
1658
                'pwd_attempts' => (int) $sessionPwdAttempts,
1659
                'message' => $lang->get('ga_flash_qr_and_login'),
1660
                'mfaStatus' => 'ga_temporary_code_correct',
1661
            ];
1662
        } else {
1663
            // verify the user GA code
1664
            if ($tfa->verifyCode($userInfo['ga'], $dataReceived['GACode'])) {
1665
                $proceedIdentification = true;
1666
            } else {
1667
                return [
1668
                    'error' => true,
1669
                    'message' => $lang->get('ga_bad_code'),
1670
                    'proceedIdentification' => false,
1671
                    'ga_bad_code' => true,
1672
                    'firstTime' => $firstTime,
1673
                ];
1674
            }
1675
        }
1676
    } else {
1677
        return [
1678
            'error' => true,
1679
            'message' => $lang->get('ga_bad_code'),
1680
            'proceedIdentification' => false,
1681
            'ga_bad_code' => true,
1682
            'firstTime' => [],
1683
        ];
1684
    }
1685
1686
    return [
1687
        'error' => false,
1688
        'message' => '',
1689
        'proceedIdentification' => $proceedIdentification,
1690
        'firstTime' => $firstTime,
1691
    ];
1692
}
1693
1694
1695
/**
1696
 * Perform DUO checks
1697
 *
1698
 * @param string $username
1699
 * @param string|array|resource $dataReceived
1700
 * @param array $SETTINGS
1701
 * @return array
1702
 */
1703
function duoMFACheck(
1704
    string $username,
1705
    $dataReceived,
1706
    array $SETTINGS
1707
): array
1708
{
1709
    $session = SessionManager::getSession();
1710
    $lang = new Language($session->get('user-language') ?? 'english');
1711
1712
    $sessionPwdAttempts = $session->get('pwd_attempts');
1713
    $saved_state = null !== $session->get('user-duo_state') ? $session->get('user-duo_state') : '';
1714
    $duo_status = null !== $session->get('user-duo_status') ? $session->get('user-duo_status') : '';
1715
1716
    // Ensure state and login are set
1717
    if (
1718
        (empty($saved_state) || empty($dataReceived['login']) || !isset($dataReceived['duo_state']) || empty($dataReceived['duo_state']))
1719
        && $duo_status === 'IN_PROGRESS'
1720
        && $dataReceived['duo_status'] !== 'start_duo_auth'
1721
    ) {
1722
        return [
1723
            'error' => true,
1724
            'message' => $lang->get('duo_no_data'),
1725
            'pwd_attempts' => (int) $sessionPwdAttempts,
1726
            'proceedIdentification' => false,
1727
        ];
1728
    }
1729
1730
    // Ensure state matches from initial request
1731
    if ($duo_status === 'IN_PROGRESS' && $dataReceived['duo_state'] !== $saved_state) {
1732
        $session->set('user-duo_state', '');
1733
        $session->set('user-duo_status', '');
1734
1735
        // We did not received a proper Duo state
1736
        return [
1737
            'error' => true,
1738
            'message' => $lang->get('duo_error_state'),
1739
            'pwd_attempts' => (int) $sessionPwdAttempts,
1740
            'proceedIdentification' => false,
1741
        ];
1742
    }
1743
1744
    return [
1745
        'error' => false,
1746
        'pwd_attempts' => (int) $sessionPwdAttempts,
1747
        'saved_state' => $saved_state,
1748
        'duo_status' => $duo_status,
1749
    ];
1750
}
1751
1752
1753
/**
1754
 * Create the redirect URL or check if the DUO Universal prompt was completed successfully.
1755
 *
1756
 * @param string                $username               Username
1757
 * @param string|array|resource $dataReceived           DataReceived
1758
 * @param array                 $sessionPwdAttempts     Nb of pwd attempts
1759
 * @param array                 $saved_state            Saved state
1760
 * @param array                 $duo_status             Duo status
1761
 * @param array                 $SETTINGS               Teampass settings
1762
 *
1763
 * @return array
1764
 */
1765
function duoMFAPerform(
1766
    string $username,
1767
    $dataReceived,
1768
    int $sessionPwdAttempts,
1769
    string $saved_state,
1770
    string $duo_status,
1771
    array $SETTINGS
1772
): array
1773
{
1774
    $session = SessionManager::getSession();
1775
    $lang = new Language($session->get('user-language') ?? 'english');
1776
1777
    try {
1778
        $duo_client = new Client(
1779
            $SETTINGS['duo_ikey'],
1780
            $SETTINGS['duo_skey'],
1781
            $SETTINGS['duo_host'],
1782
            $SETTINGS['cpassman_url'].'/'.DUO_CALLBACK
1783
        );
1784
    } catch (DuoException $e) {
1785
        return [
1786
            'error' => true,
1787
            'message' => $lang->get('duo_config_error'),
1788
            'debug_message' => $e->getMessage(),
1789
            'pwd_attempts' => (int) $sessionPwdAttempts,
1790
            'proceedIdentification' => false,
1791
        ];
1792
    }
1793
        
1794
    try {
1795
        $duo_error = $lang->get('duo_error_secure');
1796
        $duo_failmode = "none";
1797
        $duo_client->healthCheck();
1798
    } catch (DuoException $e) {
1799
        //Not implemented Duo Failmode in case the Duo services are not available
1800
        /*if ($SETTINGS['duo_failmode'] == "safe") {
1801
            # If we're failing open, errors in 2FA still allow for success
1802
            $duo_error = $lang->get('duo_error_failopen');
1803
            $duo_failmode = "safe";
1804
        } else {
1805
            # Duo has failed and is unavailable, redirect user to the login page
1806
            $duo_error = $lang->get('duo_error_secure');
1807
            $duo_failmode = "secure";
1808
        }*/
1809
        return [
1810
            'error' => true,
1811
            'message' => $duo_error . $lang->get('duo_error_check_config'),
1812
            'pwd_attempts' => (int) $sessionPwdAttempts,
1813
            'debug_message' => $e->getMessage(),
1814
            'proceedIdentification' => false,
1815
        ];
1816
    }
1817
    
1818
    // Check if no one played with the javascript
1819
    if ($duo_status !== 'IN_PROGRESS' && $dataReceived['duo_status'] === 'start_duo_auth') {
1820
        # Create the Duo URL to send the user to
1821
        try {
1822
            $duo_state = $duo_client->generateState();
1823
            $duo_redirect_url = $duo_client->createAuthUrl($username, $duo_state);
1824
        } catch (DuoException $e) {
1825
            return [
1826
                'error' => true,
1827
                'message' => $duo_error . $lang->get('duo_error_url'),
1828
                'pwd_attempts' => (int) $sessionPwdAttempts,
1829
                'debug_message' => $e->getMessage(),
1830
                'proceedIdentification' => false,
1831
            ];
1832
        }
1833
        
1834
        // Somethimes Duo return success but fail to return a URL, double check if the URL has been created
1835
        if (!empty($duo_redirect_url) && filter_var($duo_redirect_url,FILTER_SANITIZE_URL)) {
1836
            // Since Duo Universal requires a redirect, let's store some info when the user get's back after completing the Duo prompt
1837
            $key = hash('sha256', $duo_state);
1838
            $iv = substr(hash('sha256', $duo_state), 0, 16);
1839
            $duo_data = serialize([
1840
                'duo_login' => $username,
1841
                'duo_pwd' => $dataReceived['pw'],
1842
            ]);
1843
            $duo_data_enc = openssl_encrypt($duo_data, 'AES-256-CBC', $key, 0, $iv);
1844
            $session->set('user-duo_state', $duo_state);
1845
            $session->set('user-duo_data', base64_encode($duo_data_enc));
1846
            $session->set('user-duo_status', 'IN_PROGRESS');
1847
            $session->set('user-login', $username);
1848
            
1849
            // If we got here we can reset the password attempts
1850
            $session->set('pwd_attempts', 0);
1851
            
1852
            return [
1853
                'error' => false,
1854
                'message' => '',
1855
                'proceedIdentification' => false,
1856
                'duo_url_ready' => true,
1857
                'duo_redirect_url' => $duo_redirect_url,
1858
                'duo_failmode' => $duo_failmode,
1859
            ];
1860
        } else {
1861
            return [
1862
                'error' => true,
1863
                'message' => $duo_error . $lang->get('duo_error_url'),
1864
                'pwd_attempts' => (int) $sessionPwdAttempts,
1865
                'proceedIdentification' => false,
1866
            ];
1867
        }
1868
    } elseif ($duo_status === 'IN_PROGRESS' && $dataReceived['duo_code'] !== '') {
1869
        try {
1870
            // Check if the Duo code received is valid
1871
            $decoded_token = $duo_client->exchangeAuthorizationCodeFor2FAResult($dataReceived['duo_code'], $username);
1872
        } catch (DuoException $e) {
1873
            return [
1874
                'error' => true,
1875
                'message' => $lang->get('duo_error_decoding'),
1876
                'pwd_attempts' => (int) $sessionPwdAttempts,
1877
                'debug_message' => $e->getMessage(),
1878
                'proceedIdentification' => false,
1879
            ];
1880
        }
1881
        // return the response (which should be the user name)
1882
        if ($decoded_token['preferred_username'] === $username) {
1883
            $session->set('user-duo_status', 'COMPLET');
1884
            $session->set('user-duo_state','');
1885
            $session->set('user-duo_data','');
1886
            $session->set('user-login', $username);
1887
1888
            return [
1889
                'error' => false,
1890
                'message' => '',
1891
                'proceedIdentification' => true,
1892
                'authenticated_username' => $decoded_token['preferred_username']
1893
            ];
1894
        } else {
1895
            // Something wrong, username from the original Duo request is different than the one received now
1896
            $session->set('user-duo_status','');
1897
            $session->set('user-duo_state','');
1898
            $session->set('user-duo_data','');
1899
1900
            return [
1901
                'error' => true,
1902
                'message' => $lang->get('duo_login_mismatch'),
1903
                'pwd_attempts' => (int) $sessionPwdAttempts,
1904
                'proceedIdentification' => false,
1905
            ];
1906
        }
1907
    }
1908
    // If we are here something wrong
1909
    $session->set('user-duo_status','');
1910
    $session->set('user-duo_state','');
1911
    $session->set('user-duo_data','');
1912
    return [
1913
        'error' => true,
1914
        'message' => $lang->get('duo_login_mismatch'),
1915
        'pwd_attempts' => (int) $sessionPwdAttempts,
1916
        'proceedIdentification' => false,
1917
    ];
1918
}
1919
1920
/**
1921
 * Undocumented function.
1922
 *
1923
 * @param string                $passwordClear Password in clear
1924
 * @param array|string          $userInfo      Array of user data
1925
 *
1926
 * @return arrau
0 ignored issues
show
Bug introduced by
The type arrau 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...
1927
 */
1928
function checkCredentials($passwordClear, $userInfo): array
1929
{
1930
    $passwordManager = new PasswordManager();
1931
    // Migrate password if needed
1932
    $result = $passwordManager->migratePassword(
1933
        $userInfo['pw'],
1934
        $passwordClear,
1935
        (int) $userInfo['id'],
1936
        (bool) $userInfo['admin']
1937
    );
1938
1939
    if ($result['status'] === false || $passwordManager->verifyPassword($result['hashedPassword'], $passwordClear) === false) {
1940
        // Password is not correct
1941
        return [
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('authentica...e, 'migrated' => false) returns the type array<string,false> which is incompatible with the documented return type arrau.
Loading history...
1942
            'authenticated' => false,
1943
            'migrated' => false
1944
        ];
1945
    }
1946
1947
    return [
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('authentica...result['migratedUser']) returns the type array<string,mixed|true> which is incompatible with the documented return type arrau.
Loading history...
1948
        'authenticated' => true,
1949
        'migrated' => $result['migratedUser']
1950
    ];
1951
}
1952
1953
/**
1954
 * Undocumented function.
1955
 *
1956
 * @param bool   $enabled text1
1957
 * @param string $dbgFile text2
1958
 * @param string $text    text3
1959
 */
1960
function debugIdentify(bool $enabled, string $dbgFile, string $text): void
1961
{
1962
    if ($enabled === true) {
1963
        $fp = fopen($dbgFile, 'a');
1964
        if ($fp !== false) {
1965
            fwrite(
1966
                $fp,
1967
                $text
1968
            );
1969
        }
1970
    }
1971
}
1972
1973
1974
1975
function identifyGetUserCredentials(
1976
    array $SETTINGS,
1977
    string $serverPHPAuthUser,
1978
    string $serverPHPAuthPw,
1979
    string $userPassword,
1980
    string $userLogin
1981
): array
1982
{
1983
    if ((int) $SETTINGS['enable_http_request_login'] === 1
1984
        && $serverPHPAuthUser !== null
1985
        && (int) $SETTINGS['maintenance_mode'] === 1
1986
    ) {
1987
        if (strpos($serverPHPAuthUser, '@') !== false) {
1988
            return [
1989
                'username' => explode('@', $serverPHPAuthUser)[0],
1990
                'passwordClear' => $serverPHPAuthPw
1991
            ];
1992
        }
1993
        
1994
        if (strpos($serverPHPAuthUser, '\\') !== false) {
1995
            return [
1996
                'username' => explode('\\', $serverPHPAuthUser)[1],
1997
                'passwordClear' => $serverPHPAuthPw
1998
            ];
1999
        }
2000
2001
        return [
2002
            'username' => $serverPHPAuthPw,
2003
            'passwordClear' => $serverPHPAuthPw
2004
        ];
2005
    }
2006
    
2007
    return [
2008
        'username' => $userLogin,
2009
        'passwordClear' => $userPassword
2010
    ];
2011
}
2012
2013
2014
class initialChecks {
2015
    // Properties
2016
    public $login;
2017
2018
    /**
2019
     * Check if the user or his IP address is blocked due to a high number of
2020
     * failed attempts.
2021
     * 
2022
     * @param string $username - The login tried to login.
2023
     * @param string $ip - The remote address of the user.
2024
     */
2025
    public function isTooManyPasswordAttempts($username, $ip) {
2026
2027
        // Check for existing lock
2028
        $unlock_at = DB::queryFirstField(
2029
            'SELECT MAX(unlock_at)
2030
             FROM ' . prefixTable('auth_failures') . '
2031
             WHERE unlock_at > %s
2032
             AND ((source = %s AND value = %s) OR (source = %s AND value = %s))',
2033
            date('Y-m-d H:i:s', time()),
2034
            'login',
2035
            $username,
2036
            'remote_ip',
2037
            $ip
2038
        );
2039
2040
        // Account or remote address locked
2041
        if ($unlock_at) {
2042
            throw new Exception((string) $unlock_at);
2043
        }
2044
    }
2045
2046
    public function getUserInfo($login, $enable_ad_user_auto_creation, $oauth2_enabled) {
2047
        $session = SessionManager::getSession();
2048
    
2049
        // Get user info from DB
2050
        $data = DB::queryFirstRow(
2051
            'SELECT u.*, a.value AS api_key
2052
            FROM ' . prefixTable('users') . ' AS u
2053
            LEFT JOIN ' . prefixTable('api') . ' AS a ON (u.id = a.user_id)
2054
            WHERE login = %s AND deleted_at IS NULL',
2055
            $login
2056
        );
2057
    
2058
        // User doesn't exist then return error
2059
        // Except if user creation from LDAP is enabled
2060
        if (
2061
            DB::count() === 0
2062
            && !filter_var($enable_ad_user_auto_creation, FILTER_VALIDATE_BOOLEAN) 
2063
            && !filter_var($oauth2_enabled, FILTER_VALIDATE_BOOLEAN)
2064
        ) {
2065
            throw new Exception("error");
2066
        }
2067
    
2068
        // We cannot create a user with LDAP if the OAuth2 login is ongoing
2069
        $data['oauth2_login_ongoing'] = filter_var($session->get('userOauth2Info')['oauth2LoginOngoing'] ?? false, FILTER_VALIDATE_BOOLEAN) ?? false;
2070
    
2071
        $data['ldap_user_to_be_created'] = (
2072
            filter_var($enable_ad_user_auto_creation, FILTER_VALIDATE_BOOLEAN) &&
2073
            DB::count() === 0 &&
2074
            !$data['oauth2_login_ongoing']
2075
        );
2076
        $data['oauth2_user_not_exists'] = (
2077
            filter_var($oauth2_enabled, FILTER_VALIDATE_BOOLEAN) &&
2078
            DB::count() === 0 &&
2079
            $data['oauth2_login_ongoing']
2080
        );
2081
    
2082
        return $data;
2083
    }
2084
2085
    public function isMaintenanceModeEnabled($maintenance_mode, $user_admin) {
2086
        if ((int) $maintenance_mode === 1 && (int) $user_admin === 0) {
2087
            throw new Exception(
2088
                "error" 
2089
            );
2090
        }
2091
    }
2092
2093
    public function is2faCodeRequired(
2094
        $yubico,
2095
        $ga,
2096
        $duo,
2097
        $admin,
2098
        $adminMfaRequired,
2099
        $mfa,
2100
        $userMfaSelection,
2101
        $userMfaEnabled
2102
    ) {
2103
        if (
2104
            (empty($userMfaSelection) === true &&
2105
            isOneVarOfArrayEqualToValue(
2106
                [
2107
                    (int) $yubico,
2108
                    (int) $ga,
2109
                    (int) $duo
2110
                ],
2111
                1
2112
            ) === true)
2113
            && (((int) $admin !== 1 && $userMfaEnabled === true) || ((int) $adminMfaRequired === 1 && (int) $admin === 1))
2114
            && $mfa === true
2115
        ) {
2116
            throw new Exception(
2117
                "error" 
2118
            );
2119
        }
2120
    }
2121
2122
    public function isInstallFolderPresent($admin, $install_folder) {
2123
        if ((int) $admin === 1 && is_dir($install_folder) === true) {
2124
            throw new Exception(
2125
                "error" 
2126
            );
2127
        }
2128
    }
2129
}
2130
2131
2132
/**
2133
 * Permit to get info about user before auth step
2134
 *
2135
 * @param array $SETTINGS
2136
 * @param integer $sessionPwdAttempts
2137
 * @param string $username
2138
 * @param integer $sessionAdmin
2139
 * @param string $sessionUrl
2140
 * @param string $user2faSelection
2141
 * @param boolean $oauth2Token
2142
 * @return array
2143
 */
2144
function identifyDoInitialChecks(
2145
    $SETTINGS,
2146
    int $sessionPwdAttempts,
2147
    string $username,
2148
    int $sessionAdmin,
2149
    string $sessionUrl,
2150
    string $user2faSelection
2151
): array
2152
{
2153
    $session = SessionManager::getSession();
2154
    $checks = new initialChecks();
2155
    $enableAdUserAutoCreation = $SETTINGS['enable_ad_user_auto_creation'] ?? false;
2156
    $oauth2Enabled = $SETTINGS['oauth2_enabled'] ?? false;
2157
    $lang = new Language($session->get('user-language') ?? 'english');
2158
2159
    // Brute force management
2160
    try {
2161
        $checks->isTooManyPasswordAttempts($username, getClientIpServer());
2162
    } catch (Exception $e) {
2163
        $session->set('userOauth2Info', '');
2164
        logEvents($SETTINGS, 'failed_auth', 'user_not_exists', '', stripslashes($username), stripslashes($username));
2165
        return [
2166
            'error' => true,
2167
            'skip_anti_bruteforce' => true,
2168
            'array' => [
2169
                'value' => 'bruteforce_wait',
2170
                'error' => true,
2171
                'message' => $lang->get('bruteforce_wait') . (string) $e->getMessage(),
2172
            ]
2173
        ];
2174
    }
2175
2176
    // Check if user exists
2177
    try {
2178
        $userInfo = $checks->getUserInfo($username, $enableAdUserAutoCreation, $oauth2Enabled);
2179
    } catch (Exception $e) {
2180
        logEvents($SETTINGS, 'failed_auth', 'user_not_exists', '', stripslashes($username), stripslashes($username));
2181
        return [
2182
            'error' => true,
2183
            'array' => [
2184
                'error' => true,
2185
                'message' => $lang->get('error_bad_credentials'),
2186
            ]
2187
        ];
2188
    }
2189
2190
    // Manage Maintenance mode
2191
    try {
2192
        $checks->isMaintenanceModeEnabled(
2193
            $SETTINGS['maintenance_mode'],
2194
            $userInfo['admin']
2195
        );
2196
    } catch (Exception $e) {
2197
        return [
2198
            'error' => true,
2199
            'skip_anti_bruteforce' => true,
2200
            'array' => [
2201
                'value' => '',
2202
                'error' => 'maintenance_mode_enabled',
2203
                'message' => '',
2204
            ]
2205
        ];
2206
    }
2207
    
2208
    // user should use MFA?
2209
    $userInfo['mfa_auth_requested_roles'] = mfa_auth_requested_roles(
2210
        (string) $userInfo['fonction_id'],
2211
        is_null($SETTINGS['mfa_for_roles']) === true ? '' : (string) $SETTINGS['mfa_for_roles']
2212
    );
2213
2214
    // Check if 2FA code is requested
2215
    try {
2216
        $checks->is2faCodeRequired(
2217
            $SETTINGS['yubico_authentication'],
2218
            $SETTINGS['google_authentication'],
2219
            $SETTINGS['duo'],
2220
            $userInfo['admin'],
2221
            $SETTINGS['admin_2fa_required'],
2222
            $userInfo['mfa_auth_requested_roles'],
2223
            $user2faSelection,
2224
            $userInfo['mfa_enabled']
2225
        );
2226
    } catch (Exception $e) {
2227
        return [
2228
            'error' => true,
2229
            'array' => [
2230
                'value' => '2fa_not_set',
2231
                'user_admin' => (int) $sessionAdmin,
2232
                'initial_url' => $sessionUrl,
2233
                'pwd_attempts' => (int) $sessionPwdAttempts,
2234
                'error' => '2fa_not_set',
2235
                'message' => $lang->get('select_valid_2fa_credentials'),
2236
            ]
2237
        ];
2238
    }
2239
    // If admin user then check if folder install exists
2240
    // if yes then refuse connection
2241
    try {
2242
        $checks->isInstallFolderPresent(
2243
            $userInfo['admin'],
2244
            '../install'
2245
        );
2246
    } catch (Exception $e) {
2247
        return [
2248
            'error' => true,
2249
            'array' => [
2250
                'value' => '',
2251
                'user_admin' => $sessionAdmin,
2252
                'initial_url' => $sessionUrl,
2253
                'pwd_attempts' => (int) $sessionPwdAttempts,
2254
                'error' => true,
2255
                'message' => $lang->get('remove_install_folder'),
2256
            ]
2257
        ];
2258
    }
2259
2260
    // Check if  migration of password hash has previously failed
2261
    //cleanFailedAuthRecords($userInfo);
2262
2263
    // Return some usefull information about user
2264
    return [
2265
        'error' => false,
2266
        'user_mfa_mode' => $user2faSelection,
2267
        'userInfo' => $userInfo,
2268
    ];
2269
}
2270
2271
function cleanFailedAuthRecords(array $userInfo) : void
2272
{
2273
    // Clean previous failed attempts
2274
    $failedTasks = DB::query(
2275
        'SELECT increment_id
2276
        FROM ' . prefixTable('background_tasks') . '
2277
        WHERE process_type = %s
2278
        AND JSON_EXTRACT(arguments, "$.new_user_id") = %i
2279
        AND status = %s',
2280
        'create_user_keys',
2281
        $userInfo['id'],
2282
        'failed'
2283
    );
2284
2285
    // Supprimer chaque tâche échouée et ses sous-tâches
2286
    foreach ($failedTasks as $task) {
2287
        $incrementId = $task['increment_id'];
2288
2289
        // Supprimer les sous-tâches associées
2290
        DB::delete(
2291
            prefixTable('background_subtasks'),
2292
            'sub_task_in_progress = %i',
2293
            $incrementId
2294
        );
2295
2296
        // Supprimer la tâche principale
2297
        DB::delete(
2298
            prefixTable('background_tasks'),
2299
            'increment_id = %i',
2300
            $incrementId
2301
        );
2302
    }
2303
}
2304
2305
function identifyDoLDAPChecks(
2306
    $SETTINGS,
2307
    $userInfo,
2308
    string $username,
2309
    string $passwordClear,
2310
    int $sessionAdmin,
2311
    string $sessionUrl,
2312
    int $sessionPwdAttempts
2313
): array
2314
{
2315
    $session = SessionManager::getSession();
2316
    $lang = new Language($session->get('user-language') ?? 'english');
2317
2318
    // Prepare LDAP connection if set up
2319
    if ((int) $SETTINGS['ldap_mode'] === 1
2320
        && $username !== 'admin'
2321
        && ((string) $userInfo['auth_type'] === 'ldap' || $userInfo['ldap_user_to_be_created'] === true)
2322
    ) {
2323
        $retLDAP = authenticateThroughAD(
2324
            $username,
2325
            $userInfo,
2326
            $passwordClear,
2327
            $SETTINGS
2328
        );
2329
        if ($retLDAP['error'] === true) {
2330
            return [
2331
                'error' => true,
2332
                'array' => [
2333
                    'value' => '',
2334
                    'user_admin' => $sessionAdmin,
2335
                    'initial_url' => $sessionUrl,
2336
                    'pwd_attempts' => (int) $sessionPwdAttempts,
2337
                    'error' => true,
2338
                    'message' => $lang->get('error_bad_credentials'),
2339
                ]
2340
            ];
2341
        }
2342
        return [
2343
            'error' => false,
2344
            'retLDAP' => $retLDAP,
2345
            'ldapConnection' => true,
2346
            'userPasswordVerified' => true,
2347
        ];
2348
    }
2349
2350
    // return if no addmin
2351
    return [
2352
        'error' => false,
2353
        'retLDAP' => [],
2354
        'ldapConnection' => false,
2355
        'userPasswordVerified' => false,
2356
    ];
2357
}
2358
2359
2360
function shouldUserAuthWithOauth2(
2361
    array $SETTINGS,
2362
    array $userInfo,
2363
    string $username
2364
): array
2365
{
2366
    // Security issue without this return if an user auth_type == oauth2 and
2367
    // oauth2 disabled : we can login as a valid user by using hashUserId(username)
2368
    // as password in the login the form.
2369
    if ((int) $SETTINGS['oauth2_enabled'] !== 1 && filter_var($userInfo['oauth2_login_ongoing'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) === true) {
2370
        return [
2371
            'error' => true,
2372
            'message' => 'user_not_allowed_to_auth_to_teampass_app',
2373
            'oauth2Connection' => false,
2374
            'userPasswordVerified' => false,
2375
        ];
2376
    }
2377
2378
    // Prepare Oauth2 connection if set up
2379
    if ($username !== 'admin') {
2380
        // User has started to auth with oauth2
2381
        if ((bool) $userInfo['oauth2_login_ongoing'] === true) {
2382
            // Case where user exists in Teampass password login type
2383
            if ((string) $userInfo['auth_type'] === 'ldap' || (string) $userInfo['auth_type'] === 'local') {
2384
                // Add transparent recovery data if available
2385
                /*if (isset($userKeys['user_seed'])) {
2386
                    // Recrypt user private key with oauth2
2387
                    recryptUserPrivateKeyWithOauth2(
2388
                        (int) $userInfo['id'],
2389
                        $userKeys['user_seed'],
2390
                        $userKeys['public_key']
2391
                    );
2392
                }*/
2393
                    error_log('Switch user ' . $username . ' auth_type to oauth2');
2394
                // Update user in database:
2395
                DB::update(
2396
                    prefixTable('users'),
2397
                    array(
2398
                        //'special' => 'recrypt-private-key',
2399
                        'auth_type' => 'oauth2',
2400
                    ),
2401
                    'id = %i',
2402
                    $userInfo['id']
2403
                );
2404
                // Update session auth type
2405
                $session = SessionManager::getSession();
2406
                $session->set('user-auth_type', 'oauth2');
2407
                // Accept login request
2408
                return [
2409
                    'error' => false,
2410
                    'message' => '',
2411
                    'oauth2Connection' => true,
2412
                    'userPasswordVerified' => true,
2413
                ];
2414
            } elseif ((string) $userInfo['auth_type'] === 'oauth2' || (bool) $userInfo['oauth2_login_ongoing'] === true) {
2415
                // OAuth2 login request on OAuth2 user account.
2416
                return [
2417
                    'error' => false,
2418
                    'message' => '',
2419
                    'oauth2Connection' => true,
2420
                    'userPasswordVerified' => true,
2421
                ];
2422
            } else {
2423
                // Case where auth_type is not managed
2424
                return [
2425
                    'error' => true,
2426
                    'message' => 'user_not_allowed_to_auth_to_teampass_app',
2427
                    'oauth2Connection' => false,
2428
                    'userPasswordVerified' => false,
2429
                ];
2430
            }
2431
        } else {
2432
            // User has started to auth the normal way
2433
            if ((string) $userInfo['auth_type'] === 'oauth2') {
2434
                // Case where user exists in Teampass but not allowed to auth with Oauth2
2435
                return [
2436
                    'error' => true,
2437
                    'message' => 'error_bad_credentials',
2438
                    'oauth2Connection' => false,
2439
                    'userPasswordVerified' => false,
2440
                ];
2441
            }
2442
        }
2443
    }
2444
2445
    // return if no addmin
2446
    return [
2447
        'error' => false,
2448
        'message' => '',
2449
        'oauth2Connection' => false,
2450
        'userPasswordVerified' => false,
2451
    ];
2452
}
2453
2454
function checkOauth2User(
2455
    array $SETTINGS,
2456
    array $userInfo,
2457
    string $username,
2458
    string $passwordClear,
2459
    int $userLdapHasBeenCreated
2460
): array
2461
{
2462
    // Is oauth2 user in Teampass?
2463
    if ((int) $SETTINGS['oauth2_enabled'] === 1
2464
        && $username !== 'admin'
2465
        && filter_var($userInfo['oauth2_user_not_exists'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) === true
2466
        && (int) $userLdapHasBeenCreated === 0
2467
    ) {
2468
        // Is allowed to self register with oauth2?
2469
        if (empty($SETTINGS['oauth_self_register_groups'])) {
2470
            // No self registration is allowed
2471
            return [
2472
                'error' => true,
2473
                'message' => 'error_bad_credentials',
2474
            ];
2475
        } else {
2476
            // Self registration is allowed
2477
            // Create user in Teampass
2478
            $userInfo['oauth2_user_to_be_created'] = true;
2479
            return createOauth2User(
2480
                $SETTINGS,
2481
                $userInfo,
2482
                $username,
2483
                $passwordClear,
2484
                true
2485
            );
2486
        }
2487
    
2488
    } elseif (isset($userInfo['id']) === true && empty($userInfo['id']) === false) {
2489
        // User is in construction, please wait for email
2490
        if (
2491
            isset($userInfo['is_ready_for_usage']) && (int) $userInfo['is_ready_for_usage'] !== 1 && 
2492
            $userInfo['ongoing_process_id'] !== null && (int) $userInfo['ongoing_process_id'] >= 0
2493
        ) {
2494
            // Check if the creation of user keys has failed
2495
            $errorMessage = checkIfUserKeyCreationFailed((int) $userInfo['id']);
2496
            if (!is_null($errorMessage)) {
2497
                // Refresh user info to permit retry
2498
                DB::update(
2499
                    prefixTable('users'),
2500
                    array(
2501
                        'is_ready_for_usage' => 1,
2502
                        'otp_provided' => 1,
2503
                        'ongoing_process_id' => NULL,
2504
                        'special' => 'none',
2505
                    ),
2506
                    'id = %i',
2507
                    $userInfo['id']
2508
                );
2509
2510
                // Prepare task to create user keys again
2511
                handleUserKeys(
2512
                    (int) $userInfo['id'],
2513
                    (string) $passwordClear,
2514
                    (int) NUMBER_ITEMS_IN_BATCH,
2515
                    '',
2516
                    true,
2517
                    true,
2518
                    true,
2519
                    false,
2520
                    'email_body_user_config_4',
2521
                    true,
2522
                    '',
2523
                    '',
2524
                );
2525
2526
                // Return error message
2527
                return [
2528
                    'error' => true,
2529
                    'message' => $errorMessage,
2530
                    'no_log_event' => true
2531
                ];
2532
            } else {
2533
                return [
2534
                    'error' => true,
2535
                    'message' => 'account_in_construction_please_wait_email',
2536
                    'no_log_event' => true
2537
                ];
2538
            }
2539
        }
2540
2541
        // CCheck if user should use oauth2
2542
        $ret = shouldUserAuthWithOauth2(
2543
            $SETTINGS,
2544
            $userInfo,
2545
            $username
2546
        );
2547
        if ($ret['error'] === true) {
2548
            return [
2549
                'error' => true,
2550
                'message' => $ret['message'],
2551
            ];
2552
        }
2553
2554
        // login/password attempt on a local account:
2555
        // Return to avoid overwrite of user password that can allow a user
2556
        // to steal a local account.
2557
        if (!$ret['oauth2Connection'] || !$ret['userPasswordVerified']) {
2558
            return [
2559
                'error' => false,
2560
                'message' => $ret['message'],
2561
                'ldapConnection' => false,
2562
                'userPasswordVerified' => false,        
2563
            ];
2564
        }
2565
2566
        // Oauth2 user already exists and authenticated
2567
        $userInfo['has_been_created'] = 0;
2568
        $passwordManager = new PasswordManager();
2569
2570
        // Update user hash un database if needed
2571
        if (!$passwordManager->verifyPassword($userInfo['pw'], $passwordClear)) {
2572
            DB::update(
2573
                prefixTable('users'),
2574
                [
2575
                    'pw' => $passwordManager->hashPassword($passwordClear),
2576
                ],
2577
                'id = %i',
2578
                $userInfo['id']
2579
            );
2580
2581
            // Transparent recovery: handle external OAuth2 password change
2582
            handleExternalPasswordChange(
2583
                (int) $userInfo['id'],
2584
                $passwordClear,
2585
                $userInfo,
2586
                $SETTINGS
2587
            );
2588
        }
2589
2590
        return [
2591
            'error' => false,
2592
            'retExternalAD' => $userInfo,
2593
            'oauth2Connection' => $ret['oauth2Connection'],
2594
            'userPasswordVerified' => $ret['userPasswordVerified'],
2595
        ];
2596
    }
2597
2598
    // return if no admin
2599
    return [
2600
        'error' => false,
2601
        'retLDAP' => [],
2602
        'ldapConnection' => false,
2603
        'userPasswordVerified' => false,
2604
    ];
2605
}
2606
2607
/**
2608
 * Check if a "create_user_keys" task failed for the given user_id.
2609
 *
2610
 * @param int $userId The user ID to check.
2611
 * @return string|null Returns an error message in English if a failed task is found, otherwise null.
2612
 */
2613
function checkIfUserKeyCreationFailed(int $userId): ?string
2614
{
2615
    // Find the latest "create_user_keys" task for the given user_id
2616
    $latestTask = DB::queryFirstRow(
2617
        'SELECT arguments, status FROM ' . prefixTable('background_tasks') . '
2618
        WHERE process_type = %s
2619
        AND arguments LIKE %s
2620
        ORDER BY increment_id DESC
2621
        LIMIT 1',
2622
        'create_user_keys', '%"new_user_id":' . $userId . '%'
2623
    );
2624
2625
    // If a failed task is found, return an error message
2626
    if ($latestTask && $latestTask['status'] === 'failed') {
2627
        return "The creation of user keys for user ID {$userId} failed. Please contact your administrator to check the background tasks log for more details.";
2628
    }
2629
2630
    // No failed task found for this user_id
2631
    return null;
2632
}
2633
2634
2635
/* * Create the user in Teampass
2636
 *
2637
 * @param array $SETTINGS
2638
 * @param array $userInfo
2639
 * @param string $username
2640
 * @param string $passwordClear
2641
 *
2642
 * @return array
2643
 */
2644
function createOauth2User(
2645
    array $SETTINGS,
2646
    array $userInfo,
2647
    string $username,
2648
    string $passwordClear,
2649
    bool $userSelfRegister = false
2650
): array
2651
{
2652
    // Prepare creating the new oauth2 user in Teampass
2653
    if ((int) $SETTINGS['oauth2_enabled'] === 1
2654
        && $username !== 'admin'
2655
        && filter_var($userInfo['oauth2_user_to_be_created'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) === true
2656
    ) {
2657
        $session = SessionManager::getSession();    
2658
        $lang = new Language($session->get('user-language') ?? 'english');
2659
2660
        // Prepare user groups
2661
        foreach ($userInfo['groups'] as $key => $group) {
2662
            // Check if the group is in the list of groups allowed to self register
2663
            // If the group is in the list, we remove it
2664
            if ($userSelfRegister === true && $group["displayName"] === $SETTINGS['oauth_self_register_groups']) {
2665
                unset($userInfo['groups'][$key]);
2666
            }
2667
        }
2668
        // Rebuild indexes
2669
        $userInfo['groups'] = array_values($userInfo['groups'] ?? []);
2670
        
2671
        // Create Oauth2 user if not exists and tasks enabled
2672
        $ret = externalAdCreateUser(
2673
            $username,
2674
            $passwordClear,
2675
            $userInfo['mail'] ?? '',
2676
            is_null($userInfo['givenname']) ? (is_null($userInfo['givenName']) ? '' : $userInfo['givenName']) : $userInfo['givenname'],
2677
            is_null($userInfo['surname']) ? '' : $userInfo['surname'],
2678
            'oauth2',
2679
            is_null($userInfo['groups']) ? [] : $userInfo['groups'],
2680
            $SETTINGS
2681
        );
2682
        $userInfo = array_merge($userInfo, $ret);
2683
2684
        // prepapre background tasks for item keys generation  
2685
        handleUserKeys(
2686
            (int) $userInfo['id'],
2687
            (string) $passwordClear,
2688
            (int) (isset($SETTINGS['maximum_number_of_items_to_treat']) === true ? $SETTINGS['maximum_number_of_items_to_treat'] : NUMBER_ITEMS_IN_BATCH),
2689
            uniqidReal(20),
2690
            true,
2691
            true,
2692
            true,
2693
            false,
2694
            $lang->get('email_body_user_config_2'),
2695
        );
2696
2697
        // Complete $userInfo
2698
        $userInfo['has_been_created'] = 1;
2699
2700
        if (WIP === true) error_log("--- USER CREATED ---");
2701
2702
        return [
2703
            'error' => false,
2704
            'retExternalAD' => $userInfo,
2705
            'oauth2Connection' => true,
2706
            'userPasswordVerified' => true,
2707
        ];
2708
    
2709
    } elseif (isset($userInfo['id']) === true && empty($userInfo['id']) === false) {
2710
        // CHeck if user should use oauth2
2711
        $ret = shouldUserAuthWithOauth2(
2712
            $SETTINGS,
2713
            $userInfo,
2714
            $username
2715
        );
2716
        if ($ret['error'] === true) {
2717
            return [
2718
                'error' => true,
2719
                'message' => $ret['message'],
2720
            ];
2721
        }
2722
2723
        // login/password attempt on a local account:
2724
        // Return to avoid overwrite of user password that can allow a user
2725
        // to steal a local account.
2726
        if (!$ret['oauth2Connection'] || !$ret['userPasswordVerified']) {
2727
            return [
2728
                'error' => false,
2729
                'message' => $ret['message'],
2730
                'ldapConnection' => false,
2731
                'userPasswordVerified' => false,        
2732
            ];
2733
        }
2734
2735
        // Oauth2 user already exists and authenticated
2736
        if (WIP === true) error_log("--- USER AUTHENTICATED ---");
2737
        $userInfo['has_been_created'] = 0;
2738
2739
        $passwordManager = new PasswordManager();
2740
2741
        // Update user hash un database if needed
2742
        if (!$passwordManager->verifyPassword($userInfo['pw'], $passwordClear)) {
2743
            DB::update(
2744
                prefixTable('users'),
2745
                [
2746
                    'pw' => $passwordManager->hashPassword($passwordClear),
2747
                ],
2748
                'id = %i',
2749
                $userInfo['id']
2750
            );
2751
        }
2752
2753
        return [
2754
            'error' => false,
2755
            'retExternalAD' => $userInfo,
2756
            'oauth2Connection' => $ret['oauth2Connection'],
2757
            'userPasswordVerified' => $ret['userPasswordVerified'],
2758
        ];
2759
    }
2760
2761
    // return if no admin
2762
    return [
2763
        'error' => false,
2764
        'retLDAP' => [],
2765
        'ldapConnection' => false,
2766
        'userPasswordVerified' => false,
2767
    ];
2768
}
2769
2770
function identifyDoMFAChecks(
2771
    $SETTINGS,
2772
    $userInfo,
2773
    $dataReceived,
2774
    $userInitialData,
2775
    string $username
2776
): array
2777
{
2778
    $session = SessionManager::getSession();
2779
    $lang = new Language($session->get('user-language') ?? 'english');
2780
    
2781
    switch ($userInitialData['user_mfa_mode']) {
2782
        case 'google':
2783
            $ret = googleMFACheck(
2784
                $username,
2785
                $userInfo,
2786
                $dataReceived,
2787
                $SETTINGS
2788
            );
2789
            if ($ret['error'] !== false) {
2790
                logEvents($SETTINGS, 'failed_auth', 'wrong_mfa_code', '', stripslashes($username), stripslashes($username));
2791
                return [
2792
                    'error' => true,
2793
                    'mfaData' => $ret,
2794
                    'mfaQRCodeInfos' => false,
2795
                ];
2796
            }
2797
2798
            return [
2799
                'error' => false,
2800
                'mfaData' => $ret['firstTime'],
2801
                'mfaQRCodeInfos' => $userInitialData['user_mfa_mode'] === 'google'
2802
                && count($ret['firstTime']) > 0 ? true : false,
2803
            ];
2804
        
2805
        case 'duo':
2806
            // Prepare Duo connection if set up
2807
            $checks = duoMFACheck(
2808
                $username,
2809
                $dataReceived,
2810
                $SETTINGS
2811
            );
2812
2813
            if ($checks['error'] === true) {
2814
                return [
2815
                    'error' => true,
2816
                    'mfaData' => $checks,
2817
                    'mfaQRCodeInfos' => false,
2818
                ];
2819
            }
2820
2821
            // If we are here
2822
            // Do DUO authentication
2823
            $ret = duoMFAPerform(
2824
                $username,
2825
                $dataReceived,
2826
                $checks['pwd_attempts'],
2827
                $checks['saved_state'],
2828
                $checks['duo_status'],
2829
                $SETTINGS
2830
            );
2831
2832
            if ($ret['error'] !== false) {
2833
                logEvents($SETTINGS, 'failed_auth', 'bad_duo_mfa', '', stripslashes($username), stripslashes($username));
2834
                $session->set('user-duo_status','');
2835
                $session->set('user-duo_state','');
2836
                $session->set('user-duo_data','');
2837
                return [
2838
                    'error' => true,
2839
                    'mfaData' => $ret,
2840
                    'mfaQRCodeInfos' => false,
2841
                ];
2842
            } else if ($ret['duo_url_ready'] === true){
2843
                return [
2844
                    'error' => false,
2845
                    'mfaData' => $ret,
2846
                    'duo_url_ready' => true,
2847
                    'mfaQRCodeInfos' => false,
2848
                ];
2849
            } else if ($ret['error'] === false) {
2850
                return [
2851
                    'error' => false,
2852
                    'mfaData' => $ret,
2853
                    'mfaQRCodeInfos' => false,
2854
                ];
2855
            }
2856
            break;
2857
        
2858
        default:
2859
            logEvents($SETTINGS, 'failed_auth', 'wrong_mfa_code', '', stripslashes($username), stripslashes($username));
2860
            return [
2861
                'error' => true,
2862
                'mfaData' => ['message' => $lang->get('wrong_mfa_code')],
2863
                'mfaQRCodeInfos' => false,
2864
            ];
2865
    }
2866
2867
    // If something went wrong, let's catch and return an error
2868
    logEvents($SETTINGS, 'failed_auth', 'wrong_mfa_code', '', stripslashes($username), stripslashes($username));
2869
    return [
2870
        'error' => true,
2871
        'mfaData' => ['message' => $lang->get('wrong_mfa_code')],
2872
        'mfaQRCodeInfos' => false,
2873
    ];
2874
}
2875
2876
function identifyDoAzureChecks(
2877
    array $SETTINGS,
2878
    $userInfo,
2879
    string $username
2880
): array
2881
{
2882
    $session = SessionManager::getSession();
2883
    $lang = new Language($session->get('user-language') ?? 'english');
2884
2885
    logEvents($SETTINGS, 'failed_auth', 'wrong_mfa_code', '', stripslashes($username), stripslashes($username));
2886
    return [
2887
        'error' => true,
2888
        'mfaData' => ['message' => $lang->get('wrong_mfa_code')],
2889
        'mfaQRCodeInfos' => false,
2890
    ];
2891
}
2892
2893
/**
2894
 * Add a failed authentication attempt to the database.
2895
 * If the number of failed attempts exceeds the limit, a lock is triggered.
2896
 * 
2897
 * @param string $source - The source of the failed attempt (login or remote_ip).
2898
 * @param string $value  - The value for this source (username or IP address).
2899
 * @param int    $limit  - The failure attempt limit after which the account/IP
2900
 *                         will be locked.
2901
 */
2902
function handleFailedAttempts($source, $value, $limit) {
2903
    // Count failed attempts from this source
2904
    $count = DB::queryFirstField(
2905
        'SELECT COUNT(*)
2906
        FROM ' . prefixTable('auth_failures') . '
2907
        WHERE source = %s AND value = %s',
2908
        $source,
2909
        $value
2910
    );
2911
2912
    // Add this attempt
2913
    $count++;
2914
2915
    // Calculate unlock time if number of attempts exceeds limit
2916
    $unlock_at = $count >= $limit
2917
        ? date('Y-m-d H:i:s', time() + (($count - $limit + 1) * 600))
2918
        : NULL;
2919
2920
    // Unlock account one time code
2921
    $unlock_code = ($count >= $limit && $source === 'login')
2922
        ? generateQuickPassword(30, false)
2923
        : NULL;
2924
2925
    // Insert the new failure into the database
2926
    DB::insert(
2927
        prefixTable('auth_failures'),
2928
        [
2929
            'source' => $source,
2930
            'value' => $value,
2931
            'unlock_at' => $unlock_at,
2932
            'unlock_code' => $unlock_code,
2933
        ]
2934
    );
2935
2936
    if ($unlock_at !== null && $source === 'login') {
2937
        $configManager = new ConfigManager();
2938
        $SETTINGS = $configManager->getAllSettings();
2939
        $lang = new Language($SETTINGS['default_language']);
2940
2941
        // Get user email
2942
        $userInfos = DB::queryFirstRow(
2943
            'SELECT email, name
2944
             FROM '.prefixTable('users').'
2945
             WHERE login = %s',
2946
             $value
2947
        );
2948
2949
        // No valid email address for user
2950
        if (!$userInfos || !filter_var($userInfos['email'], FILTER_VALIDATE_EMAIL))
2951
            return;
2952
2953
        $unlock_url = $SETTINGS['cpassman_url'].'/self-unlock.php?login='.$value.'&otp='.$unlock_code;
2954
2955
        sendMailToUser(
2956
            $userInfos['email'],
2957
            $lang->get('bruteforce_reset_mail_body'),
2958
            $lang->get('bruteforce_reset_mail_subject'),
2959
            [
2960
                '#name#' => $userInfos['name'],
2961
                '#reset_url#' => $unlock_url,
2962
                '#unlock_at#' => $unlock_at,
2963
            ],
2964
            true
2965
        );
2966
    }
2967
}
2968
2969
/**
2970
 * Add failed authentication attempts for both user login and IP address.
2971
 * This function will check the number of attempts for both the username and IP,
2972
 * and will trigger a lock if the number exceeds the defined limits.
2973
 * It also deletes logs older than 24 hours.
2974
 * 
2975
 * @param string $username - The username that was attempted to login.
2976
 * @param string $ip       - The IP address from which the login attempt was made.
2977
 */
2978
function addFailedAuthentication($username, $ip) {
2979
    $user_limit = 10;
2980
    $ip_limit = 30;
2981
2982
    // Remove old logs (more than 24 hours)
2983
    DB::delete(
2984
        prefixTable('auth_failures'),
2985
        'date < %s AND (unlock_at < %s OR unlock_at IS NULL)',
2986
        date('Y-m-d H:i:s', time() - (24 * 3600)),
2987
        date('Y-m-d H:i:s', time())
2988
    );
2989
2990
    // Add attempts in database
2991
    handleFailedAttempts('login', $username, $user_limit);
2992
    handleFailedAttempts('remote_ip', $ip, $ip_limit);
2993
}
2994