initialChecks::isMaintenanceModeEnabled()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 3
nc 2
nop 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
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
    $session = SessionManager::getSession();
143
    
144
    // Prepare GET variables
145
    $sessionAdmin = $session->get('user-admin');
146
    $sessionPwdAttempts = $session->get('pwd_attempts');
147
    $sessionUrl = $session->get('user-initial_url');
148
    $server = [];
149
    $server['PHP_AUTH_USER'] =  $request->getUser();
150
    $server['PHP_AUTH_PW'] = $request->getPassword();
151
    
152
    // decrypt and retreive data in JSON format
153
    if ($session->get('key') === null) {
154
        $dataReceived = $sentData;
155
    } else {
156
        $dataReceived = prepareExchangedData(
157
            $sentData,
158
            'decode',
159
            $session->get('key')
160
        );
161
    }
162
163
    // Sanitize input data
164
    $toClean = [
165
        'login' => 'trim|escape',
166
    ];
167
    $dataReceived = sanitizeData($dataReceived, $toClean);
0 ignored issues
show
Bug introduced by
It seems like $dataReceived can also be of type string; however, parameter $rawData of sanitizeData() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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