Passed
Push — master ( 12c274...87b47c )
by Nils
06:29 queued 10s
created

handleUserADGroups()   B

Complexity

Conditions 10
Paths 11

Size

Total Lines 44
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 22
c 1
b 0
f 0
nc 11
nop 4
dl 0
loc 44
rs 7.6666

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

166
    $dataReceived = sanitizeData(/** @scrutinizer ignore-type */ $dataReceived, $toClean);
Loading history...
167
168
    // Base64 decode sensitive data
169
    if (isset($dataReceived['pw'])) {
170
        $dataReceived['pw'] = base64_decode($dataReceived['pw']);
171
    }
172
    
173
    // Check if Duo auth is in progress and pass the pw and login back to the standard login process
174
    if(
175
        isKeyExistingAndEqual('duo', 1, $SETTINGS) === true
176
        && $dataReceived['user_2fa_selection'] === 'duo'
177
        && $session->get('user-duo_status') === 'IN_PROGRESS'
178
        && !empty($dataReceived['duo_state'])
179
    ){
180
        $key = hash('sha256', $dataReceived['duo_state']);
181
        $iv = substr(hash('sha256', $dataReceived['duo_state']), 0, 16);
182
        $duo_data_dec = openssl_decrypt(base64_decode($session->get('user-duo_data')), 'AES-256-CBC', $key, 0, $iv);
183
        // Clear the data from the Duo process to continue clean with the standard login process
184
        $session->set('user-duo_data','');
185
        if($duo_data_dec === false) {
186
            // Add failed authentication log
187
            addFailedAuthentication(filter_var($dataReceived['login'], FILTER_SANITIZE_FULL_SPECIAL_CHARS), getClientIpServer());
188
189
            echo prepareExchangedData(
190
                [
191
                    'error' => true,
192
                    'message' => $lang->get('duo_error_decrypt'),
193
                ],
194
                'encode'
195
            );
196
            return false;
197
        }
198
        $duo_data = unserialize($duo_data_dec);
199
        $dataReceived['pw'] = $duo_data['duo_pwd'];
200
        $dataReceived['login'] = $duo_data['duo_login'];
201
    }
202
203
    if(isset($dataReceived['pw']) === false || isset($dataReceived['login']) === false) {
204
        echo json_encode([
205
            'data' => prepareExchangedData(
206
                [
207
                    'error' => true,
208
                    'message' => $lang->get('ga_enter_credentials'),
209
                ],
210
                'encode'
211
            ),
212
            'key' => $session->get('key')
213
        ]);
214
        return false;
215
    }
216
217
    // prepare variables
218
    // IMPORTANT: Do NOT sanitize passwords - they should be treated as opaque binary data
219
    // Sanitization was removed to fix authentication issues (see upgrade 3.1.5.10)
220
    $userCredentials = identifyGetUserCredentials(
221
        $SETTINGS,
222
        (string) $server['PHP_AUTH_USER'],
223
        (string) $server['PHP_AUTH_PW'],
224
        (string) $dataReceived['pw'], // No more sanitization
225
        (string) filter_var($dataReceived['login'], FILTER_SANITIZE_FULL_SPECIAL_CHARS)
226
    );
227
    $username = $userCredentials['username'];
228
    $passwordClear = $userCredentials['passwordClear'];
229
    
230
    // DO initial checks
231
    $userInitialData = identifyDoInitialChecks(
232
        $SETTINGS,
233
        (int) $sessionPwdAttempts,
234
        (string) $username,
235
        (int) $sessionAdmin,
236
        (string) $sessionUrl,
237
        (string) filter_var($dataReceived['user_2fa_selection'], FILTER_SANITIZE_FULL_SPECIAL_CHARS)
238
    );
239
240
    // if user doesn't exist in Teampass then return error
241
    if ($userInitialData['error'] === true) {
242
        // Add log on error unless skip_anti_bruteforce flag is set to true
243
        if (empty($userInitialData['skip_anti_bruteforce'])
244
            || !$userInitialData['skip_anti_bruteforce']) {
245
246
            // Add failed authentication log
247
            addFailedAuthentication($username, getClientIpServer());
248
        }
249
250
        echo prepareExchangedData(
251
            $userInitialData['array'],
252
            'encode'
253
        );
254
        return false;
255
    }
