Passed
Branch feature/code_clean (75ffa1)
by Nils
06:31
created

createOauth2User()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 23
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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