Issues (48)

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 (4 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-2026 Teampass.net
28
 * @license   GPL-3.0
29
 * @see       https://www.teampass.net
30
 */
31
32
use voku\helper\AntiXSS;
33
use TeampassClasses\SessionManager\SessionManager;
34
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
35
use TeampassClasses\Language\Language;
36
use TeampassClasses\PerformChecks\PerformChecks;
37
use TeampassClasses\ConfigManager\ConfigManager;
38
use TeampassClasses\NestedTree\NestedTree;
39
use TeampassClasses\PasswordManager\PasswordManager;
40
use Duo\DuoUniversal\Client;
41
use Duo\DuoUniversal\DuoException;
42
use RobThree\Auth\TwoFactorAuth;
43
use TeampassClasses\LdapExtra\LdapExtra;
44
use TeampassClasses\LdapExtra\OpenLdapExtra;
45
use TeampassClasses\LdapExtra\ActiveDirectoryExtra;
46
use TeampassClasses\OAuth2Controller\OAuth2Controller;
47
48
// Load functions
49
require_once 'main.functions.php';
50
51
// init
52
loadClasses('DB');
53
$session = SessionManager::getSession();
54
$request = SymfonyRequest::createFromGlobals();
55
$lang = new Language($session->get('user-language') ?? 'english');
56
57
// Load config
58
$configManager = new ConfigManager();
59
$SETTINGS = $configManager->getAllSettings();
60
61
// Define Timezone
62
date_default_timezone_set($SETTINGS['timezone'] ?? 'UTC');
63
64
// Set header properties
65
header('Content-type: text/html; charset=utf-8');
66
header('Cache-Control: no-cache, no-store, must-revalidate');
67
error_reporting(E_ERROR);
68
69
// --------------------------------- //
70
71
// Prepare POST variables
72
$post_type = filter_input(INPUT_POST, 'type', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
73
$post_login = filter_input(INPUT_POST, 'login', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
74
$post_data = filter_input(INPUT_POST, 'data', FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_FLAG_NO_ENCODE_QUOTES);
75
76
if ($post_type === 'identify_user') {
77
    //--------
78
    // NORMAL IDENTICATION STEP
79
    //--------
80
81
    // Ensure Complexity levels are translated
82
    defineComplexity();
83
84
    // Identify the user through Teampass process
85
    identifyUser($post_data, $SETTINGS);
86
87
    // ---
88
    // ---
89
    // ---
90
} elseif ($post_type === 'get2FAMethods') {
91
    //--------
92
    // Get MFA methods
93
    //--------
94
    //
95
96
    // Encrypt data to return
97
    echo json_encode([
98
        'ret' => prepareExchangedData(
99
            [
100
                'agses' => isKeyExistingAndEqual('agses_authentication_enabled', 1, $SETTINGS) === true ? true : false,
101
                'google' => isKeyExistingAndEqual('google_authentication', 1, $SETTINGS) === true ? true : false,
102
                'yubico' => isKeyExistingAndEqual('yubico_authentication', 1, $SETTINGS) === true ? true : false,
103
                'duo' => isKeyExistingAndEqual('duo', 1, $SETTINGS) === true ? true : false,
104
            ],
105
            'encode'
106
        ),
107
        'key' => $session->get('key'),
108
    ]);
109
    return false;
110
} elseif ($post_type === 'initiateSSOLogin') {
111
    //--------
112
    // Do initiateSSOLogin
113
    //--------
114
    //
115
116
    // Création d'une instance du contrôleur
117
    $OAuth2 = new OAuth2Controller($SETTINGS);
118
119
    // Redirection vers Azure pour l'authentification
120
    $OAuth2->redirect();
121
122
    // Encrypt data to return
123
    echo json_encode([
124
        'key' => $session->get('key'),
125
    ]);
126
    return false;
127
}
128
129
/**
130
 * Complete authentication of user through Teampass
131
 *
132
 * @param string $sentData Credentials
133
 * @param array $SETTINGS Teampass settings
134
 *
135
 * @return bool
136
 */
137
function identifyUser(string $sentData, array $SETTINGS): bool
138
{
139
    $session = SessionManager::getSession();
140
    $request = SymfonyRequest::createFromGlobals();
141
    $lang = new Language($session->get('user-language') ?? 'english');
142
    
143
    // Prepare GET variables
144
    $sessionAdmin = $session->get('user-admin');
145
    $sessionPwdAttempts = $session->get('pwd_attempts');
146
    $sessionUrl = $session->get('user-initial_url');
147
    $server = [];
148
    $server['PHP_AUTH_USER'] =  $request->getUser();
149
    $server['PHP_AUTH_PW'] = $request->getPassword();
150
    
151
    // decrypt and retreive data in JSON format
152
    if ($session->get('key') === null) {
153
        $dataReceived = $sentData;
154
    } else {
155
        $dataReceived = prepareExchangedData(
156
            $sentData,
157
            'decode',
158
            $session->get('key')
159
        );
160
    }
161
162
    // Sanitize input data
163
    $toClean = [
164
        'login' => 'trim|escape',
165
    ];
166
    $dataReceived = sanitizeData($dataReceived, $toClean);
167
168
    // Base64 decode sensitive data
169
    if (isset($dataReceived['pw'])) {
170
        $dataReceived['pw'] = base64_decode($dataReceived['pw']);
171
    }
172
    
173
    // Check if Duo auth is in progress and pass the pw and login back to the standard login process
174
    if(
175
        isKeyExistingAndEqual('duo', 1, $SETTINGS) === true
176
        && $dataReceived['user_2fa_selection'] === 'duo'
177
        && $session->get('user-duo_status') === 'IN_PROGRESS'
178
        && !empty($dataReceived['duo_state'])
179
    ){
180
        $key = hash('sha256', $dataReceived['duo_state']);
181
        $iv = substr(hash('sha256', $dataReceived['duo_state']), 0, 16);
182
        $duo_data_dec = openssl_decrypt(base64_decode($session->get('user-duo_data')), 'AES-256-CBC', $key, 0, $iv);
183
        // Clear the data from the Duo process to continue clean with the standard login process
184
        $session->set('user-duo_data','');
185
        if($duo_data_dec === false) {
186
            // Add failed authentication log
187
            addFailedAuthentication(filter_var($dataReceived['login'], FILTER_SANITIZE_FULL_SPECIAL_CHARS), getClientIpServer());
188
189
            echo prepareExchangedData(
190
                [
191
                    'error' => true,
192
                    'message' => $lang->get('duo_error_decrypt'),
193
                ],
194
                'encode'
195
            );
196
            return false;
197
        }
198
        $duo_data = unserialize($duo_data_dec);
199
        $dataReceived['pw'] = $duo_data['duo_pwd'];
200
        $dataReceived['login'] = $duo_data['duo_login'];
201
    }
202
203
    if(isset($dataReceived['pw']) === false || isset($dataReceived['login']) === false) {
204
        echo json_encode([
205
            'data' => prepareExchangedData(
206
                [
207
                    'error' => true,
208
                    'message' => $lang->get('ga_enter_credentials'),
209
                ],
210
                'encode'
211
            ),
212
            'key' => $session->get('key')
213
        ]);
214
        return false;
215
    }
216
217
    // prepare variables
218
    // IMPORTANT: Do NOT sanitize passwords - they should be treated as opaque binary data
219
    // Sanitization was removed to fix authentication issues (see upgrade 3.1.5.10)
220
    $userCredentials = identifyGetUserCredentials(
221
        $SETTINGS,
222
        (string) $server['PHP_AUTH_USER'],
223
        (string) $server['PHP_AUTH_PW'],
224
        (string) $dataReceived['pw'], // No more sanitization
225
        (string) filter_var($dataReceived['login'], FILTER_SANITIZE_FULL_SPECIAL_CHARS)
226
    );
227
    $username = $userCredentials['username'];
228
    $passwordClear = $userCredentials['passwordClear'];
229
    
230
    // DO initial checks
231
    $userInitialData = identifyDoInitialChecks(
232
        $SETTINGS,
233
        (int) $sessionPwdAttempts,
234
        (string) $username,
235
        (int) $sessionAdmin,
236
        (string) $sessionUrl,
237
        (string) filter_var($dataReceived['user_2fa_selection'], FILTER_SANITIZE_FULL_SPECIAL_CHARS)
238
    );
239
240
    // if user doesn't exist in Teampass then return error
241
    if ($userInitialData['error'] === true) {
242
        // Add log on error unless skip_anti_bruteforce flag is set to true
243
        if (empty($userInitialData['skip_anti_bruteforce'])
244
            || !$userInitialData['skip_anti_bruteforce']) {
245
246
            // Add failed authentication log
247
            addFailedAuthentication($username, getClientIpServer());
248
        }
249
250
        echo prepareExchangedData(
251
            $userInitialData['array'],
252
            'encode'
253
        );
254
        return false;
255
    }
256
257
    $userInfo = $userInitialData['userInfo'] + $dataReceived;
258
    $return = '';
259
260
    // Check if LDAP is enabled and user is in AD
261
    $userLdap = identifyDoLDAPChecks(
262
        $SETTINGS,
263
        $userInfo,
264
        (string) $username,
265
        (string) $passwordClear,
266
        (int) $sessionAdmin,
267
        (string) $sessionUrl,
268
        (int) $sessionPwdAttempts
269
    );
270
    if ($userLdap['error'] === true) {
271
        // Log LDAP/AD failed authentication into teampass_log_system
272
        logEvents(
273
            $SETTINGS,
274
            'failed_auth',
275
            'password_is_not_correct',
276
            '',
277
            stripslashes($username),
278
            stripslashes($username)
279
        );
280
281
        // Anti-bruteforce log
282
        addFailedAuthentication($username, getClientIpServer());
283
284
        echo prepareExchangedData(
285
            $userLdap['array'],
286
            'encode'
287
        );
288
        return false;
289
    }
290
291
    if (isset($userLdap['user_info']) === true && (int) $userLdap['user_info']['has_been_created'] === 1) {
292
        // Add failed authentication log
293
        addFailedAuthentication($username, getClientIpServer());
294
295
        echo json_encode([
296
            'data' => prepareExchangedData(
297
                [
298
                    'error' => true,
299
                    'message' => '',
300
                    'extra' => 'ad_user_created',
301
                ],
302
                'encode'
303
            ),
304
            'key' => $session->get('key')
305
        ]);
306
        return false;
307
    }
308
    
309
    // Is oauth2 user exists?
310
    $userOauth2 = checkOauth2User(
311
        (array) $SETTINGS,
312
        (array) $userInfo,
313
        (string) $username,
314
        (string) $passwordClear,
315
        (int) $userLdap['user_info']['has_been_created']
316
    );
317
    if ($userOauth2['error'] === true) {
318
        $session->set('userOauth2Info', '');
319
320
        // Add failed authentication log
321
        if ($userOauth2['no_log_event'] !== true) {
322
            addFailedAuthentication($username, getClientIpServer());
323
        }
324
325
        // deepcode ignore ServerLeak: File and path are secured directly inside the function decryptFile()        
326
        echo prepareExchangedData(
327
            [
328
                'error' => true,
329
                'message' => $lang->get($userOauth2['message']),
330
                'extra' => 'oauth2_user_not_found',
331
            ],
332
            'encode'
333
        );
334
        return false;
335
    }
336
337
    // Check user and password
338
    $authResult = checkCredentials($passwordClear, $userInfo);
339
    if ($userLdap['userPasswordVerified'] === false && $userOauth2['userPasswordVerified'] === false && $authResult['authenticated'] !== true) {
340
        // Log failure (wrong password on existing account) into teampass_log_system
341
        logEvents($SETTINGS, 'failed_auth', 'password_is_not_correct', '', stripslashes($username), stripslashes($username));
342
        // Add failed authentication log
343
        addFailedAuthentication($username, getClientIpServer());
344
        echo prepareExchangedData(
345
            [
346
                'value' => '',
347
                'error' => true,
348
                'message' => $lang->get('error_bad_credentials'),
349
            ],
350
            'encode'
351
        );
352
        return false;
353
    }
354
    
355
    // If password was migrated, then update private key
356
    if ($authResult['password_migrated'] === true) {
357
        $userInfo['private_key'] = $authResult['private_key_reencrypted'];
358
    }
359
    
360
    // Check if MFA is required
361
    if ((
362
        isOneVarOfArrayEqualToValue(
363
            [
364
                (int) $SETTINGS['yubico_authentication'],
365
                (int) $SETTINGS['google_authentication'],
366
                (int) $SETTINGS['duo']
367
            ],
368
            1
369
        ) === true)
370
        && (((int) $userInfo['admin'] !== 1 && (int) $userInfo['mfa_enabled'] === 1 && $userInfo['mfa_auth_requested_roles'] === true)
371
        || ((int) $SETTINGS['admin_2fa_required'] === 1 && (int) $userInfo['admin'] === 1))
372
    ) {
373
        // Check user against MFA method if selected
374
        $userMfa = identifyDoMFAChecks(
375
            $SETTINGS,
376
            $userInfo,
377
            $dataReceived,
378
            $userInitialData,
379
            (string) $username
380
        );
381
        if ($userMfa['error'] === true) {
382
            // Add failed authentication log
383
            addFailedAuthentication($username, getClientIpServer());
384
385
            echo prepareExchangedData(
386
                [
387
                    'error' => true,
388
                    'message' => $userMfa['mfaData']['message'],
389
                    'mfaStatus' => $userMfa['mfaData']['mfaStatus'],
390
                ],
391
                'encode'
392
            );
393
            return false;
394
        } elseif ($userMfa['mfaQRCodeInfos'] === true) {
395
            // Add failed authentication log
396
            addFailedAuthentication($username, getClientIpServer());
397
398
            // Case where user has initiated Google Auth
399
            // Return QR code
400
            echo prepareExchangedData(
401
                [
402
                    'value' => $userMfa['mfaData']['value'],
403
                    'user_admin' => isset($sessionAdmin) ? (int) $sessionAdmin : 0,
404
                    'initial_url' => isset($sessionUrl) === true ? $sessionUrl : '',
405
                    'pwd_attempts' => (int) $sessionPwdAttempts,
406
                    'error' => false,
407
                    'message' => $userMfa['mfaData']['message'],
408
                    'mfaStatus' => $userMfa['mfaData']['mfaStatus'],
409
                ],
410
                'encode'
411
            );
412
            return false;
413
        } elseif ($userMfa['duo_url_ready'] === true) {
414
            // Add failed authentication log
415
            addFailedAuthentication($username, getClientIpServer());
416
417
            // Case where user has initiated Duo Auth
418
            // Return the DUO redirect URL
419
            echo prepareExchangedData(
420
                [
421
                    'user_admin' => isset($sessionAdmin) ? (int) $sessionAdmin : 0,
422
                    'initial_url' => isset($sessionUrl) === true ? $sessionUrl : '',
423
                    'pwd_attempts' => (int) $sessionPwdAttempts,
424
                    'error' => false,
425
                    'message' => $userMfa['mfaData']['message'],
426
                    'duo_url_ready' => $userMfa['mfaData']['duo_url_ready'],
427
                    'duo_redirect_url' => $userMfa['mfaData']['duo_redirect_url'],
428
                    'mfaStatus' => $userMfa['mfaData']['mfaStatus'],
429
                ],
430
                'encode'
431
            );
432
            return false;
433
        }
434
    }
435
436
    // Can connect if
437
    // 1- no LDAP mode + user enabled + pw ok
438
    // 2- LDAP mode + user enabled + ldap connection ok + user is not admin
439
    // 3- LDAP mode + user enabled + pw ok + usre is admin
440
    // This in order to allow admin by default to connect even if LDAP is activated
441
    if (canUserGetLog(
442
            $SETTINGS,
443
            (int) $userInfo['disabled'],
444
            $username,
445
            $userLdap['ldapConnection']
446
        ) === true
447
    ) {
448
        $session->set('pwd_attempts', 0);
449
450
        // Check if any unsuccessfull login tries exist
451
        handleLoginAttempts(
452
            $userInfo['id'],
453
            $userInfo['login'],
454
            $userInfo['last_connexion'],
455
            $username,
456
            $SETTINGS,
457
        );
458
459
        // Avoid unlimited session.
460
        $max_time = isset($SETTINGS['maximum_session_expiration_time']) ? (int) $SETTINGS['maximum_session_expiration_time'] : 60;
461
        $session_time = min($dataReceived['duree_session'], $max_time);
462
        $lifetime = time() + ($session_time * 60);
463
464
        // Build user session - Extracted to separate function for readability
465
        $sessionData = buildUserSession($session, $userInfo, $username, $passwordClear, $SETTINGS, $lifetime);
466
        $old_key = $sessionData['old_key'];
467
        $returnKeys = $sessionData['returnKeys'];
468
        
469
        // Setup user roles and permissions - Extracted to separate function for readability
470
        $rolesDbUpdateData = setupUserRolesAndPermissions($session, $userInfo, $SETTINGS);
471
        
472
        // Perform post-login tasks - Extracted to separate function for readability
473
        $isNewExternalUser = $userLdap['user_initial_creation_through_external_ad'] === true 
474
            || $userOauth2['retExternalAD']['has_been_created'] === 1;
475
        
476
        // Merge roles DB update into returnKeys for performPostLoginTasks
477
        $returnKeys['roles_db_update'] = $rolesDbUpdateData;
478
        
479
        performPostLoginTasks(
480
            $session,
481
            $userInfo,
482
            $passwordClear,
483
            $SETTINGS,
484
            $dataReceived,
485
            $returnKeys,
486
            $isNewExternalUser
487
        );
488
        
489
        // send back the random key
490
        $return = $dataReceived['randomstring'];
491
        
492
        // Ensure Complexity levels are translated
493
        defineComplexity();
494
        echo prepareExchangedData(
495
            buildAuthResponse(
496
                $session,
497
                (string) $sessionUrl,
498
                (int) $sessionPwdAttempts,
499
                (string) $return,
500
                (array) $userInfo ?? [],
501
                true  // success
502
            ),
503
            'encode',
504
            $old_key
505
        );
506
    
507
        return true;
508
509
    } elseif ((int) $userInfo['disabled'] === 1) {
510
        // User and password is okay but account is locked
511
        echo prepareExchangedData(
512
            buildAuthResponse(
513
                $session,
514
                (string) $sessionUrl,
515
                0,
516
                (string) $return,
517
                (array) $userInfo ?? [],
518
                false,  // not success
519
                'user_is_locked',
520
                $lang->get('account_is_locked')
521
            ),
522
            'encode'
523
        );
524
        return false;
525
    }
526
527
    echo prepareExchangedData(
528
        buildAuthResponse(
529
            $session,
530
            (string) $sessionUrl,
531
            (int) $sessionPwdAttempts,
532
            (string) $return,
533
            (array) $userInfo ?? [],
534
            false,  // not success
535
            true,
536
            $lang->get('error_not_allowed_to_authenticate')
537
        ),
538
        'encode'
539
    );
540
    return false;
541
}
542
543
/**
544
 * Build authentication response array
545
 * Unified function to avoid code duplication in identifyUser()
546
 *
547
 * @param SessionManager $session
548
 * @param string $sessionUrl
549
 * @param int $sessionPwdAttempts
550
 * @param string $returnValue
551
 * @param array $userInfo
552
 * @param bool $success
553
 * @param string|bool $errorType
554
 * @param string $errorMessage
555
 * @return array
556
 */
557
function buildAuthResponse(
558
    $session,
559
    string $sessionUrl,
560
    int $sessionPwdAttempts,
561
    string $returnValue,
562
    array $userInfo,
563
    bool $success = true,
564
    $errorType = false,
565
    string $errorMessage = ''
566
): array {
567
    $antiXss = new AntiXSS();
568
    $session = SessionManager::getSession();
569
    
570
    // Base response (common to all responses)
571
    $response = [
572
        'value' => $returnValue,
573
        'user_id' => $session->get('user-id') !== null ? (int) $session->get('user-id') : '',
574
        'user_admin' => null !== $session->get('user-admin') ? (int) $session->get('user-admin') : 0,
575
        'initial_url' => $success ? $antiXss->xss_clean($sessionUrl) : $sessionUrl,
576
        'pwd_attempts' => $success ? 0 : $sessionPwdAttempts,
577
        'first_connection' => $session->get('user-validite_pw') === 0 ? true : false,
578
        'password_complexity' => TP_PW_COMPLEXITY[$session->get('user-pw_complexity')][1],
579
        'password_change_expected' => ($userInfo['special'] ?? '') === 'password_change_expected' ? true : false,
580
        'private_key_conform' => $session->get('user-id') !== null
581
            && empty($session->get('user-private_key')) === false
582
            && $session->get('user-private_key') !== 'none' ? true : false,
583
        'session_key' => $session->get('key'),
584
        'can_create_root_folder' => null !== $session->get('user-can_create_root_folder') ? (int) $session->get('user-can_create_root_folder') : '',
585
    ];
586
587
    // Add error or success specific fields
588
    if (!$success) {
589
        $response['error'] = $errorType === false ? true : $errorType;
590
        $response['message'] = $errorMessage;
591
    } else {
592
        // Success-specific fields
593
        $response['error'] = false;
594
        $response['message'] = $session->has('user-upgrade_needed') 
595
            && (int) $session->get('user-upgrade_needed') 
596
            && (int) $session->get('user-upgrade_needed') === 1 
597
                ? 'ask_for_otc' 
598
                : '';
599
        $response['upgrade_needed'] = isset($userInfo['upgrade_needed']) === true ? (int) $userInfo['upgrade_needed'] : 0;
600
        $response['special'] = isset($userInfo['special']) === true ? (int) $userInfo['special'] : 0;
601
        $response['split_view_mode'] = isset($userInfo['split_view_mode']) === true ? (int) $userInfo['split_view_mode'] : 0;
602
        $response['show_subfolders'] = isset($userInfo['show_subfolders']) === true ? (int) $userInfo['show_subfolders'] : 0;
603
        $response['validite_pw'] = $session->get('user-validite_pw') !== null ? $session->get('user-validite_pw') : '';
604
        $response['num_days_before_exp'] = $session->get('user-num_days_before_exp') !== null ? (int) $session->get('user-num_days_before_exp') : '';
605
    }
606
607
    return $response;
608
}
609
610
/**
611
 * Build and configure user session variables
612
 * Extracts session configuration logic from identifyUser()
613
 *
614
 * @param SessionManager $session
615
 * @param array $userInfo
616
 * @param string $username
617
 * @param string $passwordClear
618
 * @param array $SETTINGS
619
 * @param int $lifetime
620
 * @return array Returns encryption keys data
621
 */
622
function buildUserSession(
623
    $session,
624
    array $userInfo,
625
    string $username,
626
    string $passwordClear,
627
    array $SETTINGS,
628
    int $lifetime
629
): array {
630
    $session = SessionManager::getSession();
631
632
    // Save old key for response encryption
633
    $old_key = $session->get('key');
634
635
    // Good practice: reset PHPSESSID and key after successful authentication
636
    $session->migrate();
637
    $session->set('key', bin2hex(random_bytes(16)));
638
639
    // Save account in SESSION - Basic user info
640
    $session->set('user-login', stripslashes($username));
641
    $session->set('user-name', empty($userInfo['name']) === false ? stripslashes($userInfo['name']) : '');
642
    $session->set('user-lastname', empty($userInfo['lastname']) === false ? stripslashes($userInfo['lastname']) : '');
643
    $session->set('user-id', (int) $userInfo['id']);
644
    $session->set('user-admin', (int) $userInfo['admin']);
645
    $session->set('user-manager', (int) $userInfo['gestionnaire']);
646
    $session->set('user-can_manage_all_users', $userInfo['can_manage_all_users']);
647
    $session->set('user-read_only', $userInfo['read_only']);
648
    $session->set('user-last_pw_change', $userInfo['last_pw_change']);
649
    $session->set('user-last_pw', $userInfo['last_pw']);
650
    $session->set('user-force_relog', $userInfo['force-relog']);
651
    $session->set('user-can_create_root_folder', $userInfo['can_create_root_folder']);
652
    $session->set('user-email', $userInfo['email']);
653
    $session->set('user-avatar', $userInfo['avatar']);
654
    $session->set('user-avatar_thumb', $userInfo['avatar_thumb']);
655
    $session->set('user-upgrade_needed', $userInfo['upgrade_needed']);
656
    $session->set('user-is_ready_for_usage', $userInfo['is_ready_for_usage']);
657
    $session->set('user-personal_folder_enabled', $userInfo['personal_folder']);
658
    $session->set(
659
        'user-tree_load_strategy',
660
        (isset($userInfo['treeloadstrategy']) === false || empty($userInfo['treeloadstrategy']) === true) ? 'full' : $userInfo['treeloadstrategy']
661
    );
662
    $session->set(
663
        'user-split_view_mode',
664
        (isset($userInfo['split_view_mode']) === false || empty($userInfo['split_view_mode']) === true) ? 0 : (int) $userInfo['split_view_mode']
665
    );
666
    $session->set(
667
        'user-show_subfolders',
668
        (isset($userInfo['show_subfolders']) === false || empty($userInfo['show_subfolders']) === true) ? 0 : (int) $userInfo['show_subfolders']
669
    );
670
    $session->set('user-language', $userInfo['user_language']);
671
    $session->set('user-timezone', $userInfo['usertimezone']);
672
    $session->set('user-keys_recovery_time', $userInfo['keys_recovery_time']);
673
    
674
    // Manage session expiration
675
    $session->set('user-session_duration', (int) $lifetime);
676
677
    // User signature keys
678
    $returnKeys = prepareUserEncryptionKeys($userInfo, $passwordClear, $SETTINGS);
679
    $session->set('user-public_key', $returnKeys['public_key']);
680
    
681
    // Did user has his AD password changed
682
    if (null !== $session->get('user-private_key_recovered') && $session->get('user-private_key_recovered') === true) {
683
        $session->set('user-private_key', $session->get('user-private_key'));
684
    } else {
685
        $session->set('user-private_key', $returnKeys['private_key_clear']);
686
    }
687
688
    // API key - use recovered private key from session if available, otherwise from returnKeys
689
    $privateKeyForApiDecrypt = !empty($returnKeys['private_key_clear']) ? $returnKeys['private_key_clear'] : $session->get('user-private_key');
690
    $session->set(
691
        'user-api_key',
692
        empty($userInfo['api_key']) === false && !empty($privateKeyForApiDecrypt) ? base64_decode(decryptUserObjectKey($userInfo['api_key'], $privateKeyForApiDecrypt)) : '',
693
    );
694
    
695
    $session->set('user-special', $userInfo['special']);
696
    $session->set('user-auth_type', $userInfo['auth_type']);
697
698
    // Check feedback regarding user password validity
699
    $return = checkUserPasswordValidity(
700
        $userInfo,
701
        (int) $session->get('user-num_days_before_exp'),
702
        (int) $session->get('user-last_pw_change'),
703
        $SETTINGS
704
    );
705
    $session->set('user-validite_pw', $return['validite_pw']);
706
    $session->set('user-last_pw_change', $return['last_pw_change']);
707
    $session->set('user-num_days_before_exp', $return['numDaysBeforePwExpiration']);
708
    $session->set('user-force_relog', $return['user_force_relog']);
709
    
710
    $session->set('user-last_connection', empty($userInfo['last_connexion']) === false ? (int) $userInfo['last_connexion'] : (int) time());
711
    $session->set('user-latest_items', empty($userInfo['latest_items']) === false ? explode(';', $userInfo['latest_items']) : []);
712
    $session->set('user-favorites', empty($userInfo['favourites']) === false ? explode(';', $userInfo['favourites']) : []);
713
    $session->set('user-accessible_folders', empty($userInfo['groupes_visibles']) === false ? explode(';', $userInfo['groupes_visibles']) : []);
714
    $session->set('user-no_access_folders', empty($userInfo['groupes_interdits']) === false ? explode(';', $userInfo['groupes_interdits']) : []);
715
716
    return [
717
        'old_key' => $old_key,
718
        'returnKeys' => $returnKeys,
719
    ];
720
}
721
722
/**
723
 * Perform post-login tasks
724
 * Handles migration, DB updates, rights configuration, cache and email notifications
725
 *
726
 * @param SessionManager $session
727
 * @param array $userInfo
728
 * @param string $passwordClear
729
 * @param array $SETTINGS
730
 * @param array $dataReceived
731
 * @param array $returnKeys
732
 * @param bool $isNewExternalUser
733
 * @return void
734
 */
735
function performPostLoginTasks(
736
    $session,
737
    array $userInfo,
738
    string $passwordClear,
739
    array $SETTINGS,
740
    array $dataReceived,
741
    array $returnKeys,
742
    bool $isNewExternalUser
743
): void {
744
    $session = SessionManager::getSession();
745
746
    // Version 3.1.5 - Migrate personal items password to similar encryption protocol as public ones.
747
    checkAndMigratePersonalItems($session->get('user-id'), $session->get('user-private_key'), $passwordClear);
748
749
    // Version 3.1.6 - Trigger forced phpseclib v3 migration if enabled
750
    triggerPhpseclibV3MigrationOnLogin(
751
        (int) $session->get('user-id'),
752
        $session->get('user-private_key'),
753
        $passwordClear
754
    );
755
756
    // Set some settings
757
    $SETTINGS['update_needed'] = '';
758
759
    // Update table - Final user update in database
760
    $finalUpdateData = [
761
        'key_tempo' => $session->get('key'),
762
        'key_tempo_created_at' => time(),
763
        'last_connexion' => time(),
764
        'timestamp' => time(),
765
        'disabled' => 0,
766
        'session_end' => $session->get('user-session_duration'),
767
        'user_ip' => $dataReceived['client'],
768
    ];
769
770
    // Merge encryption keys update if needed
771
    if (!empty($returnKeys['update_keys_in_db'])) {
772
        $finalUpdateData = array_merge($finalUpdateData, $returnKeys['update_keys_in_db']);
773
    }
774
    
775
    // Merge role-based permissions update if needed
776
    if (!empty($returnKeys['roles_db_update'])) {
777
        $finalUpdateData = array_merge($finalUpdateData, $returnKeys['roles_db_update']);
778
    }
779
780
    DB::update(
781
        prefixTable('users'),
782
        $finalUpdateData,
783
        'id=%i',
784
        $userInfo['id']
785
    );
786
    
787
    // Get user's rights
788
    if ($isNewExternalUser) {
789
        // is new LDAP/OAuth2 user. Show only his personal folder
790
        if ($SETTINGS['enable_pf_feature'] === '1') {
791
            $session->set('user-personal_visible_folders', [$userInfo['id']]);
792
            $session->set('user-personal_folders', [$userInfo['id']]);
793
        } else {
794
            $session->set('user-personal_visible_folders', []);
795
            $session->set('user-personal_folders', []);
796
        }
797
        $session->set('user-roles_array', []);
798
        $session->set('user-read_only_folders', []);
799
        $session->set('user-list_folders_limited', []);
800
        $session->set('system-list_folders_editable_by_role', []);
801
        $session->set('system-list_restricted_folders_for_items', []);
802
        $session->set('user-nb_folders', 1);
803
        $session->set('user-nb_roles', 1);
804
    } else {
805
        identifyUserRights(
806
            $userInfo['groupes_visibles'],
807
            $session->get('user-no_access_folders'),
808
            $userInfo['admin'],
809
            $userInfo['fonction_id'],
810
            $SETTINGS
811
        );
812
    }
813
    
814
    // Get some more elements
815
    $session->set('system-screen_height', $dataReceived['screenHeight']);
816
817
    // Get cache tree info
818
    $cacheTreeData = DB::queryFirstRow(
819
        'SELECT visible_folders
820
        FROM ' . prefixTable('cache_tree') . '
821
        WHERE user_id=%i',
822
        (int) $session->get('user-id')
823
    );
824
    if (DB::count() > 0 && empty($cacheTreeData['visible_folders']) === true) {
825
        $session->set('user-cache_tree', '');
826
        // Prepare new task
827
        DB::insert(
828
            prefixTable('background_tasks'),
829
            array(
830
                'created_at' => time(),
831
                'process_type' => 'user_build_cache_tree',
832
                'arguments' => json_encode([
833
                    'user_id' => (int) $session->get('user-id'),
834
                ], JSON_HEX_QUOT | JSON_HEX_TAG),
835
                'updated_at' => null,
836
                'finished_at' => null,
837
                'output' => null,
838
            )
839
        );
840
    } else {
841
        $session->set('user-cache_tree', $cacheTreeData['visible_folders']);
842
    }
843
844
    // Send email notification if enabled
845
    $lang = new Language($session->get('user-language') ?? 'english');
846
    if (isKeyExistingAndEqual('enable_send_email_on_user_login', 1, $SETTINGS) === true) {
847
        // get all Admin users
848
        $val = DB::queryFirstRow('SELECT email FROM ' . prefixTable('users') . " WHERE admin = %i and email != ''", 1);
849
        if (DB::count() > 0) {
850
            // Add email to table
851
            prepareSendingEmail(
852
                $lang->get('email_subject_on_user_login'),
853
                str_replace(
854
                    [
855
                        '#tp_user#',
856
                        '#tp_date#',
857
                        '#tp_time#',
858
                    ],
859
                    [
860
                        ' ' . $session->get('user-login') . ' (IP: ' . getClientIpServer() . ')',
861
                        date($SETTINGS['date_format'], (int) time()),
862
                        date($SETTINGS['time_format'], (int) time()),
863
                    ],
864
                    $lang->get('email_body_on_user_login')
865
                ),
866
                $val['email'],
867
                $lang->get('administrator')
868
            );
869
        }
870
    }
871
}
872
873
/**
874
 * Determine if user permissions should be adjusted based on role names
875
 * Logic: Users with ID >= 1000000 get permissions from role "needles" in titles
876
 *
877
 * @param int $userId
878
 * @param string $userLogin
879
 * @param array $SETTINGS
880
 * @return bool
881
 */
882
function shouldAdjustPermissionsFromRoleNames(int $userId, string $userLogin, array $SETTINGS): bool
883
{
884
    if ($userId < 1000000) {
885
        return false;
886
    }
887
888
    $excludeUser = isset($SETTINGS['exclude_user']) ? str_contains($userLogin, $SETTINGS['exclude_user']) : false;
889
    if ($excludeUser) {
890
        return false;
891
    }
892
893
    return isset($SETTINGS['admin_needle']) 
894
        || isset($SETTINGS['manager_needle']) 
895
        || isset($SETTINGS['tp_manager_needle']) 
896
        || isset($SETTINGS['read_only_needle']);
897
}
898
899
/**
900
 * Apply role-based permissions using "needle" detection in role titles
901
 * 
902
 * @param array $role
903
 * @param array $currentPermissions
904
 * @param array $SETTINGS
905
 * @return array Updated permissions
906
 */
907
function applyRoleNeedlePermissions(array $role, array $currentPermissions, array $SETTINGS): array
908
{
909
    $needleConfig = [
910
        'admin_needle' => [
911
            'admin' => 1,
912
            'gestionnaire' => 0,
913
            'can_manage_all_users' => 0,
914
            'read_only' => 0,
915
        ],
916
        'manager_needle' => [
917
            'admin' => 0,
918
            'gestionnaire' => 1,
919
            'can_manage_all_users' => 0,
920
            'read_only' => 0,
921
        ],
922
        'tp_manager_needle' => [
923
            'admin' => 0,
924
            'gestionnaire' => 0,
925
            'can_manage_all_users' => 1,
926
            'read_only' => 0,
927
        ],
928
        'read_only_needle' => [
929
            'admin' => 0,
930
            'gestionnaire' => 0,
931
            'can_manage_all_users' => 0,
932
            'read_only' => 1,
933
        ],
934
    ];
935
936
    foreach ($needleConfig as $needleSetting => $permissions) {
937
        if (isset($SETTINGS[$needleSetting]) && str_contains($role['title'], $SETTINGS[$needleSetting])) {
938
            return $permissions;
939
        }
940
    }
941
942
    return $currentPermissions;
943
}
944
945
/**
946
 * Setup user roles and permissions
947
 * Handles role conversion, AD groups merge, and permission calculation
948
 *
949
 * @param SessionManager $session
950
 * @param array $userInfo (passed by reference to allow modifications)
951
 * @param array $SETTINGS
952
 * @return array DB update data if permissions were adjusted
953
 */
954
function setupUserRolesAndPermissions($session, array &$userInfo, array $SETTINGS): array
955
{
956
    $session = SessionManager::getSession();
957
958
    // User's roles - Convert , to ; if needed
959
    if (strpos($userInfo['fonction_id'] !== NULL ? (string) $userInfo['fonction_id'] : '', ',') !== -1) {
960
        $userInfo['fonction_id'] = str_replace(',', ';', (string) $userInfo['fonction_id']);
961
    }
962
    
963
    // Append with roles from AD groups
964
    if (is_null($userInfo['roles_from_ad_groups']) === false) {
965
        $userInfo['fonction_id'] = empty($userInfo['fonction_id']) === true 
966
            ? $userInfo['roles_from_ad_groups'] 
967
            : $userInfo['fonction_id'] . ';' . $userInfo['roles_from_ad_groups'];
968
    }
969
    
970
    // Store roles in session
971
    $session->set('user-roles', $userInfo['fonction_id']);
972
    $session->set('user-roles_array', array_unique(array_filter(explode(';', $userInfo['fonction_id']))));
973
    
974
    // Build array of roles and calculate permissions
975
    $session->set('user-pw_complexity', 0);
976
    $session->set('system-array_roles', []);
977
    
978
    $dbUpdateData = [];
979
    
980
    if (count($session->get('user-roles_array')) > 0) {
981
        // Get roles from database
982
        $rolesList = DB::query(
983
            'SELECT id, title, complexity
984
            FROM ' . prefixTable('roles_title') . '
985
            WHERE id IN %li',
986
            $session->get('user-roles_array')
987
        );
988
        
989
        // Check if we should adjust permissions based on role names
990
        $adjustPermissions = shouldAdjustPermissionsFromRoleNames(
991
            $session->get('user-id'),
992
            $session->get('user-login'),
993
            $SETTINGS
994
        );
995
        
996
        if ($adjustPermissions) {
997
            // Reset all permissions initially
998
            $userInfo['admin'] = $userInfo['gestionnaire'] = $userInfo['can_manage_all_users'] = $userInfo['read_only'] = 0;
999
        }
1000
        
1001
        // Process each role
1002
        foreach ($rolesList as $role) {
1003
            // Add to session roles array
1004
            SessionManager::addRemoveFromSessionAssociativeArray(
1005
                'system-array_roles',
1006
                [
1007
                    'id' => $role['id'],
1008
                    'title' => $role['title'],
1009
                ],
1010
                'add'
1011
            );
1012
            
1013
            // Adjust permissions based on role title "needles"
1014
            if ($adjustPermissions) {
1015
                $newPermissions = applyRoleNeedlePermissions($role, [
1016
                    'admin' => $userInfo['admin'],
1017
                    'gestionnaire' => $userInfo['gestionnaire'],
1018
                    'can_manage_all_users' => $userInfo['can_manage_all_users'],
1019
                    'read_only' => $userInfo['read_only'],
1020
                ], $SETTINGS);
1021
                
1022
                $userInfo['admin'] = $newPermissions['admin'];
1023
                $userInfo['gestionnaire'] = $newPermissions['gestionnaire'];
1024
                $userInfo['can_manage_all_users'] = $newPermissions['can_manage_all_users'];
1025
                $userInfo['read_only'] = $newPermissions['read_only'];
1026
            }
1027
1028
            // Get highest complexity
1029
            if ($session->get('user-pw_complexity') < (int) $role['complexity']) {
1030
                $session->set('user-pw_complexity', (int) $role['complexity']);
1031
            }
1032
        }
1033
        
1034
        // If permissions were adjusted, update session and prepare DB update
1035
        if ($adjustPermissions) {
1036
            $session->set('user-admin', (int) $userInfo['admin']);
1037
            $session->set('user-manager', (int) $userInfo['gestionnaire']);
1038
            $session->set('user-can_manage_all_users', (int) $userInfo['can_manage_all_users']);
1039
            $session->set('user-read_only', (int) $userInfo['read_only']);
1040
            
1041
            $dbUpdateData = [
1042
                'admin' => $userInfo['admin'],
1043
                'gestionnaire' => $userInfo['gestionnaire'],
1044
                'can_manage_all_users' => $userInfo['can_manage_all_users'],
1045
                'read_only' => $userInfo['read_only'],
1046
            ];
1047
        }
1048
    }
1049
    
1050
    return $dbUpdateData;
1051
}
1052
1053
/**
1054
 * Check if any unsuccessfull login tries exist
1055
 *
1056
 * @param int       $userInfoId
1057
 * @param string    $userInfoLogin
1058
 * @param string    $userInfoLastConnection
1059
 * @param string    $username
1060
 * @param array     $SETTINGS
1061
 * @return array
1062
 */
1063
function handleLoginAttempts(
1064
    $userInfoId,
1065
    $userInfoLogin,
1066
    $userInfoLastConnection,
1067
    $username,
1068
    $SETTINGS
1069
) : array
1070
{
1071
    $rows = DB::query(
1072
        'SELECT date
1073
        FROM ' . prefixTable('log_system') . "
1074
        WHERE field_1 = %s
1075
        AND type = 'failed_auth'
1076
        AND label = 'password_is_not_correct'
1077
        AND date >= %s AND date < %s",
1078
        $userInfoLogin,
1079
        $userInfoLastConnection,
1080
        time()
1081
    );
1082
    $arrAttempts = [];
1083
    if (DB::count() > 0) {
1084
        foreach ($rows as $record) {
1085
            array_push(
1086
                $arrAttempts,
1087
                date($SETTINGS['date_format'] . ' ' . $SETTINGS['time_format'], (int) $record['date'])
1088
            );
1089
        }
1090
    }
1091
    
1092
1093
    // Log into DB the user's connection
1094
    if (isKeyExistingAndEqual('log_connections', 1, $SETTINGS) === true) {
1095
        logEvents($SETTINGS, 'user_connection', 'connection', (string) $userInfoId, stripslashes($username));
1096
    }
1097
1098
    return [
1099
        'attemptsList' => $arrAttempts,
1100
        'attemptsCount' => count($rows),
1101
    ];
1102
}
1103
1104
1105
/**
1106
 * Can you user get logged into main page
1107
 *
1108
 * @param array     $SETTINGS
1109
 * @param int       $userInfoDisabled
1110
 * @param string    $username
1111
 * @param bool      $ldapConnection
1112
 *
1113
 * @return boolean
1114
 */
1115
function canUserGetLog(
1116
    $SETTINGS,
1117
    $userInfoDisabled,
1118
    $username,
1119
    $ldapConnection
1120
) : bool
1121
{
1122
    include_once $SETTINGS['cpassman_dir'] . '/sources/main.functions.php';
1123
1124
    if ((int) $userInfoDisabled === 1) {
1125
        return false;
1126
    }
1127
1128
    if (isKeyExistingAndEqual('ldap_mode', 0, $SETTINGS) === true) {
1129
        return true;
1130
    }
1131
    
1132
    if (isKeyExistingAndEqual('ldap_mode', 1, $SETTINGS) === true 
1133
        && (
1134
            ($ldapConnection === true && $username !== 'admin')
1135
            || $username === 'admin'
1136
        )
1137
    ) {
1138
        return true;
1139
    }
1140
1141
    if (isKeyExistingAndEqual('ldap_and_local_authentication', 1, $SETTINGS) === true
1142
        && isset($SETTINGS['ldap_mode']) === true && in_array($SETTINGS['ldap_mode'], ['1', '2']) === true
1143
    ) {
1144
        return true;
1145
    }
1146
1147
    return false;
1148
}
1149
1150
/**
1151
 * 
1152
 * Prepare user keys
1153
 * 
1154
 * @param array $userInfo   User account information
1155
 * @param string $passwordClear
1156
 *
1157
 * @return array
1158
 */
1159
function prepareUserEncryptionKeys($userInfo, $passwordClear, array $SETTINGS = []) : array
1160
{
1161
    $updateData = [];
1162
    if (is_null($userInfo['private_key']) === true || empty($userInfo['private_key']) === true || $userInfo['private_key'] === 'none') {
1163
        // No keys have been generated yet
1164
        // Create them
1165
        $userKeys = generateUserKeys($passwordClear, $SETTINGS);
1166
1167
        $updateData = [
1168
            'public_key' => $userKeys['public_key'],
1169
            'private_key' => $userKeys['private_key'],
1170
        ];
1171
1172
        // Add transparent recovery data if available
1173
        if (isset($userKeys['user_seed'])) {
1174
            $updateData['user_derivation_seed'] = $userKeys['user_seed'];
1175
            $updateData['private_key_backup'] = $userKeys['private_key_backup'];
1176
            $updateData['key_integrity_hash'] = $userKeys['key_integrity_hash'];
1177
            $updateData['last_pw_change'] = time();
1178
        }
1179
1180
        return [
1181
            'public_key' => $userKeys['public_key'],
1182
            'private_key_clear' => $userKeys['private_key_clear'],
1183
            'update_keys_in_db' => $updateData,
1184
        ];
1185
    }
1186
1187
    if ($userInfo['special'] === 'generate-keys') {
1188
        return [
1189
            'public_key' => $userInfo['public_key'],
1190
            'private_key_clear' => '',
1191
            'update_keys_in_db' => [],
1192
        ];
1193
    }
1194
1195
    // Don't perform this in case of special login action
1196
    if ($userInfo['special'] === 'otc_is_required_on_next_login' || $userInfo['special'] === 'user_added_from_ad') {
1197
        return [
1198
            'public_key' => $userInfo['public_key'],
1199
            'private_key_clear' => '',
1200
            'update_keys_in_db' => [],
1201
        ];
1202
    }
1203
1204
    // Get encryption version (default to 1 if not set)
1205
    $encryptionVersion = isset($userInfo['encryption_version']) ? (int) $userInfo['encryption_version'] : 1;
1206
    
1207
    // Try to decrypt private key with migration support
1208
    $decryptResult = decryptPrivateKeyWithMigration(
1209
        $passwordClear,
1210
        $userInfo['private_key'],
1211
        (int) $userInfo['id'],
1212
        $encryptionVersion
1213
    );
1214
1215
    // What phpseclib version is used?
1216
    if ($encryptionVersion !== $decryptResult['version_used']) {
1217
        $updateData['encryption_version'] = $decryptResult['version_used'];
1218
    }
1219
1220
    // Check for migration errors
1221
    if ($decryptResult['migration_error'] === true) {
1222
        // Decryption failed - try transparent recovery as fallback
1223
        if (!empty($userInfo['user_derivation_seed'])
1224
            && !empty($userInfo['private_key_backup'])) {
1225
1226
            $recovery = attemptTransparentRecovery($userInfo, $passwordClear, $SETTINGS);
1227
1228
            if ($recovery['success']) {
1229
                return [
1230
                    'public_key' => $userInfo['public_key'],
1231
                    'private_key_clear' => $recovery['private_key_clear'],
1232
                    'update_keys_in_db' => [],
1233
                ];
1234
            }
1235
        }
1236
1237
        // No recovery possible - throw exception to trigger normal error handling
1238
        throw new Exception('Failed to decrypt private key');
1239
    }
1240
1241
    $privateKeyClear = $decryptResult['private_key_clear'];
1242
1243
    // If migration is needed, perform it now
1244
    if ($decryptResult['needs_migration'] === true) {
1245
        $migrationSuccess = migrateAllUserKeysToV3(
1246
            (int) $userInfo['id'],
1247
            $passwordClear,
1248
            $privateKeyClear,
1249
            $userInfo
1250
        );
1251
1252
        if ($migrationSuccess) {
1253
            // Migration successful - encryption_version updated in DB by migrateAllUserKeysToV3
1254
            if (defined('LOG_TO_SERVER') && LOG_TO_SERVER === true) {
1255
                error_log('TEAMPASS Migration - User ' . $userInfo['id'] . ' successfully migrated to v3');
1256
            }
1257
        } else {
1258
            // Migration failed but decryption worked, so continue with v1
1259
            if (defined('LOG_TO_SERVER') && LOG_TO_SERVER === true) {
1260
                error_log('TEAMPASS Migration Warning - User ' . $userInfo['id'] . ' migration to v3 failed, staying in v1');
1261
            }
1262
        }
1263
    }
1264
1265
    try {
1266
        // If user has seed but no backup, create it on first successful login
1267
        if (!empty($userInfo['user_derivation_seed']) && empty($userInfo['private_key_backup'])) {
1268
            if (defined('LOG_TO_SERVER') && LOG_TO_SERVER === true) {
1269
                error_log('TEAMPASS Transparent Recovery - Creating backup for user ' . ($userInfo['login'] ?? 'unknown'));
1270
            }
1271
1272
            $derivedKey = deriveBackupKey($userInfo['user_derivation_seed'], $userInfo['public_key'], $SETTINGS);
1273
            // Encrypt private key backup using CryptoManager (use v3 if migration happened, otherwise v1)
1274
            $backupHashAlgorithm = ($decryptResult['needs_migration'] === true) ? 'sha256' : 'sha1';
1275
            $privateKeyBackup = base64_encode(
1276
                \TeampassClasses\CryptoManager\CryptoManager::aesEncrypt(
1277
                    base64_decode($privateKeyClear),
1278
                    $derivedKey,
1279
                    'cbc',
1280
                    $backupHashAlgorithm
1281
                )
1282
            );
1283
1284
            // Generate integrity hash
1285
            $serverSecret = getServerSecret();
1286
            $integrityHash = generateKeyIntegrityHash($userInfo['user_derivation_seed'], $userInfo['public_key'], $serverSecret);
1287
1288
            $updateData['private_key_backup'] = $privateKeyBackup;
1289
            $updateData['key_integrity_hash'] = $integrityHash;
1290
            $updateData['last_pw_change'] = time();
1291
        }
1292
1293
        return [
1294
            'public_key' => $userInfo['public_key'],
1295
            'private_key_clear' => $privateKeyClear,
1296
            'update_keys_in_db' => $updateData,
1297
        ];
1298
    } catch (Exception $e) {
1299
        // Exception during backup creation - log but don't fail login
1300
        if (defined('LOG_TO_SERVER') && LOG_TO_SERVER === true) {
1301
            error_log('TEAMPASS Error - prepareUserEncryptionKeys backup creation failed: ' . $e->getMessage());
1302
        }
1303
1304
        return [
1305
            'public_key' => $userInfo['public_key'],
1306
            'private_key_clear' => $privateKeyClear,
1307
            'update_keys_in_db' => [],
1308
        ];
1309
    }
1310
}
1311
1312
1313
/**
1314
 * CHECK PASSWORD VALIDITY
1315
 * Don't take into consideration if LDAP in use
1316
 * 
1317
 * @param array $userInfo User account information
1318
 * @param int $numDaysBeforePwExpiration Number of days before password expiration
1319
 * @param int $lastPwChange Last password change
1320
 * @param array $SETTINGS Teampass settings
1321
 *
1322
 * @return array
1323
 */
1324
function checkUserPasswordValidity(array $userInfo, int $numDaysBeforePwExpiration, int $lastPwChange, array $SETTINGS)
1325
{
1326
    if (isKeyExistingAndEqual('ldap_mode', 1, $SETTINGS) === true && $userInfo['auth_type'] !== 'local') {
1327
        return [
1328
            'validite_pw' => true,
1329
            'last_pw_change' => $userInfo['last_pw_change'],
1330
            'user_force_relog' => '',
1331
            'numDaysBeforePwExpiration' => '',
1332
        ];
1333
    }
1334
    
1335
    if (isset($userInfo['last_pw_change']) === true) {
1336
        if ((int) $SETTINGS['pw_life_duration'] === 0) {
1337
            return [
1338
                'validite_pw' => true,
1339
                'last_pw_change' => '',
1340
                'user_force_relog' => 'infinite',
1341
                'numDaysBeforePwExpiration' => '',
1342
            ];
1343
        } elseif ((int) $SETTINGS['pw_life_duration'] > 0) {
1344
            $numDaysBeforePwExpiration = (int) $SETTINGS['pw_life_duration'] - round(
1345
                (mktime(0, 0, 0, (int) date('m'), (int) date('d'), (int) date('y')) - $userInfo['last_pw_change']) / (24 * 60 * 60)
1346
            );
1347
            return [
1348
                'validite_pw' => $numDaysBeforePwExpiration <= 0 ? false : true,
1349
                'last_pw_change' => $userInfo['last_pw_change'],
1350
                'user_force_relog' => 'infinite',
1351
                'numDaysBeforePwExpiration' => (int) $numDaysBeforePwExpiration,
1352
            ];
1353
        } else {
1354
            return [
1355
                'validite_pw' => false,
1356
                'last_pw_change' => '',
1357
                'user_force_relog' => '',
1358
                'numDaysBeforePwExpiration' => '',
1359
            ];
1360
        }
1361
    } else {
1362
        return [
1363
            'validite_pw' => false,
1364
            'last_pw_change' => '',
1365
            'user_force_relog' => '',
1366
            'numDaysBeforePwExpiration' => '',
1367
        ];
1368
    }
1369
}
1370
1371
1372
/**
1373
 * Authenticate a user through AD/LDAP.
1374
 *
1375
 * @param string $username      Username
1376
 * @param array $userInfo       User account information
1377
 * @param string $passwordClear Password
1378
 * @param array $SETTINGS       Teampass settings
1379
 *
1380
 * @return array
1381
 */
1382
function authenticateThroughAD(string $username, array $userInfo, string $passwordClear, array $SETTINGS): array
1383
{
1384
    $session = SessionManager::getSession();
1385
    $lang = new Language($session->get('user-language') ?? 'english');
1386
    
1387
    try {
1388
        // Get LDAP connection and handler
1389
        $ldapHandler = initializeLdapConnection($SETTINGS);
1390
        
1391
        // Authenticate user
1392
        $authResult = authenticateUser($username, $passwordClear, $ldapHandler, $SETTINGS, $lang);
1393
        if ($authResult['error']) {
1394
            return $authResult;
1395
        }
1396
        
1397
        $userADInfos = $authResult['user_info'];
1398
        
1399
        // Verify account expiration
1400
        if (isAccountExpired($userADInfos)) {
1401
            return [
1402
                'error' => true,
1403
                'message' => $lang->get('error_ad_user_expired'),
1404
            ];
1405
        }
1406
        
1407
        // Handle user creation if needed
1408
        if ($userInfo['ldap_user_to_be_created']) {
1409
            $userInfo = handleNewUser($username, $passwordClear, $userADInfos, $userInfo, $SETTINGS, $lang);
1410
        }
1411
        
1412
        // Get and handle user groups
1413
        $userGroupsData = getUserADGroups($userADInfos, $ldapHandler, $SETTINGS);
1414
        handleUserADGroups($username, $userInfo, $userGroupsData['userGroups'], $SETTINGS);
1415
        
1416
        // Finalize authentication
1417
        finalizeAuthentication($userInfo, $passwordClear, $SETTINGS);
1418
        
1419
        return [
1420
            'error' => false,
1421
            'message' => '',
1422
            'user_info' => $userInfo,
1423
        ];
1424
        
1425
    } catch (Exception $e) {
1426
        return [
1427
            'error' => true,
1428
            'message' => "Error: " . $e->getMessage(),
1429
        ];
1430
    }
1431
}
1432
1433
/**
1434
 * Initialize LDAP connection based on type
1435
 * 
1436
 * @param array $SETTINGS Teampass settings
1437
 * @return array Contains connection and type-specific handler
1438
 * @throws Exception
1439
 */
1440
function initializeLdapConnection(array $SETTINGS): array
1441
{
1442
    $ldapExtra = new LdapExtra($SETTINGS);
1443
    $ldapConnection = $ldapExtra->establishLdapConnection();
1444
    
1445
    switch ($SETTINGS['ldap_type']) {
1446
        case 'ActiveDirectory':
1447
            return [
1448
                'connection' => $ldapConnection,
1449
                'handler' => new ActiveDirectoryExtra(),
1450
                'type' => 'ActiveDirectory'
1451
            ];
1452
        case 'OpenLDAP':
1453
            return [
1454
                'connection' => $ldapConnection,
1455
                'handler' => new OpenLdapExtra(),
1456
                'type' => 'OpenLDAP'
1457
            ];
1458
        default:
1459
            throw new Exception("Unsupported LDAP type: " . $SETTINGS['ldap_type']);
1460
    }
1461
}
1462
1463
/**
1464
 * Authenticate user against LDAP
1465
 * 
1466
 * @param string $username Username
1467
 * @param string $passwordClear Password
1468
 * @param array $ldapHandler LDAP connection and handler
1469
 * @param array $SETTINGS Teampass settings
1470
 * @param Language $lang Language instance
1471
 * @return array Authentication result
1472
 */
1473
function authenticateUser(string $username, string $passwordClear, array $ldapHandler, array $SETTINGS, Language $lang): array
1474
{
1475
    try {
1476
        $userAttribute = $SETTINGS['ldap_user_attribute'] ?? 'samaccountname';
1477
        $userADInfos = $ldapHandler['connection']->query()
1478
            ->where($userAttribute, '=', $username)
1479
            ->firstOrFail();
1480
        
1481
        // Verify user status for ActiveDirectory
1482
        if ($ldapHandler['type'] === 'ActiveDirectory' && !$ldapHandler['handler']->userIsEnabled((string) $userADInfos['dn'], $ldapHandler['connection'])) {
1483
            return [
1484
                'error' => true,
1485
                'message' => "Error: User is not enabled"
1486
            ];
1487
        }
1488
        
1489
        // Attempt authentication
1490
        $authIdentifier = $ldapHandler['type'] === 'ActiveDirectory' 
1491
            ? $userADInfos['userprincipalname'][0] 
1492
            : $userADInfos['dn'];
1493
            
1494
        if (!$ldapHandler['connection']->auth()->attempt($authIdentifier, $passwordClear)) {
1495
            return [
1496
                'error' => true,
1497
                'message' => "Error: User is not authenticated"
1498
            ];
1499
        }
1500
        
1501
        return [
1502
            'error' => false,
1503
            'user_info' => $userADInfos
1504
        ];
1505
        
1506
    } catch (\LdapRecord\Query\ObjectNotFoundException $e) {
1507
        return [
1508
            'error' => true,
1509
            'message' => $lang->get('error_bad_credentials')
1510
        ];
1511
    }
1512
}
1513
1514
/**
1515
 * Check if user account is expired
1516
 * 
1517
 * @param array $userADInfos User AD information
1518
 * @return bool
1519
 */
1520
function isAccountExpired(array $userADInfos): bool
1521
{
1522
    return (isset($userADInfos['shadowexpire'][0]) && (int) $userADInfos['shadowexpire'][0] === 1)
1523
        || (isset($userADInfos['accountexpires'][0]) 
1524
            && (int) $userADInfos['accountexpires'][0] < time() 
1525
            && (int) $userADInfos['accountexpires'][0] !== 0);
1526
}
1527
1528
/**
1529
 * Handle creation of new user
1530
 * 
1531
 * @param string $username Username
1532
 * @param string $passwordClear Password
1533
 * @param array $userADInfos User AD information
1534
 * @param array $userInfo User information
1535
 * @param array $SETTINGS Teampass settings
1536
 * @param Language $lang Language instance
1537
 * @return array User information
1538
 */
1539
function handleNewUser(string $username, string $passwordClear, array $userADInfos, array $userInfo, array $SETTINGS, Language $lang): array
1540
{
1541
    $userInfo = externalAdCreateUser(
1542
        $username,
1543
        $passwordClear,
1544
        $userADInfos['mail'][0] ?? '',
1545
        $userADInfos['givenname'][0],
1546
        $userADInfos['sn'][0],
1547
        'ldap',
1548
        [],
1549
        $SETTINGS
1550
    );
1551
1552
    handleUserKeys(
1553
        (int) $userInfo['id'],
1554
        $passwordClear,
1555
        (int) ($SETTINGS['maximum_number_of_items_to_treat'] ?? NUMBER_ITEMS_IN_BATCH),
1556
        uniqidReal(20),
1557
        true,
1558
        true,
1559
        true,
1560
        false,
1561
        $lang->get('email_body_user_config_2')
1562
    );
1563
1564
    $userInfo['has_been_created'] = 1;
1565
    return $userInfo;
1566
}
1567
1568
/**
1569
 * Get user groups based on LDAP type
1570
 * 
1571
 * @param array $userADInfos User AD information
1572
 * @param array $ldapHandler LDAP connection and handler
1573
 * @param array $SETTINGS Teampass settings
1574
 * @return array User groups
1575
 */
1576
function getUserADGroups(array $userADInfos, array $ldapHandler, array $SETTINGS): array
1577
{
1578
    $dnAttribute = $SETTINGS['ldap_user_dn_attribute'] ?? 'distinguishedname';
1579
    
1580
    if ($ldapHandler['type'] === 'ActiveDirectory') {
1581
        return $ldapHandler['handler']->getUserADGroups(
1582
            $userADInfos[$dnAttribute][0],
1583
            $ldapHandler['connection'],
1584
            $SETTINGS
1585
        );
1586
    }
1587
    
1588
    if ($ldapHandler['type'] === 'OpenLDAP') {
1589
        return $ldapHandler['handler']->getUserADGroups(
1590
            $userADInfos['dn'],
1591
            $ldapHandler['connection'],
1592
            $SETTINGS
1593
        );
1594
    }
1595
    
1596
    throw new Exception("Unsupported LDAP type: " . $ldapHandler['type']);
1597
}
1598
1599
/**
1600
 * Permits to update the user's AD groups with mapping roles
1601
 *
1602
 * @param string $username
1603
 * @param array $userInfo
1604
 * @param array $groups
1605
 * @param array $SETTINGS
1606
 * @return void
1607
 */
1608
function handleUserADGroups(string $username, array $userInfo, array $groups, array $SETTINGS): void
1609
{
1610
    if (isset($SETTINGS['enable_ad_users_with_ad_groups']) === true && (int) $SETTINGS['enable_ad_users_with_ad_groups'] === 1) {
1611
        // Get user groups from AD
1612
        $user_ad_groups = [];
1613
        foreach($groups as $group) {
1614
            // Skip invalid/corrupted group data
1615
            if (empty($group) || !mb_check_encoding($group, 'UTF-8') || !preg_match('/^[\x20-\x7E\x80-\xFF]+$/u', $group)) {
1616
                if (WIP === true) error_log('DEBUG TeamPass - LDAP: Invalid group data detected and skipped: ' . bin2hex($group));
1617
                continue;
1618
            }
1619
            
1620
            // Clean the group string
1621
            $group = trim($group);
1622
            
1623
            // get relation role id for AD group
1624
            $role = DB::queryFirstRow(
1625
                'SELECT lgr.role_id
1626
                FROM ' . prefixTable('ldap_groups_roles') . ' AS lgr
1627
                WHERE lgr.ldap_group_id = %s',
1628
                $group
1629
            );
1630
            if (DB::count() > 0) {
1631
                array_push($user_ad_groups, (int) $role['role_id']); 
1632
            }
1633
        }
1634
        
1635
        // Save AD roles in users_roles table with source='ad'
1636
        if (count($user_ad_groups) > 0) {
1637
            setUserRoles((int) $userInfo['id'], $user_ad_groups, 'ad');
1638
            
1639
            // Update userInfo array for session (format with semicolons for compatibility)
1640
            $userInfo['roles_from_ad_groups'] = implode(';', $user_ad_groups);
1641
        } else {
1642
            // Remove all AD roles
1643
            removeUserRolesBySource((int) $userInfo['id'], 'ad');
1644
            
1645
            $userInfo['roles_from_ad_groups'] = '';
1646
        }
1647
    } else {
1648
        // Delete all user's AD roles
1649
        removeUserRolesBySource((int) $userInfo['id'], 'ad');
1650
        
1651
        $userInfo['roles_from_ad_groups'] = '';
1652
    }
1653
}
1654
1655
1656
/**
1657
 * Permits to finalize the authentication process.
1658
 *
1659
 * @param array $userInfo
1660
 * @param string $passwordClear
1661
 * @param array $SETTINGS
1662
 */
1663
function finalizeAuthentication(
1664
    array $userInfo,
1665
    string $passwordClear,
1666
    array $SETTINGS
1667
): void
1668
{
1669
    $passwordManager = new PasswordManager();
1670
    
1671
    // Migrate password if needed
1672
    $result  = $passwordManager->migratePassword(
1673
        $userInfo['pw'],
1674
        $passwordClear,
1675
        (int) $userInfo['id']
1676
    );
1677
1678
    // Use the new hashed password if migration was successful
1679
    $hashedPassword = $result['hashedPassword'];
1680
    if (empty($userInfo['pw']) === true || $userInfo['special'] === 'user_added_from_ad') {
1681
        // 2 cases are managed here:
1682
        // Case where user has never been connected then erase current pwd with the ldap's one
1683
        // Case where user has been added from LDAP and never being connected to TP
1684
        DB::update(
1685
            prefixTable('users'),
1686
            [
1687
                'pw' => $passwordManager->hashPassword($passwordClear),
1688
                'otp_provided' => 1,
1689
                'special' => 'none',
1690
            ],
1691
            'id = %i',
1692
            $userInfo['id']
1693
        );
1694
1695
        // Re-encrypt private key with AD password via transparent recovery
1696
        // The private key was encrypted with a random password during manual creation
1697
        handleExternalPasswordChange((int) $userInfo['id'], $passwordClear, $userInfo, $SETTINGS);
1698
    } elseif ($passwordManager->verifyPassword($hashedPassword, $passwordClear) === false) {
1699
        // Case where user is auth by LDAP but his password in Teampass is not synchronized
1700
        // For example when user has changed his password in AD.
1701
        // So we need to update it in Teampass and handle private key re-encryption
1702
        DB::update(
1703
            prefixTable('users'),
1704
            [
1705
                'pw' => $passwordManager->hashPassword($passwordClear),
1706
                'otp_provided' => 1,
1707
                'special' => 'none',
1708
            ],
1709
            'id = %i',
1710
            $userInfo['id']
1711
        );
1712
1713
        // Try transparent recovery for automatic re-encryption
1714
        handleExternalPasswordChange((int) $userInfo['id'], $passwordClear, $userInfo, $SETTINGS);
1715
    }
1716
}
1717
1718
/**
1719
 * Undocumented function.
1720
 *
1721
 * @param string $username      User name
1722
 * @param string $passwordClear User password in clear
1723
 * @param array $retLDAP       Received data from LDAP
1724
 * @param array $SETTINGS      Teampass settings
1725
 *
1726
 * @return array
1727
 */
1728
function externalAdCreateUser(
1729
    string $login,
1730
    string $passwordClear,
1731
    string $userEmail,
1732
    string $userName,
1733
    string $userLastname,
1734
    string $authType,
1735
    array $userGroups,
1736
    array $SETTINGS
1737
): array
1738
{
1739
    // Generate user keys pair with transparent recovery support
1740
    $userKeys = generateUserKeys($passwordClear, $SETTINGS);
1741
1742
    // Create password hash
1743
    $passwordManager = new PasswordManager();
1744
    $hashedPassword = $passwordManager->hashPassword($passwordClear);
1745
    
1746
    // If any groups provided, add user to them
1747
    if (count($userGroups) > 0) {
1748
        $groupIds = [];
1749
        foreach ($userGroups as $group) {
1750
            // Check if exists in DB
1751
            $groupData = DB::queryFirstRow(
1752
                'SELECT id
1753
                FROM ' . prefixTable('roles_title') . '
1754
                WHERE title = %s',
1755
                $group["displayName"]
1756
            );
1757
1758
            if (DB::count() > 0) {
1759
                array_push($groupIds, $groupData['id']);
1760
            }
1761
        }
1762
        $userGroups = implode(';', $groupIds);
1763
    } else {
1764
        $userGroups = '';
1765
    }
1766
    
1767
    if (empty($userGroups) && !empty($SETTINGS['oauth_selfregistered_user_belongs_to_role'])) {
1768
        $userGroups = $SETTINGS['oauth_selfregistered_user_belongs_to_role'];
1769
    }
1770
1771
    // Prepare user data
1772
    $userData = [
1773
        'login' => (string) $login,
1774
        'pw' => (string) $hashedPassword,
1775
        'email' => (string) $userEmail,
1776
        'name' => (string) $userName,
1777
        'lastname' => (string) $userLastname,
1778
        'admin' => '0',
1779
        'gestionnaire' => '0',
1780
        'can_manage_all_users' => '0',
1781
        'personal_folder' => $SETTINGS['enable_pf_feature'] === '1' ? '1' : '0',
1782
        'groupes_interdits' => '',
1783
        'groupes_visibles' => '',
1784
        'fonction_id' => $userGroups,
1785
        'last_pw_change' => (int) time(),
1786
        'user_language' => (string) $SETTINGS['default_language'],
1787
        'encrypted_psk' => '',
1788
        'isAdministratedByRole' => $authType === 'ldap' ?
1789
            (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)
1790
            : (
1791
                $authType === 'oauth2' ?
1792
                (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)
1793
                : 0
1794
            ),
1795
        'public_key' => $userKeys['public_key'],
1796
        'private_key' => $userKeys['private_key'],
1797
        'special' => 'none',
1798
        'auth_type' => $authType,
1799
        'otp_provided' => '1',
1800
        'is_ready_for_usage' => '0',
1801
        'created_at' => time(),
1802
    ];
1803
1804
    // Add transparent recovery fields if available
1805
    if (isset($userKeys['user_seed'])) {
1806
        $userData['user_derivation_seed'] = $userKeys['user_seed'];
1807
        $userData['private_key_backup'] = $userKeys['private_key_backup'];
1808
        $userData['key_integrity_hash'] = $userKeys['key_integrity_hash'];
1809
        $userData['last_pw_change'] = time();
1810
    }
1811
1812
    // Insert user in DB
1813
    DB::insert(prefixTable('users'), $userData);
1814
    $newUserId = DB::insertId();
1815
1816
    // Create the API key
1817
    DB::insert(
1818
        prefixTable('api'),
1819
        array(
1820
            'type' => 'user',
1821
            'user_id' => $newUserId,
1822
            'value' => encryptUserObjectKey(base64_encode(base64_encode(uniqidReal(39))), $userKeys['public_key']),
1823
            'timestamp' => time(),
1824
            'allowed_to_read' => 1,
1825
            'allowed_folders' => '',
1826
            'enabled' => 0,
1827
        )
1828
    );
1829
1830
    // Create personnal folder
1831
    if (isKeyExistingAndEqual('enable_pf_feature', 1, $SETTINGS) === true) {
1832
        DB::insert(
1833
            prefixTable('nested_tree'),
1834
            [
1835
                'parent_id' => '0',
1836
                'title' => $newUserId,
1837
                'bloquer_creation' => '0',
1838
                'bloquer_modification' => '0',
1839
                'personal_folder' => '1',
1840
                'categories' => '',
1841
            ]
1842
        );
1843
        // Rebuild tree
1844
        $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
1845
        $tree->rebuild();
1846
    }
1847
1848
1849
    return [
1850
        'error' => false,
1851
        'message' => '',
1852
        'proceedIdentification' => true,
1853
        'user_initial_creation_through_external_ad' => true,
1854
        'id' => $newUserId,
1855
        'oauth2_login_ongoing' => true,
1856
    ];
1857
}
1858
1859
/**
1860
 * Undocumented function.
1861
 *
1862
 * @param string                $username     Username
1863
 * @param array                 $userInfo     Result of query
1864
 * @param string|array|resource $dataReceived DataReceived
1865
 * @param array                 $SETTINGS     Teampass settings
1866
 *
1867
 * @return array
1868
 */
1869
function googleMFACheck(string $username, array $userInfo, $dataReceived, array $SETTINGS): array
1870
{
1871
    $session = SessionManager::getSession();    
1872
    $lang = new Language($session->get('user-language') ?? 'english');
1873
1874
    if (
1875
        isset($dataReceived['GACode']) === true
1876
        && empty($dataReceived['GACode']) === false
1877
    ) {
1878
        $sessionAdmin = $session->get('user-admin');
1879
        $sessionUrl = $session->get('user-initial_url');
1880
        $sessionPwdAttempts = $session->get('pwd_attempts');
1881
        // create new instance
1882
        $tfa = new TwoFactorAuth($SETTINGS['ga_website_name']);
1883
        // Init
1884
        $firstTime = [];
1885
        // now check if it is the 1st time the user is using 2FA
1886
        if ($userInfo['ga_temporary_code'] !== 'none' && $userInfo['ga_temporary_code'] !== 'done') {
1887
            if ($userInfo['ga_temporary_code'] !== $dataReceived['GACode']) {
1888
                return [
1889
                    'error' => true,
1890
                    'message' => $lang->get('ga_bad_code'),
1891
                    'proceedIdentification' => false,
1892
                    'ga_bad_code' => true,
1893
                    'firstTime' => $firstTime,
1894
                ];
1895
            }
1896
1897
            // If first time with MFA code
1898
            $proceedIdentification = false;
1899
            
1900
            // generate new QR
1901
            $new_2fa_qr = $tfa->getQRCodeImageAsDataUri(
1902
                'Teampass - ' . $username,
1903
                $userInfo['ga']
1904
            );
1905
            // clear temporary code from DB
1906
            DB::update(
1907
                prefixTable('users'),
1908
                [
1909
                    'ga_temporary_code' => 'done',
1910
                ],
1911
                'id=%i',
1912
                $userInfo['id']
1913
            );
1914
            $firstTime = [
1915
                'value' => '<img src="' . $new_2fa_qr . '">',
1916
                'user_admin' => isset($sessionAdmin) ? (int) $sessionAdmin : '',
1917
                'initial_url' => isset($sessionUrl) === true ? $sessionUrl : '',
1918
                'pwd_attempts' => (int) $sessionPwdAttempts,
1919
                'message' => $lang->get('ga_flash_qr_and_login'),
1920
                'mfaStatus' => 'ga_temporary_code_correct',
1921
            ];
1922
        } else {
1923
            // verify the user GA code
1924
            if ($tfa->verifyCode($userInfo['ga'], $dataReceived['GACode'])) {
1925
                $proceedIdentification = true;
1926
            } else {
1927
                return [
1928
                    'error' => true,
1929
                    'message' => $lang->get('ga_bad_code'),
1930
                    'proceedIdentification' => false,
1931
                    'ga_bad_code' => true,
1932
                    'firstTime' => $firstTime,
1933
                ];
1934
            }
1935
        }
1936
    } else {
1937
        return [
1938
            'error' => true,
1939
            'message' => $lang->get('ga_bad_code'),
1940
            'proceedIdentification' => false,
1941
            'ga_bad_code' => true,
1942
            'firstTime' => [],
1943
        ];
1944
    }
1945
1946
    return [
1947
        'error' => false,
1948
        'message' => '',
1949
        'proceedIdentification' => $proceedIdentification,
1950
        'firstTime' => $firstTime,
1951
    ];
1952
}
1953
1954
1955
/**
1956
 * Perform DUO checks
1957
 *
1958
 * @param string $username
1959
 * @param string|array|resource $dataReceived
1960
 * @param array $SETTINGS
1961
 * @return array
1962
 */
1963
function duoMFACheck(
1964
    string $username,
1965
    $dataReceived,
1966
    array $SETTINGS
1967
): array
1968
{
1969
    $session = SessionManager::getSession();
1970
    $lang = new Language($session->get('user-language') ?? 'english');
1971
1972
    $sessionPwdAttempts = $session->get('pwd_attempts');
1973
    $saved_state = null !== $session->get('user-duo_state') ? $session->get('user-duo_state') : '';
1974
    $duo_status = null !== $session->get('user-duo_status') ? $session->get('user-duo_status') : '';
1975
1976
    // Ensure state and login are set
1977
    if (
1978
        (empty($saved_state) || empty($dataReceived['login']) || !isset($dataReceived['duo_state']) || empty($dataReceived['duo_state']))
1979
        && $duo_status === 'IN_PROGRESS'
1980
        && $dataReceived['duo_status'] !== 'start_duo_auth'
1981
    ) {
1982
        return [
1983
            'error' => true,
1984
            'message' => $lang->get('duo_no_data'),
1985
            'pwd_attempts' => (int) $sessionPwdAttempts,
1986
            'proceedIdentification' => false,
1987
        ];
1988
    }
1989
1990
    // Ensure state matches from initial request
1991
    if ($duo_status === 'IN_PROGRESS' && $dataReceived['duo_state'] !== $saved_state) {
1992
        $session->set('user-duo_state', '');
1993
        $session->set('user-duo_status', '');
1994
1995
        // We did not received a proper Duo state
1996
        return [
1997
            'error' => true,
1998
            'message' => $lang->get('duo_error_state'),
1999
            'pwd_attempts' => (int) $sessionPwdAttempts,
2000
            'proceedIdentification' => false,
2001
        ];
2002
    }
2003
2004
    return [
2005
        'error' => false,
2006
        'pwd_attempts' => (int) $sessionPwdAttempts,
2007
        'saved_state' => $saved_state,
2008
        'duo_status' => $duo_status,
2009
    ];
2010
}
2011
2012
2013
/**
2014
 * Create the redirect URL or check if the DUO Universal prompt was completed successfully.
2015
 *
2016
 * @param string                $username               Username
2017
 * @param string|array|resource $dataReceived           DataReceived
2018
 * @param array                 $sessionPwdAttempts     Nb of pwd attempts
2019
 * @param array                 $saved_state            Saved state
2020
 * @param array                 $duo_status             Duo status
2021
 * @param array                 $SETTINGS               Teampass settings
2022
 *
2023
 * @return array
2024
 */
2025
function duoMFAPerform(
2026
    string $username,
2027
    $dataReceived,
2028
    int $sessionPwdAttempts,
2029
    string $saved_state,
2030
    string $duo_status,
2031
    array $SETTINGS
2032
): array
2033
{
2034
    $session = SessionManager::getSession();
2035
    $lang = new Language($session->get('user-language') ?? 'english');
2036
2037
    try {
2038
        $duo_client = new Client(
2039
            $SETTINGS['duo_ikey'],
2040
            $SETTINGS['duo_skey'],
2041
            $SETTINGS['duo_host'],
2042
            $SETTINGS['cpassman_url'].'/'.DUO_CALLBACK
2043
        );
2044
    } catch (DuoException $e) {
2045
        return [
2046
            'error' => true,
2047
            'message' => $lang->get('duo_config_error'),
2048
            'debug_message' => $e->getMessage(),
2049
            'pwd_attempts' => (int) $sessionPwdAttempts,
2050
            'proceedIdentification' => false,
2051
        ];
2052
    }
2053
        
2054
    try {
2055
        $duo_error = $lang->get('duo_error_secure');
2056
        $duo_failmode = "none";
2057
        $duo_client->healthCheck();
2058
    } catch (DuoException $e) {
2059
        //Not implemented Duo Failmode in case the Duo services are not available
2060
        /*if ($SETTINGS['duo_failmode'] == "safe") {
2061
            # If we're failing open, errors in 2FA still allow for success
2062
            $duo_error = $lang->get('duo_error_failopen');
2063
            $duo_failmode = "safe";
2064
        } else {
2065
            # Duo has failed and is unavailable, redirect user to the login page
2066
            $duo_error = $lang->get('duo_error_secure');
2067
            $duo_failmode = "secure";
2068
        }*/
2069
        return [
2070
            'error' => true,
2071
            'message' => $duo_error . $lang->get('duo_error_check_config'),
2072
            'pwd_attempts' => (int) $sessionPwdAttempts,
2073
            'debug_message' => $e->getMessage(),
2074
            'proceedIdentification' => false,
2075
        ];
2076
    }
2077
    
2078
    // Check if no one played with the javascript
2079
    if ($duo_status !== 'IN_PROGRESS' && $dataReceived['duo_status'] === 'start_duo_auth') {
2080
        # Create the Duo URL to send the user to
2081
        try {
2082
            $duo_state = $duo_client->generateState();
2083
            $duo_redirect_url = $duo_client->createAuthUrl($username, $duo_state);
2084
        } catch (DuoException $e) {
2085
            return [
2086
                'error' => true,
2087
                'message' => $duo_error . $lang->get('duo_error_url'),
2088
                'pwd_attempts' => (int) $sessionPwdAttempts,
2089
                'debug_message' => $e->getMessage(),
2090
                'proceedIdentification' => false,
2091
            ];
2092
        }
2093
        
2094
        // Somethimes Duo return success but fail to return a URL, double check if the URL has been created
2095
        if (!empty($duo_redirect_url) && filter_var($duo_redirect_url,FILTER_SANITIZE_URL)) {
2096
            // Since Duo Universal requires a redirect, let's store some info when the user get's back after completing the Duo prompt
2097
            $key = hash('sha256', $duo_state);
2098
            $iv = substr(hash('sha256', $duo_state), 0, 16);
2099
            $duo_data = serialize([
2100
                'duo_login' => $username,
2101
                'duo_pwd' => $dataReceived['pw'],
2102
            ]);
2103
            $duo_data_enc = openssl_encrypt($duo_data, 'AES-256-CBC', $key, 0, $iv);
2104
            $session->set('user-duo_state', $duo_state);
2105
            $session->set('user-duo_data', base64_encode($duo_data_enc));
2106
            $session->set('user-duo_status', 'IN_PROGRESS');
2107
            $session->set('user-login', $username);
2108
            
2109
            // If we got here we can reset the password attempts
2110
            $session->set('pwd_attempts', 0);
2111
            
2112
            return [
2113
                'error' => false,
2114
                'message' => '',
2115
                'proceedIdentification' => false,
2116
                'duo_url_ready' => true,
2117
                'duo_redirect_url' => $duo_redirect_url,
2118
                'duo_failmode' => $duo_failmode,
2119
            ];
2120
        } else {
2121
            return [
2122
                'error' => true,
2123
                'message' => $duo_error . $lang->get('duo_error_url'),
2124
                'pwd_attempts' => (int) $sessionPwdAttempts,
2125
                'proceedIdentification' => false,
2126
            ];
2127
        }
2128
    } elseif ($duo_status === 'IN_PROGRESS' && $dataReceived['duo_code'] !== '') {
2129
        try {
2130
            // Check if the Duo code received is valid
2131
            $decoded_token = $duo_client->exchangeAuthorizationCodeFor2FAResult($dataReceived['duo_code'], $username);
2132
        } catch (DuoException $e) {
2133
            return [
2134
                'error' => true,
2135
                'message' => $lang->get('duo_error_decoding'),
2136
                'pwd_attempts' => (int) $sessionPwdAttempts,
2137
                'debug_message' => $e->getMessage(),
2138
                'proceedIdentification' => false,
2139
            ];
2140
        }
2141
        // return the response (which should be the user name)
2142
        if ($decoded_token['preferred_username'] === $username) {
2143
            $session->set('user-duo_status', 'COMPLET');
2144
            $session->set('user-duo_state','');
2145
            $session->set('user-duo_data','');
2146
            $session->set('user-login', $username);
2147
2148
            return [
2149
                'error' => false,
2150
                'message' => '',
2151
                'proceedIdentification' => true,
2152
                'authenticated_username' => $decoded_token['preferred_username']
2153
            ];
2154
        } else {
2155
            // Something wrong, username from the original Duo request is different than the one received now
2156
            $session->set('user-duo_status','');
2157
            $session->set('user-duo_state','');
2158
            $session->set('user-duo_data','');
2159
2160
            return [
2161
                'error' => true,
2162
                'message' => $lang->get('duo_login_mismatch'),
2163
                'pwd_attempts' => (int) $sessionPwdAttempts,
2164
                'proceedIdentification' => false,
2165
            ];
2166
        }
2167
    }
2168
    // If we are here something wrong
2169
    $session->set('user-duo_status','');
2170
    $session->set('user-duo_state','');
2171
    $session->set('user-duo_data','');
2172
    return [
2173
        'error' => true,
2174
        'message' => $lang->get('duo_login_mismatch'),
2175
        'pwd_attempts' => (int) $sessionPwdAttempts,
2176
        'proceedIdentification' => false,
2177
    ];
2178
}
2179
2180
/**
2181
 * Undocumented function.
2182
 *
2183
 * @param string                $passwordClear Password in clear
2184
 * @param array|string          $userInfo      Array of user data
2185
 *
2186
 * @return arrau
0 ignored issues
show
The type arrau was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
2187
 */
2188
function checkCredentials($passwordClear, $userInfo): array
2189
{
2190
    $passwordManager = new PasswordManager();
2191
2192
    // Strategy 1: Try with raw password (new behavior, correct way)
2193
    // Migrate password if needed
2194
    $result = $passwordManager->migratePassword(
2195
        $userInfo['pw'],
2196
        $passwordClear,
2197
        (int) $userInfo['id'],
2198
        (bool) $userInfo['admin']
2199
    );
2200
    
2201
    if ($result['status'] === true && $passwordManager->verifyPassword($result['hashedPassword'], $passwordClear) === true) {
2202
        // Password is correct with raw password (new behavior)
2203
        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...
2204
            'authenticated' => true,
2205
        ];
2206
    }
2207
2208
    // Strategy 2: Try with sanitized password (legacy behavior for backward compatibility)
2209
    // This handles users who registered before fix 3.1.5.10
2210
    $passwordSanitized = filter_var($passwordClear, FILTER_SANITIZE_FULL_SPECIAL_CHARS);
2211
2212
    // Only try sanitized version if it's different from the raw password
2213
    if ($passwordSanitized !== $passwordClear) {
2214
        $resultSanitized = $passwordManager->migratePassword(
2215
            $userInfo['pw'],
2216
            $passwordSanitized,
2217
            (int) $userInfo['id'],
2218
            (bool) $userInfo['admin']
2219
        );
2220
2221
        if ($resultSanitized['status'] === true && $passwordManager->verifyPassword($resultSanitized['hashedPassword'], $passwordSanitized) === true) {
2222
            // Password is correct with sanitized password (legacy behavior)
2223
            // This means the user's hash in DB was created with the sanitized version
2224
            // We need to MIGRATE: re-hash the RAW password and save it
2225
2226
            // Re-hash the raw (non-sanitized) password
2227
            $newHash = $passwordManager->hashPassword($passwordClear);
2228
            
2229
            $userCurrentPrivateKey = decryptPrivateKey($passwordSanitized, $userInfo['private_key']);
2230
            $newUserPrivateKey = encryptPrivateKey($passwordClear, $userCurrentPrivateKey);
2231
            
2232
            // Update user with new hash and mark migration as COMPLETE (0 = done)
2233
            DB::update(
2234
                prefixTable('users'),
2235
                [
2236
                    'pw' => $newHash,
2237
                    'needs_password_migration' => 0,  // 0 = migration completed
2238
                    'private_key' => $newUserPrivateKey,
2239
                ],
2240
                'id = %i',
2241
                $userInfo['id']
2242
            );
2243
2244
            // Log the migration event
2245
            $configManager = new ConfigManager();
2246
            logEvents(
2247
                $configManager->getAllSettings(),
2248
                'user_password',
2249
                'password_migrated_from_sanitized',
2250
                (string) $userInfo['id'],
2251
                stripslashes($userInfo['login'] ?? ''),
2252
                ''
2253
            );
2254
2255
            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...
2256
                'authenticated' => true,
2257
                'password_migrated' => true,
2258
                'private_key_reencrypted' => $newUserPrivateKey,
2259
            ];
2260
        }
2261
    }
2262
2263
    // Password is not correct with either method
2264
    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...
2265
        'authenticated' => false,
2266
    ];
2267
}
2268
2269
/**
2270
 * Undocumented function.
2271
 *
2272
 * @param bool   $enabled text1
2273
 * @param string $dbgFile text2
2274
 * @param string $text    text3
2275
 */