256
257
    $userInfo = $userInitialData['userInfo'] + $dataReceived;
258
    $return = '';
259
260
    // Check if LDAP is enabled and user is in AD
261
    $userLdap = identifyDoLDAPChecks(
262
        $SETTINGS,
263
        $userInfo,
264
        (string) $username,
265
        (string) $passwordClear,
266
        (int) $sessionAdmin,
267
        (string) $sessionUrl,
268
        (int) $sessionPwdAttempts
269
    );
270
    if ($userLdap['error'] === true) {
271
        // Log LDAP/AD failed authentication into teampass_log_system
272
        logEvents(
273
            $SETTINGS,
274
            'failed_auth',
275
            'password_is_not_correct',
276
            '',
277
            stripslashes($username),
278
            stripslashes($username)
279
        );
280
281
        // Anti-bruteforce log
282
        addFailedAuthentication($username, getClientIpServer());
283
284
        echo prepareExchangedData(
285
            $userLdap['array'],
286
            'encode'
287
        );
288
        return false;
289
    }
290
291
    if (isset($userLdap['user_info']) === true && (int) $userLdap['user_info']['has_been_created'] === 1) {
292
        // Add failed authentication log
293
        addFailedAuthentication($username, getClientIpServer());
294
295
        echo json_encode([
296
            'data' => prepareExchangedData(
297
                [
298
                    'error' => true,
299
                    'message' => '',
300
                    'extra' => 'ad_user_created',
301
                ],
302
                'encode'
303
            ),
304
            'key' => $session->get('key')
305
        ]);
306
        return false;
307
    }
308
    
309
    // Is oauth2 user exists?
310
    $userOauth2 = checkOauth2User(
311
        (array) $SETTINGS,
312
        (array) $userInfo,
313
        (string) $username,
314
        (string) $passwordClear,
315
        (int) $userLdap['user_info']['has_been_created']
316
    );
317
    if ($userOauth2['error'] === true) {
318
        $session->set('userOauth2Info', '');
319
320
        // Add failed authentication log
321
        if ($userOauth2['no_log_event'] !== true) {
322
            addFailedAuthentication($username, getClientIpServer());
323
        }
324
325
        // deepcode ignore ServerLeak: File and path are secured directly inside the function decryptFile()        
326
        echo prepareExchangedData(
327
            [
328
                'error' => true,
329
                'message' => $lang->get($userOauth2['message']),
330
                'extra' => 'oauth2_user_not_found',
331
            ],
332
            'encode'
333
        );
334
        return false;
335
    }
336
337
    // Check user and password
338
    $authResult = checkCredentials($passwordClear, $userInfo);
339
    if ($userLdap['userPasswordVerified'] === false && $userOauth2['userPasswordVerified'] === false && $authResult['authenticated'] !== true) {
340
        // Log failure (wrong password on existing account) into teampass_log_system
341
        logEvents($SETTINGS, 'failed_auth', 'password_is_not_correct', '', stripslashes($username), stripslashes($username));
342
        // Add failed authentication log
343
        addFailedAuthentication($username, getClientIpServer());
344
        echo prepareExchangedData(
345
            [
346
                'value' => '',
347
                'error' => true,
348
                'message' => $lang->get('error_bad_credentials'),
349
            ],
350
            'encode'
351
        );
352
        return false;
353
    }
354
    
355
    // If password was migrated, then update private key
356
    if ($authResult['password_migrated'] === true) {
357
        $userInfo['private_key'] = $authResult['private_key_reencrypted'];
358
    }
359
    
360
    // Check if MFA is required
