Passed
Pull Request — master (#4496)
by
unknown
07:43
created

handleFailedAttempts()   B

Complexity

Conditions 8
Paths 24

Size

Total Lines 63
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 8
eloc 37
c 2
b 0
f 0
nc 24
nop 3
dl 0
loc 63
rs 8.0835

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

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