2276
function debugIdentify(bool $enabled, string $dbgFile, string $text): void
2277
{
2278
    if ($enabled === true) {
2279
        $fp = fopen($dbgFile, 'a');
2280
        if ($fp !== false) {
2281
            fwrite(
2282
                $fp,
2283
                $text
2284
            );
2285
        }
2286
    }
2287
}
2288
2289
2290
2291
function identifyGetUserCredentials(
2292
    array $SETTINGS,
2293
    string $serverPHPAuthUser,
2294
    string $serverPHPAuthPw,
2295
    string $userPassword,
2296
    string $userLogin
2297
): array
2298
{
2299
    if ((int) $SETTINGS['enable_http_request_login'] === 1
2300
        && $serverPHPAuthUser !== null
2301
        && (int) $SETTINGS['maintenance_mode'] === 1
2302
    ) {
2303
        if (strpos($serverPHPAuthUser, '@') !== false) {
2304
            return [
2305
                'username' => explode('@', $serverPHPAuthUser)[0],
2306
                'passwordClear' => $serverPHPAuthPw
2307
            ];
2308
        }
2309
        
2310
        if (strpos($serverPHPAuthUser, '\\') !== false) {
2311
            return [
2312
                'username' => explode('\\', $serverPHPAuthUser)[1],
2313
                'passwordClear' => $serverPHPAuthPw
2314
            ];
2315
        }
2316
2317
        return [
2318
            'username' => $serverPHPAuthPw,
2319
            'passwordClear' => $serverPHPAuthPw
2320
        ];
2321
    }
2322
    
2323
    return [
2324
        'username' => $userLogin,
2325
        'passwordClear' => $userPassword
2326
    ];
2327
}
2328
2329
2330
class initialChecks {
2331
    // Properties
2332
    public $login;
2333
2334
    /**
2335
     * Check if the user or his IP address is blocked due to a high number of
2336
     * failed attempts.
2337
     * 
2338
     * @param string $username - The login tried to login.
2339
     * @param string $ip - The remote address of the user.
2340
     */
2341
    public function isTooManyPasswordAttempts($username, $ip) {
2342
2343
        // Check for existing lock
2344
        $unlock_at = DB::queryFirstField(
2345
            'SELECT MAX(unlock_at)
2346
             FROM ' . prefixTable('auth_failures') . '
2347
             WHERE unlock_at > %s
2348
             AND ((source = %s AND value = %s) OR (source = %s AND value = %s))',
2349
            date('Y-m-d H:i:s', time()),
2350
            'login',
2351
            $username,
2352
            'remote_ip',
2353
            $ip
2354
        );
2355
2356
        // Account or remote address locked
2357
        if ($unlock_at) {
2358
            throw new Exception((string) $unlock_at);
2359
        }
2360
    }
2361
2362
    public function getUserInfo($login, $enable_ad_user_auto_creation, $oauth2_enabled) {
2363
        $session = SessionManager::getSession();
2364
    
2365
        // Get user info from DB with all related data
2366
        $data = getUserCompleteData($login);
2367
        
2368
        $dataUserCount = DB::count();
2369
        
2370
        // User doesn't exist then return error
2371
        // Except if user creation from LDAP is enabled
2372
        if (
2373
            $dataUserCount === 0
2374
            && !filter_var($enable_ad_user_auto_creation, FILTER_VALIDATE_BOOLEAN) 
2375
            && !filter_var($oauth2_enabled, FILTER_VALIDATE_BOOLEAN)
2376
        ) {
2377
            throw new Exception("error");
2378
        }
2379
2380
        // Check if similar login deleted exists
2381
        DB::queryFirstRow(
2382
            'SELECT id, login
2383
            FROM ' . prefixTable('users') . '
2384
            WHERE login LIKE %s AND deleted_at IS NOT NULL',
2385
            $login . '_deleted_%'
2386
        );
2387
2388
        if (DB::count() > 0) {
2389
            throw new Exception("error_user_deleted_exists");
2390
        }
2391
2392
        // We cannot create a user with LDAP if the OAuth2 login is ongoing
2393
        $data['oauth2_login_ongoing'] = filter_var($session->get('userOauth2Info')['oauth2LoginOngoing'] ?? false, FILTER_VALIDATE_BOOLEAN) ?? false;
2394
    
2395
        $data['ldap_user_to_be_created'] = (
2396
            filter_var($enable_ad_user_auto_creation, FILTER_VALIDATE_BOOLEAN) &&
2397
            $dataUserCount === 0 &&
2398
            !$data['oauth2_login_ongoing']
2399
        );
2400
        $data['oauth2_user_not_exists'] = (
2401
            filter_var($oauth2_enabled, FILTER_VALIDATE_BOOLEAN) &&
2402
            $dataUserCount === 0 &&
2403
            $data['oauth2_login_ongoing']
2404
        );
2405
2406
        return $data;
2407
    }
2408
2409
    public function isMaintenanceModeEnabled($maintenance_mode, $user_admin) {
2410
        if ((int) $maintenance_mode === 1 && (int) $user_admin === 0) {
2411
            throw new Exception(
2412
                "error" 
2413
            );
2414
        }
2415
    }
2416
2417
    public function is2faCodeRequired(
2418
        $yubico,
2419
        $ga,
2420
        $duo,
2421
        $admin,
2422
        $adminMfaRequired,
2423
        $mfa,
2424
        $userMfaSelection,
2425
        $userMfaEnabled
2426
    ) {
2427
        if (
2428
            (empty($userMfaSelection) === true &&
2429
            isOneVarOfArrayEqualToValue(
2430
                [
2431
                    (int) $yubico,
2432
                    (int) $ga,
2433
                    (int) $duo
2434
                ],
2435
                1
2436
            ) === true)
2437
            && (((int) $admin !== 1 && $userMfaEnabled === true) || ((int) $adminMfaRequired === 1 && (int) $admin === 1))
2438
            && $mfa === true
2439
        ) {
2440
            throw new Exception(
2441
                "error" 
2442
            );
2443
        }
2444
    }
2445
2446
    public function isInstallFolderPresent($admin, $install_folder) {
2447
        if ((int) $admin === 1 && is_dir($install_folder) === true) {
2448
            throw new Exception(
2449
                "error" 
2450
            );
2451
        }
2452
    }
2453
}
2454
2455
2456
/**
2457
 * Permit to get info about user before auth step
2458
 *
2459
 * @param array $SETTINGS
2460
 * @param integer $sessionPwdAttempts
2461
 * @param string $username
2462
 * @param integer $sessionAdmin
2463
 * @param string $sessionUrl
2464
 * @param string $user2faSelection
2465
 * @param boolean $oauth2Token
2466
 * @return array
2467
 */