361
    if ((
362
        isOneVarOfArrayEqualToValue(
363
            [
364
                (int) $SETTINGS['yubico_authentication'],
365
                (int) $SETTINGS['google_authentication'],
366
                (int) $SETTINGS['duo']
367
            ],
368
            1
369
        ) === true)
370
        && (((int) $userInfo['admin'] !== 1 && (int) $userInfo['mfa_enabled'] === 1 && $userInfo['mfa_auth_requested_roles'] === true)
371
        || ((int) $SETTINGS['admin_2fa_required'] === 1 && (int) $userInfo['admin'] === 1))
372
    ) {
373
        // Check user against MFA method if selected
374
        $userMfa = identifyDoMFAChecks(
375
            $SETTINGS,
376
            $userInfo,
377
            $dataReceived,
378
            $userInitialData,
379
            (string) $username
380
        );
381
        if ($userMfa['error'] === true) {
382
            // Add failed authentication log
383
            addFailedAuthentication($username, getClientIpServer());
384
385
            echo prepareExchangedData(
386
                [
387
                    'error' => true,
388
                    'message' => $userMfa['mfaData']['message'],
389
                    'mfaStatus' => $userMfa['mfaData']['mfaStatus'],
390
                ],
391
                'encode'
392
            );
393
            return false;
394
        } elseif ($userMfa['mfaQRCodeInfos'] === true) {
395
            // Add failed authentication log
396
            addFailedAuthentication($username, getClientIpServer());
397
398
            // Case where user has initiated Google Auth
399
            // Return QR code
400
            echo prepareExchangedData(
401
                [
402
                    'value' => $userMfa['mfaData']['value'],
403
                    'user_admin' => isset($sessionAdmin) ? (int) $sessionAdmin : 0,
404
                    'initial_url' => isset($sessionUrl) === true ? $sessionUrl : '',
405
                    'pwd_attempts' => (int) $sessionPwdAttempts,
406
                    'error' => false,
407
                    'message' => $userMfa['mfaData']['message'],
408
                    'mfaStatus' => $userMfa['mfaData']['mfaStatus'],
409
                ],
410
                'encode'
411
            );
412
            return false;
413
        } elseif ($userMfa['duo_url_ready'] === true) {
414
            // Add failed authentication log
415
            addFailedAuthentication($username, getClientIpServer());
416
417
            // Case where user has initiated Duo Auth
418
            // Return the DUO redirect URL
419
            echo prepareExchangedData(
420
                [
421
                    'user_admin' => isset($sessionAdmin) ? (int) $sessionAdmin : 0,
422
                    'initial_url' => isset($sessionUrl) === true ? $sessionUrl : '',
423
                    'pwd_attempts' => (int) $sessionPwdAttempts,
424
                    'error' => false,
425
                    'message' => $userMfa['mfaData']['message'],
426
                    'duo_url_ready' => $userMfa['mfaData']['duo_url_ready'],
427
                    'duo_redirect_url' => $userMfa['mfaData']['duo_redirect_url'],
428
                    'mfaStatus' => $userMfa['mfaData']['mfaStatus'],
429
                ],
430
                'encode'
431
            );
432
            return false;
433
        }
434
    }
435
436
    // Can connect if
437
    // 1- no LDAP mode + user enabled + pw ok
438
    // 2- LDAP mode + user enabled + ldap connection ok + user is not admin
439
    // 3- LDAP mode + user enabled + pw ok + usre is admin
440
    // This in order to allow admin by default to connect even if LDAP is activated
