initialChecks::is2faCodeRequired()   B
last analyzed

Complexity

Conditions 8
Paths 2

Size

Total Lines 25
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 11
nc 2
nop 8
dl 0
loc 25
rs 8.4444
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
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-2026 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
    // Sanitize input data
164
    $toClean = [
165
        'login' => 'trim|escape',
166
    ];
167
    $dataReceived = sanitizeData($dataReceived, $toClean);
0 ignored issues
show
Bug introduced by
It seems like $dataReceived can also be of type string; however, parameter $rawData of sanitizeData() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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