identifyUser()   F
last analyzed

Complexity

Conditions 122
Paths > 20000

Size

Total Lines 694
Code Lines 448

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 122
eloc 448
nc 1513860
nop 2
dl 0
loc 694
rs 0
c 5
b 0
f 0

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