Passed
Push — master ( 97d9b0...f5b400 )
by Nils
06:48 queued 15s
created

getUserADGroups()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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