buildAuthResponse()   F
last analyzed

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