Passed
Push — master ( e9bc10...106709 )
by Nils
07:23
created

buildAuthResponse()   F

Complexity

Conditions 22
Paths > 20000

Size

Total Lines 51
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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