Passed
Push — master ( 12ac9e...e9bc10 )
by Nils
06:04
created

identifyDoInitialChecks()   C

Complexity

Conditions 9
Paths 9

Size

Total Lines 124
Code Lines 79

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 9
eloc 79
c 2
b 0
f 0
nc 9
nop 6
dl 0
loc 124
rs 6.9026

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

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