Passed
Pull Request — master (#4955)
by Nils
06:22
created

checkCredentials()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 76
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 6
eloc 40
c 1
b 1
f 0
nc 4
nop 2
dl 0
loc 76
rs 8.6577

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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