441
    if (canUserGetLog(
442
            $SETTINGS,
443
            (int) $userInfo['disabled'],
444
            $username,
445
            $userLdap['ldapConnection']
446
        ) === true
447
    ) {
448
        $session->set('pwd_attempts', 0);
449
450
        // Check if any unsuccessfull login tries exist
451
        handleLoginAttempts(
452
            $userInfo['id'],
453
            $userInfo['login'],
454
            $userInfo['last_connexion'],
455
            $username,
456
            $SETTINGS,
457
        );
458
459
        // Avoid unlimited session.
460
        $max_time = isset($SETTINGS['maximum_session_expiration_time']) ? (int) $SETTINGS['maximum_session_expiration_time'] : 60;
461
        $session_time = min($dataReceived['duree_session'], $max_time);
462
        $lifetime = time() + ($session_time * 60);
463
464
        // Build user session - Extracted to separate function for readability
465
        $sessionData = buildUserSession($session, $userInfo, $username, $passwordClear, $SETTINGS, $lifetime);
466
        $old_key = $sessionData['old_key'];
467
        $returnKeys = $sessionData['returnKeys'];
468
        
469
        // Setup user roles and permissions - Extracted to separate function for readability
470
        $rolesDbUpdateData = setupUserRolesAndPermissions($session, $userInfo, $SETTINGS);
471
        
472
        // Perform post-login tasks - Extracted to separate function for readability
473
        $isNewExternalUser = $userLdap['user_initial_creation_through_external_ad'] === true 
474
            || $userOauth2['retExternalAD']['has_been_created'] === 1;
475
        
476
        // Merge roles DB update into returnKeys for performPostLoginTasks
477
        $returnKeys['roles_db_update'] = $rolesDbUpdateData;
478
        
479
        performPostLoginTasks(
480
            $session,
481
            $userInfo,
482
            $passwordClear,
483
            $SETTINGS,
484
            $dataReceived,
485
            $returnKeys,
486
            $isNewExternalUser
487
        );
488
        
489
        // send back the random key
490
        $return = $dataReceived['randomstring'];
491
        
492
        // Ensure Complexity levels are translated
493
        defineComplexity();
494
        echo prepareExchangedData(
495
            buildAuthResponse(
496
                $session,
497
                (string) $sessionUrl,
498
                (int) $sessionPwdAttempts,
499
                (string) $return,
500
                (array) $userInfo ?? [],
501
                true  // success
502
            ),
503
            'encode',
504
            $old_key
505
        );
506
    
507
        return true;
508
509
    } elseif ((int) $userInfo['disabled'] === 1) {
510
        // User and password is okay but account is locked
511
        echo prepareExchangedData(
512
            buildAuthResponse(
513
                $session,
514
                (string) $sessionUrl,
515
                0,
516
                (string) $return,
517
                (array) $userInfo ?? [],
518
                false,  // not success
519
                'user_is_locked',
520
                $lang->get('account_is_locked')
521
            ),
522
            'encode'
523
        );
524
        return false;
525
    }
526
527
    echo prepareExchangedData(
528
        buildAuthResponse(
529
            $session,
530
            (string) $sessionUrl,
531
            (int) $sessionPwdAttempts,
532
            (string) $return,
533
            (array) $userInfo ?? [],
534
            false,  // not success
535
            true,
536
            $lang->get('error_not_allowed_to_authenticate')
537
        ),
538
        'encode'
539
    );
540
    return false;
541
}
542
543
/**
544
 * Build authentication response array
545
 * Unified function to avoid code duplication in identifyUser()
546
 *
547
 * @param SessionManager $session
548
 * @param string $sessionUrl
549
 * @param int $sessionPwdAttempts
550
 * @param string $returnValue
551
 * @param array $userInfo
552
 * @param bool $success
553
 * @param string|bool $errorType
554
 * @param string $errorMessage
555
 * @return array
556
 */
