buildAuthResponse()   F
last analyzed

Complexity

Conditions 22
Paths > 20000

Size

Total Lines 51
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 22
eloc 33
c 1
b 0
f 0
nc 296448
nop 8
dl 0
loc 51
rs 0

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Teampass - a collaborative passwords manager.
7
 * ---
8
 * This file is part of the TeamPass project.
9
 * 
10
 * TeamPass is free software: you can redistribute it and/or modify it
11
 * under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, version 3 of the License.
13
 * 
14
 * TeamPass is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 * 
19
 * You should have received a copy of the GNU General Public License
20
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21
 * 
22
 * Certain components of this file may be under different licenses. For
23
 * details, see the `licenses` directory or individual file headers.
24
 * ---
25
 * @file      identify.php
26
 * @author    Nils Laumaillé ([email protected])
27
 * @copyright 2009-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