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

applyRoleNeedlePermissions()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 36
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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

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

filter:
    dependency_paths: ["lib/*"]

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

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