557
function buildAuthResponse(
558
    $session,
559
    string $sessionUrl,
560
    int $sessionPwdAttempts,
561
    string $returnValue,
562
    array $userInfo,
563
    bool $success = true,
564
    $errorType = false,
565
    string $errorMessage = ''
566
): array {
567
    $antiXss = new AntiXSS();
568
    $session = SessionManager::getSession();
569
    
570
    // Base response (common to all responses)
571
    $response = [
572
        'value' => $returnValue,
573
        'user_id' => $session->get('user-id') !== null ? (int) $session->get('user-id') : '',
574
        'user_admin' => null !== $session->get('user-admin') ? (int) $session->get('user-admin') : 0,
575
        'initial_url' => $success ? $antiXss->xss_clean($sessionUrl) : $sessionUrl,
576
        'pwd_attempts' => $success ? 0 : $sessionPwdAttempts,
577
        'first_connection' => $session->get('user-validite_pw') === 0 ? true : false,
578
        'password_complexity' => TP_PW_COMPLEXITY[$session->get('user-pw_complexity')][1],
579
        'password_change_expected' => ($userInfo['special'] ?? '') === 'password_change_expected' ? true : false,
580
        'private_key_conform' => $session->get('user-id') !== null
581
            && empty($session->get('user-private_key')) === false
582
            && $session->get('user-private_key') !== 'none' ? true : false,
583
        'session_key' => $session->get('key'),
584
        'can_create_root_folder' => null !== $session->get('user-can_create_root_folder') ? (int) $session->get('user-can_create_root_folder') : '',
585
    ];
586
587
    // Add error or success specific fields
588
    if (!$success) {
589
        $response['error'] = $errorType === false ? true : $errorType;
590
        $response['message'] = $errorMessage;
591
    } else {
592
        // Success-specific fields
593
        $response['error'] = false;
594
        $response['message'] = $session->has('user-upgrade_needed') 
595
            && (int) $session->get('user-upgrade_needed') 
596
            && (int) $session->get('user-upgrade_needed') === 1 
597
                ? 'ask_for_otc' 
598
                : '';
599
        $response['upgrade_needed'] = isset($userInfo['upgrade_needed']) === true ? (int) $userInfo['upgrade_needed'] : 0;
600
        $response['special'] = isset($userInfo['special']) === true ? (int) $userInfo['special'] : 0;
601
        $response['split_view_mode'] = isset($userInfo['split_view_mode']) === true ? (int) $userInfo['split_view_mode'] : 0;
602
        $response['show_subfolders'] = isset($userInfo['show_subfolders']) === true ? (int) $userInfo['show_subfolders'] : 0;
603
        $response['validite_pw'] = $session->get('user-validite_pw') !== null ? $session->get('user-validite_pw') : '';
604
        $response['num_days_before_exp'] = $session->get('user-num_days_before_exp') !== null ? (int) $session->get('user-num_days_before_exp') : '';
605
    }
606
607
    return $response;
608
}
609
610
/**
611
 * Build and configure user session variables
612
 * Extracts session configuration logic from identifyUser()
613
 *
614
 * @param SessionManager $session
615
 * @param array $userInfo
616
 * @param string $username
617
 * @param string $passwordClear
618
 * @param array $SETTINGS
619
 * @param int $lifetime
620
 * @return array Returns encryption keys data
621
 */
