identifyUser()   F
last analyzed

Complexity

Conditions 39
Paths 308

Size

Total Lines 399
Code Lines 245

Duplication

Lines 0
Ratio 0 %

Importance

Changes 11
Bugs 1 Features 0
Metric Value
cc 39
eloc 245
c 11
b 1
f 0
nc 308
nop 2
dl 0
loc 399
rs 1.5866

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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