prepareUserEncryptionKeys()   F
last analyzed

Complexity

Conditions 28
Paths 1636

Size

Total Lines 149
Code Lines 84

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 2 Features 0
Metric Value
cc 28
eloc 84
c 2
b 2
f 0
nc 1636
nop 3
dl 0
loc 149
rs 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Teampass - a collaborative passwords manager.
7
 * ---
8
 * This file is part of the TeamPass project.
9
 * 
10
 * TeamPass is free software: you can redistribute it and/or modify it
11
 * under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, version 3 of the License.
13
 * 
14
 * TeamPass is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 * 
19
 * You should have received a copy of the GNU General Public License
20
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21
 * 
22
 * Certain components of this file may be under different licenses. For
23
 * details, see the `licenses` directory or individual file headers.
24
 * ---
25
 * @file      identify.php
26
 * @author    Nils Laumaillé ([email protected])
27
 * @copyright 2009-2026 Teampass.net
28
 * @license   GPL-3.0
29
 * @see       https://www.teampass.net
30
 */
31
32
use voku\helper\AntiXSS;
33
use TeampassClasses\SessionManager\SessionManager;
34
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
35
use TeampassClasses\Language\Language;
36
use TeampassClasses\PerformChecks\PerformChecks;
37
use TeampassClasses\ConfigManager\ConfigManager;
38
use TeampassClasses\NestedTree\NestedTree;
39
use TeampassClasses\PasswordManager\PasswordManager;
40
use Duo\DuoUniversal\Client;
41
use Duo\DuoUniversal\DuoException;
42
use RobThree\Auth\TwoFactorAuth;
43
use TeampassClasses\LdapExtra\LdapExtra;
44
use TeampassClasses\LdapExtra\OpenLdapExtra;
45
use TeampassClasses\LdapExtra\ActiveDirectoryExtra;
46
use TeampassClasses\OAuth2Controller\OAuth2Controller;
47
48
// Load functions
49
require_once 'main.functions.php';
50
51
// init
52
loadClasses('DB');
53
$session = SessionManager::getSession();
54
$request = SymfonyRequest::createFromGlobals();
55
$lang = new Language($session->get('user-language') ?? 'english');
56
57
// Load config
58
$configManager = new ConfigManager();
59
$SETTINGS = $configManager->getAllSettings();
60
61
// Define Timezone
62
date_default_timezone_set($SETTINGS['timezone'] ?? 'UTC');
63
64
// Set header properties
65
header('Content-type: text/html; charset=utf-8');
66
header('Cache-Control: no-cache, no-store, must-revalidate');
67
error_reporting(E_ERROR);
68
69
// --------------------------------- //
70
71
// Prepare POST variables
72
$post_type = filter_input(INPUT_POST, 'type', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
73
$post_login = filter_input(INPUT_POST, 'login', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
74
$post_data = filter_input(INPUT_POST, 'data', FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_FLAG_NO_ENCODE_QUOTES);
75
76
if ($post_type === 'identify_user') {
77
    //--------
78
    // NORMAL IDENTICATION STEP
79
    //--------
80
81
    // Ensure Complexity levels are translated
82
    defineComplexity();
83
84
    // Identify the user through Teampass process
85
    identifyUser($post_data, $SETTINGS);
86
87
    // ---
88
    // ---
89
    // ---
90
} elseif ($post_type === 'get2FAMethods') {
91
    //--------
92
    // Get MFA methods
93
    //--------
94
    //
95
96
    // Encrypt data to return
97
    echo json_encode([
98
        'ret' => prepareExchangedData(
99
            [
100
                'agses' => isKeyExistingAndEqual('agses_authentication_enabled', 1, $SETTINGS) === true ? true : false,
101
                'google' => isKeyExistingAndEqual('google_authentication', 1, $SETTINGS) === true ? true : false,
102
                'yubico' => isKeyExistingAndEqual('yubico_authentication', 1, $SETTINGS) === true ? true : false,
103
                'duo' => isKeyExistingAndEqual('duo', 1, $SETTINGS) === true ? true : false,
104
            ],
105
            'encode'
106
        ),
107
        'key' => $session->get('key'),
108
    ]);
109
    return false;
110
} elseif ($post_type === 'initiateSSOLogin') {
111
    //--------
112
    // Do initiateSSOLogin
113
    //--------
114
    //
115
116
    // Création d'une instance du contrôleur
117
    $OAuth2 = new OAuth2Controller($SETTINGS);
118
119
    // Redirection vers Azure pour l'authentification
120
    $OAuth2->redirect();
121
122
    // Encrypt data to return
123
    echo json_encode([
124
        'key' => $session->get('key'),
125
    ]);
126
    return false;
127
}
128
129
/**
130
 * Complete authentication of user through Teampass
131
 *
132
 * @param string $sentData Credentials
133
 * @param array $SETTINGS Teampass settings
134
 *
135
 * @return bool
136
 */
137
function identifyUser(string $sentData, array $SETTINGS): bool
138
{
139
    $session = SessionManager::getSession();
140
    $request = SymfonyRequest::createFromGlobals();
141
    $lang = new Language($session->get('user-language') ?? 'english');
142
    
143
    // Prepare GET variables
144
    $sessionAdmin = $session->get('user-admin');
145
    $sessionPwdAttempts = $session->get('pwd_attempts');
146
    $sessionUrl = $session->get('user-initial_url');
147
    $server = [];
148
    $server['PHP_AUTH_USER'] =  $request->getUser();
149
    $server['PHP_AUTH_PW'] = $request->getPassword();
150
    
151
    // decrypt and retreive data in JSON format
152
    if ($session->get('key') === null) {
153
        $dataReceived = $sentData;
154
    } else {
155
        $dataReceived = prepareExchangedData(
156
            $sentData,
157
            'decode',
158
            $session->get('key')
159
        );
160
    }
161
162
    // Sanitize input data
163
    $toClean = [
164
        'login' => 'trim|escape',
165
    ];
166
    $dataReceived = sanitizeData($dataReceived, $toClean);
0 ignored issues
show
Bug introduced by
It seems like $dataReceived can also be of type string; however, parameter $rawData of sanitizeData() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

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