buildAuthResponse()   F
last analyzed

Complexity

Conditions 22
Paths > 20000

Size

Total Lines 51
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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