Issues (29)

Security Analysis    no vulnerabilities found

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  Header Injection
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

sources/identify.php (3 issues)

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