initialChecks   A
last analyzed

Complexity

Total Complexity 24

Size/Duplication

Total Lines 112
Duplicated Lines 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 50
c 3
b 1
f 0
dl 0
loc 112
rs 10
wmc 24

5 Methods

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