622
function buildUserSession(
623
    $session,
624
    array $userInfo,
625
    string $username,
626
    string $passwordClear,
627
    array $SETTINGS,
628
    int $lifetime
629
): array {
630
    $session = SessionManager::getSession();
631
632
    // Save old key for response encryption
633
    $old_key = $session->get('key');
634
635
    // Good practice: reset PHPSESSID and key after successful authentication
636
    $session->migrate();
637
    $session->set('key', bin2hex(random_bytes(16)));
638
639
    // Save account in SESSION - Basic user info
640
    $session->set('user-login', stripslashes($username));
641
    $session->set('user-name', empty($userInfo['name']) === false ? stripslashes($userInfo['name']) : '');
642
    $session->set('user-lastname', empty($userInfo['lastname']) === false ? stripslashes($userInfo['lastname']) : '');
643
    $session->set('user-id', (int) $userInfo['id']);
644
    $session->set('user-admin', (int) $userInfo['admin']);
645
    $session->set('user-manager', (int) $userInfo['gestionnaire']);
646
    $session->set('user-can_manage_all_users', $userInfo['can_manage_all_users']);
647
    $session->set('user-read_only', $userInfo['read_only']);
648
    $session->set('user-last_pw_change', $userInfo['last_pw_change']);
649
    $session->set('user-last_pw', $userInfo['last_pw']);
650
    $session->set('user-force_relog', $userInfo['force-relog']);
651
    $session->set('user-can_create_root_folder', $userInfo['can_create_root_folder']);
652
    $session->set('user-email', $userInfo['email']);
653
    $session->set('user-avatar', $userInfo['avatar']);
654
    $session->set('user-avatar_thumb', $userInfo['avatar_thumb']);
655
    $session->set('user-upgrade_needed', $userInfo['upgrade_needed']);
656
    $session->set('user-is_ready_for_usage', $userInfo['is_ready_for_usage']);
657
    $session->set('user-personal_folder_enabled', $userInfo['personal_folder']);
658
    $session->set(
659
        'user-tree_load_strategy',
660
        (isset($userInfo['treeloadstrategy']) === false || empty($userInfo['treeloadstrategy']) === true) ? 'full' : $userInfo['treeloadstrategy']
661
    );
662
    $session->set(
663
        'user-split_view_mode',
664
        (isset($userInfo['split_view_mode']) === false || empty($userInfo['split_view_mode']) === true) ? 0 : (int) $userInfo['split_view_mode']
665
    );
666
    $session->set(
667
        'user-show_subfolders',
668
        (isset($userInfo['show_subfolders']) === false || empty($userInfo['show_subfolders']) === true) ? 0 : (int) $userInfo['show_subfolders']
669
    );
670
    $session->set('user-language', $userInfo['user_language']);
671
    $session->set('user-timezone', $userInfo['usertimezone']);
672
    $session->set('user-keys_recovery_time', $userInfo['keys_recovery_time']);
673
    
674
    // Manage session expiration
675
    $session->set('user-session_duration', (int) $lifetime);
676
677
    // User signature keys
678
    $returnKeys = prepareUserEncryptionKeys($userInfo, $passwordClear, $SETTINGS);
679
    $session->set('user-public_key', $returnKeys['public_key']);
680
    
681
    // Did user has his AD password changed
682
    if (null !== $session->get('user-private_key_recovered') && $session->get('user-private_key_recovered') === true) {
683
        $session->set('user-private_key', $session->get('user-private_key'));
684
    } else {
685
        $session->set('user-private_key', $returnKeys['private_key_clear']);
686
    }
687
688
    // API key
689
    $session->set(
690
        'user-api_key',
691
        empty($userInfo['api_key']) === false ? base64_decode(decryptUserObjectKey($userInfo['api_key'], $returnKeys['private_key_clear'])) : '',
692
    );
693
    
694
    $session->set('user-special', $userInfo['special']);
695
    $session->set('user-auth_type', $userInfo['auth_type']);
696
697
    // Check feedback regarding user password validity
698
    $return = checkUserPasswordValidity(
699
        $userInfo,
700
        (int) $session->get('user-num_days_before_exp'),
701
        (int) $session->get('user-last_pw_change'),
702
        $SETTINGS
703
    );
704
    $session->set('user-validite_pw', $return['validite_pw']);
705
    $session->set('user-last_pw_change', $return['last_pw_change']);
706
    $session->set('user-num_days_before_exp', $return['numDaysBeforePwExpiration']);
707
    $session->set('user-force_relog', $return['user_force_relog']);
708
    
709
    $session->set('user-last_connection', empty($userInfo['last_connexion']) === false ? (int) $userInfo['last_connexion'] : (int) time());
710
    $session->set('user-latest_items', empty($userInfo['latest_items']) === false ? explode(';', $userInfo['latest_items']) : []);
711
    $session->set('user-favorites', empty($userInfo['favourites']) === false ? explode(';', $userInfo['favourites']) : []);
712
    $session->set('user-accessible_folders', empty($userInfo['groupes_visibles']) === false ? explode(';', $userInfo['groupes_visibles']) : []);
713
    $session->set('user-no_access_folders', empty($userInfo['groupes_interdits']) === false ? explode(';', $userInfo['groupes_interdits']) : []);
714
715
    return [
716
        'old_key' => $old_key,
717
        'returnKeys' => $returnKeys,
718
    ];
719
}
720
721
/**
722
 * Perform post-login tasks
723
 * Handles migration, DB updates, rights configuration, cache and email notifications
724
 *
725
 * @param SessionManager $session
726
 * @param array $userInfo
727
 * @param string $passwordClear
728
 * @param array $SETTINGS
729
 * @param array $dataReceived
730
 * @param array $returnKeys
731
 * @param bool $isNewExternalUser
732
 * @return void
733
 */
734
function performPostLoginTasks(
735
    $session,
736
    array $userInfo,
737
    string $passwordClear,
738
    array $SETTINGS,
739
    array $dataReceived,
740
    array $returnKeys,
741
    bool $isNewExternalUser
742
): void {
743
    $session = SessionManager::getSession();
744
745
    // Version 3.1.5 - Migrate personal items password to similar encryption protocol as public ones.
746
    checkAndMigratePersonalItems($session->get('user-id'), $session->get('user-private_key'), $passwordClear);
747
748
    // 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