2468
function identifyDoInitialChecks(
2469
    $SETTINGS,
2470
    int $sessionPwdAttempts,
2471
    string $username,
2472
    int $sessionAdmin,
2473
    string $sessionUrl,
2474
    string $user2faSelection
2475
): array
2476
{
2477
    $session = SessionManager::getSession();
2478
    $checks = new initialChecks();
2479
    $enableAdUserAutoCreation = $SETTINGS['enable_ad_user_auto_creation'] ?? false;
2480
    $oauth2Enabled = $SETTINGS['oauth2_enabled'] ?? false;
2481
    $lang = new Language($session->get('user-language') ?? 'english');
2482
2483
    // Brute force management
2484
    try {
2485
        $checks->isTooManyPasswordAttempts($username, getClientIpServer());
2486
    } catch (Exception $e) {
2487
        $session->set('userOauth2Info', '');
2488
        logEvents($SETTINGS, 'failed_auth', 'user_not_exists', '', stripslashes($username), stripslashes($username));
2489
        return [
2490
            'error' => true,
2491
            'skip_anti_bruteforce' => true,
2492
            'array' => [
2493
                'value' => 'bruteforce_wait',
2494
                'error' => true,
2495
                'message' => $lang->get('bruteforce_wait') . (string) $e->getMessage(),
2496
            ]
2497
        ];
2498
    }
2499
2500
    // Check if user exists
2501
    try {
2502
        $userInfo = $checks->getUserInfo($username, $enableAdUserAutoCreation, $oauth2Enabled);
2503
    } catch (Exception $e) {
2504
        logEvents($SETTINGS, 'failed_auth', 'user_not_exists', '', stripslashes($username), stripslashes($username));
2505
        return [
2506
            'error' => true,
2507
            'array' => [
2508
                'error' => true,
2509
                'message' => null !== $e->getMessage() && $e->getMessage() === 'error_user_deleted_exists' ? $lang->get('error_user_deleted_exists') : $lang->get('error_bad_credentials'),
2510
            ]
2511
        ];
2512
    }
2513
2514
    // Manage Maintenance mode
2515
    try {
2516
        $checks->isMaintenanceModeEnabled(
2517
            $SETTINGS['maintenance_mode'],
2518
            $userInfo['admin']
2519
        );
2520
    } catch (Exception $e) {
2521
        return [
2522
            'error' => true,
2523
            'skip_anti_bruteforce' => true,
2524
            'array' => [
2525
                'value' => '',
2526
                'error' => 'maintenance_mode_enabled',
2527
                'message' => '',
2528
            ]
2529
        ];
2530
    }
2531
    
2532
    // user should use MFA?
2533
    $userInfo['mfa_auth_requested_roles'] = mfa_auth_requested_roles(
2534
        (string) $userInfo['fonction_id'],
2535
        is_null($SETTINGS['mfa_for_roles']) === true ? '' : (string) $SETTINGS['mfa_for_roles']
2536
    );
2537
2538
    // Check if 2FA code is requested
2539
    try {
2540
        $checks->is2faCodeRequired(
2541
            $SETTINGS['yubico_authentication'],
2542
            $SETTINGS['google_authentication'],
2543
            $SETTINGS['duo'],
2544
            $userInfo['admin'],
2545
            $SETTINGS['admin_2fa_required'],
2546
            $userInfo['mfa_auth_requested_roles'],
2547
            $user2faSelection,
2548
            $userInfo['mfa_enabled']
2549
        );
2550
    } catch (Exception $e) {
2551
        return [
2552
            'error' => true,
2553
            'array' => [
2554
                'value' => '2fa_not_set',
2555
                'user_admin' => (int) $sessionAdmin,
2556
                'initial_url' => $sessionUrl,
2557
                'pwd_attempts' => (int) $sessionPwdAttempts,
2558
                'error' => '2fa_not_set',
2559
                'message' => $lang->get('select_valid_2fa_credentials'),
2560
            ]
2561
        ];
2562
    }
2563
    // If admin user then check if folder install exists
2564
    // if yes then refuse connection
2565
    try {
2566
        $checks->isInstallFolderPresent(
2567
            $userInfo['admin'],
2568
            '../install'
2569
        );
2570
    } catch (Exception $e) {
2571
        return [
2572
            'error' => true,
2573
            'array' => [
2574
                'value' => '',
2575
                'user_admin' => $sessionAdmin,
2576
                'initial_url' => $sessionUrl,
2577
                'pwd_attempts' => (int) $sessionPwdAttempts,
2578
                'error' => true,
2579
                'message' => $lang->get('remove_install_folder'),
2580
            ]
2581
        ];
2582
    }
2583
2584
    // Check if  migration of password hash has previously failed
2585
    //cleanFailedAuthRecords($userInfo);
2586
2587
    // Return some usefull information about user
2588
    return [
2589
        'error' => false,
2590
        'user_mfa_mode' => $user2faSelection,
2591
        'userInfo' => $userInfo,
2592
    ];
2593
}
2594
2595
function cleanFailedAuthRecords(array $userInfo) : void
2596
{
2597
    // Clean previous failed attempts
2598
    $failedTasks = DB::query(
2599
        'SELECT increment_id
2600
        FROM ' . prefixTable('background_tasks') . '
2601
        WHERE process_type = %s
2602
        AND JSON_EXTRACT(arguments, "$.new_user_id") = %i
2603
        AND status = %s',
2604
        'create_user_keys',
2605
        $userInfo['id'],
2606
        'failed'
2607
    );
2608
2609
    // Supprimer chaque tâche échouée et ses sous-tâches
2610
    foreach ($failedTasks as $task) {
2611
        $incrementId = $task['increment_id'];
2612
2613
        // Supprimer les sous-tâches associées
2614
        DB::delete(
2615
            prefixTable('background_subtasks'),
2616
            'sub_task_in_progress = %i',
2617
            $incrementId
2618
        );
2619
2620
        // Supprimer la tâche principale
2621
        DB::delete(
2622
            prefixTable('background_tasks'),
2623
            'increment_id = %i',
2624
            $incrementId
2625
        );
2626
    }
2627
}
2628
2629
function identifyDoLDAPChecks(
2630
    $SETTINGS,
2631
    $userInfo,
2632
    string $username,
2633
    string $passwordClear,
2634
    int $sessionAdmin,
2635
    string $sessionUrl,
2636
    int $sessionPwdAttempts
2637
): array
2638
{
2639
    $session = SessionManager::getSession();
2640
    $lang = new Language($session->get('user-language') ?? 'english');
2641
2642
    // Prepare LDAP connection if set up
2643
    if ((int) $SETTINGS['ldap_mode'] === 1
2644
        && $username !== 'admin'
2645
        && ((string) $userInfo['auth_type'] === 'ldap' || $userInfo['ldap_user_to_be_created'] === true)
2646
    ) {
2647
        $retLDAP = authenticateThroughAD(
2648
            $username,
2649
            $userInfo,
2650
            $passwordClear,
2651
            $SETTINGS
2652
        );
2653
        if ($retLDAP['error'] === true) {
2654
            return [
2655
                'error' => true,
2656
                'array' => [
2657
                    'value' => '',
2658
                    'user_admin' => $sessionAdmin,
2659
                    'initial_url' => $sessionUrl,
2660
                    'pwd_attempts' => (int) $sessionPwdAttempts,
2661
                    'error' => true,
2662
                    'message' => $lang->get('error_bad_credentials'),
2663
                ]
2664
            ];
2665
        }
2666
        return [
2667
            'error' => false,
2668
            'retLDAP' => $retLDAP,
2669
            'ldapConnection' => true,
2670
            'userPasswordVerified' => true,
2671
        ];
2672
    }
2673
2674
    // return if no addmin
2675
    return [
2676
        'error' => false,
2677
        'retLDAP' => [],
2678
        'ldapConnection' => false,
2679
        'userPasswordVerified' => false,
2680
    ];
2681
}
2682
2683
2684
function shouldUserAuthWithOauth2(
2685
    array $SETTINGS,
2686
    array $userInfo,
2687
    string $username
2688
): array
2689
{
2690
    // Security issue without this return if an user auth_type == oauth2 and
2691
    // oauth2 disabled : we can login as a valid user by using hashUserId(username)
2692
    // as password in the login the form.
2693
    if ((int) $SETTINGS['oauth2_enabled'] !== 1 && filter_var($userInfo['oauth2_login_ongoing'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) === true) {
2694
        return [
2695
            'error' => true,
2696
            'message' => 'user_not_allowed_to_auth_to_teampass_app',
2697
            'oauth2Connection' => false,
2698
            'userPasswordVerified' => false,
2699
        ];
2700
    }
2701
2702
    // Prepare Oauth2 connection if set up
2703
    if ($username !== 'admin') {
2704
        // User has started to auth with oauth2
2705
        if ((bool) $userInfo['oauth2_login_ongoing'] === true) {
2706
            // Case where user exists in Teampass password login type
2707
            if ((string) $userInfo['auth_type'] === 'ldap' || (string) $userInfo['auth_type'] === 'local') {
2708
                // Add transparent recovery data if available
2709
                /*if (isset($userKeys['user_seed'])) {
2710
                    // Recrypt user private key with oauth2
2711
                    recryptUserPrivateKeyWithOauth2(
2712
                        (int) $userInfo['id'],
2713
                        $userKeys['user_seed'],
2714
                        $userKeys['public_key']
2715
                    );
2716
                }*/
2717
                
2718
                // Update user in database:
2719
                DB::update(
2720
                    prefixTable('users'),
2721
                    array(
2722
                        //'special' => 'recrypt-private-key',
2723
                        'auth_type' => 'oauth2',
2724
                    ),
2725
                    'id = %i',
2726
                    $userInfo['id']
2727
                );
2728
                // Update session auth type
2729
                $session = SessionManager::getSession();
2730
                $session->set('user-auth_type', 'oauth2');
2731
                // Accept login request
2732
                return [
2733
                    'error' => false,
2734
                    'message' => '',
2735
                    'oauth2Connection' => true,
2736
                    'userPasswordVerified' => true,
2737
                ];
2738
            } elseif ((string) $userInfo['auth_type'] === 'oauth2' || (bool) $userInfo['oauth2_login_ongoing'] === true) {
2739
                // OAuth2 login request on OAuth2 user account.
2740
                return [
2741
                    'error' => false,
2742
                    'message' => '',
2743
                    'oauth2Connection' => true,
2744
                    'userPasswordVerified' => true,
2745
                ];
2746
            } else {
2747
                // Case where auth_type is not managed
2748
                return [
2749
                    'error' => true,
2750
                    'message' => 'user_not_allowed_to_auth_to_teampass_app',
2751
                    'oauth2Connection' => false,
2752
                    'userPasswordVerified' => false,
2753
                ];
2754
            }
2755
        } else {
2756
            // User has started to auth the normal way
2757
            if ((string) $userInfo['auth_type'] === 'oauth2') {
2758
                // Case where user exists in Teampass but not allowed to auth with Oauth2
2759
                return [
2760
                    'error' => true,
2761
                    'message' => 'error_bad_credentials',
2762
                    'oauth2Connection' => false,
2763
                    'userPasswordVerified' => false,
2764
                ];
2765
            }
2766
        }
2767
    }
2768
2769
    // return if no addmin
2770
    return [
2771
        'error' => false,
2772
        'message' => '',
2773
        'oauth2Connection' => false,
2774
        'userPasswordVerified' => false,
2775
    ];
2776
}
2777
2778
function checkOauth2User(
2779
    array $SETTINGS,
2780
    array $userInfo,
2781
    string $username,
2782
    string $passwordClear,
2783
    int $userLdapHasBeenCreated
2784
): array
2785
{
2786
    // Is oauth2 user in Teampass?
2787
    if ((int) $SETTINGS['oauth2_enabled'] === 1
2788
        && $username !== 'admin'
2789
        && filter_var($userInfo['oauth2_user_not_exists'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) === true
2790
        && (int) $userLdapHasBeenCreated === 0
2791
    ) {
2792
        // Is allowed to self register with oauth2?
2793
        if (empty($SETTINGS['oauth_self_register_groups'])) {
2794
            // No self registration is allowed
2795
            return [
2796
                'error' => true,
2797
                'message' => 'error_bad_credentials',
2798
            ];
2799
        } else {
2800
            // Self registration is allowed
2801
            // Create user in Teampass
2802
            $userInfo['oauth2_user_to_be_created'] = true;
2803
            return createOauth2User(
2804
                $SETTINGS,
2805
                $userInfo,
2806
                $username,
2807
                $passwordClear,
2808
                true
2809
            );
2810
        }
2811
    
2812
    } elseif (isset($userInfo['id']) === true && empty($userInfo['id']) === false) {
2813
        // User is in construction, please wait for email
2814
        if (
2815
            isset($userInfo['is_ready_for_usage']) && (int) $userInfo['is_ready_for_usage'] !== 1 && 
2816
            $userInfo['ongoing_process_id'] !== null && (int) $userInfo['ongoing_process_id'] >= 0
2817
        ) {
2818
            // Check if the creation of user keys has failed
2819
            $errorMessage = checkIfUserKeyCreationFailed((int) $userInfo['id']);
2820
            if (!is_null($errorMessage)) {
2821
                // Refresh user info to permit retry
2822
                DB::update(
2823
                    prefixTable('users'),
2824
                    array(
2825
                        'is_ready_for_usage' => 1,
2826
                        'otp_provided' => 1,
2827
                        'ongoing_process_id' => NULL,
2828
                        'special' => 'none',
2829
                    ),
2830
                    'id = %i',
2831
                    $userInfo['id']
2832
                );
2833
2834
                // Prepare task to create user keys again
2835
                handleUserKeys(
2836
                    (int) $userInfo['id'],
2837
                    (string) $passwordClear,
2838
                    (int) NUMBER_ITEMS_IN_BATCH,
2839
                    '',
2840
                    true,
2841
                    true,
2842
                    true,
2843
                    false,
2844
                    'email_body_user_config_4',
2845
                    true,
2846
                    '',
2847
                    '',
2848
                );
2849
2850
                // Return error message
2851
                return [
2852
                    'error' => true,
2853
                    'message' => $errorMessage,
2854
                    'no_log_event' => true
2855
                ];
2856
            } else {
2857
                return [
2858
                    'error' => true,
2859
                    'message' => 'account_in_construction_please_wait_email',
2860
                    'no_log_event' => true
2861
                ];
2862
            }
2863
        }
2864
2865
        // CCheck if user should use oauth2
2866
        $ret = shouldUserAuthWithOauth2(
2867
            $SETTINGS,
2868
            $userInfo,
2869
            $username
2870
        );
2871
        if ($ret['error'] === true) {
2872
            return [
2873
                'error' => true,
2874
                'message' => $ret['message'],
2875
            ];
2876
        }
2877
2878
        // login/password attempt on a local account:
2879
        // Return to avoid overwrite of user password that can allow a user
2880
        // to steal a local account.
2881
        if (!$ret['oauth2Connection'] || !$ret['userPasswordVerified']) {
2882
            return [
2883
                'error' => false,
2884
                'message' => $ret['message'],
2885
                'ldapConnection' => false,
2886
                'userPasswordVerified' => false,        
2887
            ];
2888
        }
2889
2890
        // Oauth2 user already exists and authenticated
2891
        $userInfo['has_been_created'] = 0;
2892
        $passwordManager = new PasswordManager();
2893
2894
        // Update user hash un database if needed
2895
        if (!$passwordManager->verifyPassword($userInfo['pw'], $passwordClear)) {
2896
            DB::update(
2897
                prefixTable('users'),
2898
                [
2899
                    'pw' => $passwordManager->hashPassword($passwordClear),
2900
                ],
2901
                'id = %i',
2902
                $userInfo['id']
2903
            );
2904
2905
            // Transparent recovery: handle external OAuth2 password change
2906
            handleExternalPasswordChange(
2907
                (int) $userInfo['id'],
2908
                $passwordClear,
2909
                $userInfo,
2910
                $SETTINGS
2911
            );
2912
        }
2913
2914
        return [
2915
            'error' => false,
2916
            'retExternalAD' => $userInfo,
2917
            'oauth2Connection' => $ret['oauth2Connection'],
2918
            'userPasswordVerified' => $ret['userPasswordVerified'],
2919
        ];
2920
    }
2921
2922
    // return if no admin
2923
    return [
2924
        'error' => false,
2925
        'retLDAP' => [],
2926
        'ldapConnection' => false,
2927
        'userPasswordVerified' => false,
2928
    ];
2929
}
2930
2931
/**
2932
 * Check if a "create_user_keys" task failed for the given user_id.
2933
 *
2934
 * @param int $userId The user ID to check.
2935
 * @return string|null Returns an error message in English if a failed task is found, otherwise null.
2936
 */
2937
function checkIfUserKeyCreationFailed(int $userId): ?string
2938
{
2939
    // Find the latest "create_user_keys" task for the given user_id
2940
    $latestTask = DB::queryFirstRow(
2941
        'SELECT arguments, status FROM ' . prefixTable('background_tasks') . '
2942
        WHERE process_type = %s
2943
        AND arguments LIKE %s
2944
        ORDER BY increment_id DESC
2945
        LIMIT 1',
2946
        'create_user_keys', '%"new_user_id":' . $userId . '%'
2947
    );
2948
2949
    // If a failed task is found, return an error message
2950
    if ($latestTask && $latestTask['status'] === 'failed') {
2951
        return "The creation of user keys for user ID {$userId} failed. Please contact your administrator to check the background tasks log for more details.";
2952
    }
2953
2954
    // No failed task found for this user_id
2955
    return null;
2956
}
2957
2958
2959
/* * Create the user in Teampass
2960
 *
2961
 * @param array $SETTINGS
2962
 * @param array $userInfo
2963
 * @param string $username
2964
 * @param string $passwordClear
2965
 *
2966
 * @return array
2967
 */
2968
function createOauth2User(
2969
    array $SETTINGS,
2970
    array $userInfo,
2971
    string $username,
2972
    string $passwordClear,
2973
    bool $userSelfRegister = false
2974
): array
2975
{
2976
    // Prepare creating the new oauth2 user in Teampass
2977
    if ((int) $SETTINGS['oauth2_enabled'] === 1
2978
        && $username !== 'admin'
2979
        && filter_var($userInfo['oauth2_user_to_be_created'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) === true
2980
    ) {
2981
        $session = SessionManager::getSession();    
2982
        $lang = new Language($session->get('user-language') ?? 'english');
2983
2984
        // Prepare user groups
2985
        foreach ($userInfo['groups'] as $key => $group) {
2986
            // Check if the group is in the list of groups allowed to self register
2987
            // If the group is in the list, we remove it
2988
            if ($userSelfRegister === true && $group["displayName"] === $SETTINGS['oauth_self_register_groups']) {
2989
                unset($userInfo['groups'][$key]);
2990
            }
2991
        }
2992
        // Rebuild indexes
2993
        $userInfo['groups'] = array_values($userInfo['groups'] ?? []);
2994
        
2995
        // Create Oauth2 user if not exists and tasks enabled
2996
        $ret = externalAdCreateUser(
2997
            $username,
2998
            $passwordClear,
2999
            $userInfo['mail'] ?? '',
3000
            is_null($userInfo['givenname']) ? (is_null($userInfo['givenName']) ? '' : $userInfo['givenName']) : $userInfo['givenname'],
3001
            is_null($userInfo['surname']) ? '' : $userInfo['surname'],
3002
            'oauth2',
3003
            is_null($userInfo['groups']) ? [] : $userInfo['groups'],
3004
            $SETTINGS
3005
        );
3006
        $userInfo = array_merge($userInfo, $ret);
3007
3008
        // prepapre background tasks for item keys generation  
3009
        handleUserKeys(
3010
            (int) $userInfo['id'],
3011
            (string) $passwordClear,
3012
            (int) (isset($SETTINGS['maximum_number_of_items_to_treat']) === true ? $SETTINGS['maximum_number_of_items_to_treat'] : NUMBER_ITEMS_IN_BATCH),
3013
            uniqidReal(20),
3014
            true,
3015
            true,
3016
            true,
3017
            false,
3018
            $lang->get('email_body_user_config_2'),
3019
        );
3020
3021
        // Complete $userInfo
3022
        $userInfo['has_been_created'] = 1;
3023
3024
        if (WIP === true) error_log("--- USER CREATED ---");
3025
3026
        return [
3027
            'error' => false,
3028
            'retExternalAD' => $userInfo,
3029
            'oauth2Connection' => true,
3030
            'userPasswordVerified' => true,
3031
        ];
3032
    
3033
    } elseif (isset($userInfo['id']) === true && empty($userInfo['id']) === false) {
3034
        // CHeck if user should use oauth2
3035
        $ret = shouldUserAuthWithOauth2(
3036
            $SETTINGS,
3037
            $userInfo,
3038
            $username
3039
        );
3040
        if ($ret['error'] === true) {
3041
            return [
3042
                'error' => true,
3043
                'message' => $ret['message'],
3044
            ];
3045
        }
3046
3047
        // login/password attempt on a local account:
3048
        // Return to avoid overwrite of user password that can allow a user
3049
        // to steal a local account.
3050
        if (!$ret['oauth2Connection'] || !$ret['userPasswordVerified']) {
3051
            return [
3052
                'error' => false,
3053
                'message' => $ret['message'],
3054
                'ldapConnection' => false,
3055
                'userPasswordVerified' => false,        
3056
            ];
3057
        }
3058
3059
        // Oauth2 user already exists and authenticated
3060
        if (WIP === true) error_log("--- USER AUTHENTICATED ---");
3061
        $userInfo['has_been_created'] = 0;
3062
3063
        $passwordManager = new PasswordManager();
3064
3065
        // Update user hash un database if needed
3066
        if (!$passwordManager->verifyPassword($userInfo['pw'], $passwordClear)) {
3067
            DB::update(
3068
                prefixTable('users'),
3069
                [
3070
                    'pw' => $passwordManager->hashPassword($passwordClear),
3071
                ],
3072
                'id = %i',
3073
                $userInfo['id']
3074
            );
3075
        }
3076
3077
        return [
3078
            'error' => false,
3079
            'retExternalAD' => $userInfo,
3080
            'oauth2Connection' => $ret['oauth2Connection'],
3081
            'userPasswordVerified' => $ret['userPasswordVerified'],
3082
        ];
3083
    }
3084
3085
    // return if no admin
3086
    return [
3087
        'error' => false,
3088
        'retLDAP' => [],
3089
        'ldapConnection' => false,
3090
        'userPasswordVerified' => false,
3091
    ];
3092
}
3093
3094
function identifyDoMFAChecks(
3095
    $SETTINGS,
3096
    $userInfo,
3097
    $dataReceived,
3098
    $userInitialData,
3099
    string $username
3100
): array
3101
{
3102
    $session = SessionManager::getSession();
3103
    $lang = new Language($session->get('user-language') ?? 'english');
3104
    
3105
    switch ($userInitialData['user_mfa_mode']) {
3106
        case 'google':
3107
            $ret = googleMFACheck(
3108
                $username,
3109
                $userInfo,
3110
                $dataReceived,
3111
                $SETTINGS
3112
            );
3113
            if ($ret['error'] !== false) {
3114
                logEvents($SETTINGS, 'failed_auth', 'wrong_mfa_code', '', stripslashes($username), stripslashes($username));
3115
                return [
3116
                    'error' => true,
3117
                    'mfaData' => $ret,
3118
                    'mfaQRCodeInfos' => false,
3119
                ];
3120
            }
3121
3122
            return [
3123
                'error' => false,
3124
                'mfaData' => $ret['firstTime'],
3125
                'mfaQRCodeInfos' => $userInitialData['user_mfa_mode'] === 'google'
3126
                && count($ret['firstTime']) > 0 ? true : false,
3127
            ];
3128
        
3129
        case 'duo':
3130
            // Prepare Duo connection if set up
3131
            $checks = duoMFACheck(
3132
                $username,
3133
                $dataReceived,
3134
                $SETTINGS
3135
            );
3136
3137
            if ($checks['error'] === true) {
3138
                return [
3139
                    'error' => true,
3140
                    'mfaData' => $checks,
3141
                    'mfaQRCodeInfos' => false,
3142
                ];
3143
            }
3144
3145
            // If we are here
3146
            // Do DUO authentication
3147
            $ret = duoMFAPerform(
3148
                $username,
3149
                $dataReceived,
3150
                $checks['pwd_attempts'],
3151
                $checks['saved_state'],
3152
                $checks['duo_status'],
3153
                $SETTINGS
3154
            );
3155
3156
            if ($ret['error'] !== false) {
3157
                logEvents($SETTINGS, 'failed_auth', 'bad_duo_mfa', '', stripslashes($username), stripslashes($username));
3158
                $session->set('user-duo_status','');
3159
                $session->set('user-duo_state','');
3160
                $session->set('user-duo_data','');
3161
                return [
3162
                    'error' => true,
3163
                    'mfaData' => $ret,
3164
                    'mfaQRCodeInfos' => false,
3165
                ];
3166
            } else if ($ret['duo_url_ready'] === true){
3167
                return [
3168
                    'error' => false,
3169
                    'mfaData' => $ret,
3170
                    'duo_url_ready' => true,
3171
                    'mfaQRCodeInfos' => false,
3172
                ];
3173
            } else if ($ret['error'] === false) {
3174
                return [
3175
                    'error' => false,
3176
                    'mfaData' => $ret,
3177
                    'mfaQRCodeInfos' => false,
3178
                ];
3179
            }
3180
            break;
3181
        
3182
        default:
3183
            logEvents($SETTINGS, 'failed_auth', 'wrong_mfa_code', '', stripslashes($username), stripslashes($username));
3184
            return [
3185
                'error' => true,
3186
                'mfaData' => ['message' => $lang->get('wrong_mfa_code')],
3187
                'mfaQRCodeInfos' => false,
3188
            ];
3189
    }
3190
3191
    // If something went wrong, let's catch and return an error
3192
    logEvents($SETTINGS, 'failed_auth', 'wrong_mfa_code', '', stripslashes($username), stripslashes($username));
3193
    return [
3194
        'error' => true,
3195
        'mfaData' => ['message' => $lang->get('wrong_mfa_code')],
3196
        'mfaQRCodeInfos' => false,
3197
    ];
3198
}
3199
3200
function identifyDoAzureChecks(
3201
    array $SETTINGS,
3202
    $userInfo,
3203
    string $username
3204
): array
3205
{
3206
    $session = SessionManager::getSession();
3207
    $lang = new Language($session->get('user-language') ?? 'english');
3208
3209
    logEvents($SETTINGS, 'failed_auth', 'wrong_mfa_code', '', stripslashes($username), stripslashes($username));
3210
    return [
3211
        'error' => true,
3212
        'mfaData' => ['message' => $lang->get('wrong_mfa_code')],
3213
        'mfaQRCodeInfos' => false,
3214
    ];
3215
}
3216
3217
/**
3218
 * Add a failed authentication attempt to the database.
3219
 * If the number of failed attempts exceeds the limit, a lock is triggered.
3220
 * 
3221
 * @param string $source - The source of the failed attempt (login or remote_ip).
3222
 * @param string $value  - The value for this source (username or IP address).
3223
 * @param int    $limit  - The failure attempt limit after which the account/IP
3224
 *                         will be locked.
3225
 */
3226
function handleFailedAttempts($source, $value, $limit) {
3227
    // Count failed attempts from this source
3228
    $count = DB::queryFirstField(
3229
        'SELECT COUNT(*)
3230
        FROM ' . prefixTable('auth_failures') . '
3231
        WHERE source = %s AND value = %s',
3232
        $source,
3233
        $value
3234
    );
3235
3236
    // Add this attempt
3237
    $count++;
3238
3239
    // Calculate unlock time if number of attempts exceeds limit
3240
    $unlock_at = $count >= $limit
3241
        ? date('Y-m-d H:i:s', time() + (($count - $limit + 1) * 600))
3242
        : NULL;
3243
3244
    // Unlock account one time code
3245
    $unlock_code = ($count >= $limit && $source === 'login')
3246
        ? generateQuickPassword(30, false)
3247
        : NULL;
3248
3249
    // Insert the new failure into the database
3250
    DB::insert(
3251
        prefixTable('auth_failures'),
3252
        [
3253
            'source' => $source,
3254
            'value' => $value,
3255
            'unlock_at' => $unlock_at,
3256
            'unlock_code' => $unlock_code,
3257
        ]
3258
    );
3259
3260
    if ($unlock_at !== null && $source === 'login') {
3261
        $configManager = new ConfigManager();
3262
        $SETTINGS = $configManager->getAllSettings();
3263
        $lang = new Language($SETTINGS['default_language']);
3264
3265
        // Get user email
3266
        $userInfos = DB::queryFirstRow(
3267
            'SELECT email, name
3268
             FROM '.prefixTable('users').'
3269
             WHERE login = %s',
3270
             $value
3271
        );
3272
3273
        // No valid email address for user
3274
        if (!$userInfos || !filter_var($userInfos['email'], FILTER_VALIDATE_EMAIL))
3275
            return;
3276
3277
        $unlock_url = $SETTINGS['cpassman_url'].'/self-unlock.php?login='.$value.'&otp='.$unlock_code;
3278
3279
        sendMailToUser(
3280
            $userInfos['email'],
3281
            $lang->get('bruteforce_reset_mail_body'),
3282
            $lang->get('bruteforce_reset_mail_subject'),
3283
            [
3284
                '#name#' => $userInfos['name'],
3285
                '#reset_url#' => $unlock_url,
3286
                '#unlock_at#' => $unlock_at,
3287
            ],
3288
            true
3289
        );
3290
    }
3291
}
3292
3293
/**
3294
 * Add failed authentication attempts for both user login and IP address.
3295
 * This function will check the number of attempts for both the username and IP,
3296
 * and will trigger a lock if the number exceeds the defined limits.
3297
 * It also deletes logs older than 24 hours.
3298
 * 
3299
 * @param string $username - The username that was attempted to login.
3300
 * @param string $ip       - The IP address from which the login attempt was made.
3301
 */
3302
function addFailedAuthentication($username, $ip) {
3303
    $user_limit = 10;
3304
    $ip_limit = 30;
3305
3306
    // Remove old logs (more than 24 hours)
3307
    DB::delete(
3308
        prefixTable('auth_failures'),
3309
        'date < %s AND (unlock_at < %s OR unlock_at IS NULL)',
3310
        date('Y-m-d H:i:s', time() - (24 * 3600)),
3311
        date('Y-m-d H:i:s', time())
3312
    );
3313
3314
    // Add attempts in database
3315
    handleFailedAttempts('login', $username, $user_limit);
3316
    handleFailedAttempts('remote_ip', $ip, $ip_limit);
3317
}
3318