Passed
Push — master ( 5a3ed5...a919c1 )
by Nils
06:37 queued 14s
created

checkOauth2User()   C

Complexity

Conditions 15
Paths 8

Size

Total Lines 98
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 15
eloc 53
c 2
b 0
f 0
nc 8
nop 5
dl 0
loc 98
rs 5.9166

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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