Issues (24)

Security Analysis    no vulnerabilities found

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  Header Injection
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

sources/identify.php (5 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Teampass - a collaborative passwords manager.
7
 * ---
8
 * This file is part of the TeamPass project.
9
 * 
10
 * TeamPass is free software: you can redistribute it and/or modify it
11
 * under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, version 3 of the License.
13
 * 
14
 * TeamPass is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 * 
19
 * You should have received a copy of the GNU General Public License
20
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21
 * 
22
 * Certain components of this file may be under different licenses. For
23
 * details, see the `licenses` directory or individual file headers.
24
 * ---
25
 * @file      identify.php
26
 * @author    Nils Laumaillé ([email protected])
27
 * @copyright 2009-2025 Teampass.net
28
 * @license   GPL-3.0
29
 * @see       https://www.teampass.net
30
 */
31
32
use voku\helper\AntiXSS;
33
use TeampassClasses\SessionManager\SessionManager;
34
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
35
use TeampassClasses\Language\Language;
36
use TeampassClasses\PerformChecks\PerformChecks;
37
use TeampassClasses\ConfigManager\ConfigManager;
38
use TeampassClasses\NestedTree\NestedTree;
39
use TeampassClasses\PasswordManager\PasswordManager;
40
use Duo\DuoUniversal\Client;
41
use Duo\DuoUniversal\DuoException;
42
use RobThree\Auth\TwoFactorAuth;
43
use TeampassClasses\LdapExtra\LdapExtra;
44
use TeampassClasses\LdapExtra\OpenLdapExtra;
45
use TeampassClasses\LdapExtra\ActiveDirectoryExtra;
46
use TeampassClasses\OAuth2Controller\OAuth2Controller;
47
48
// Load functions
49
require_once 'main.functions.php';
50
51
// init
52
loadClasses('DB');
53
$session = SessionManager::getSession();
54
$request = SymfonyRequest::createFromGlobals();
55
$lang = new Language($session->get('user-language') ?? 'english');
56
57
// Load config
58
$configManager = new ConfigManager();
59
$SETTINGS = $configManager->getAllSettings();
60
61
// Define Timezone
62
date_default_timezone_set($SETTINGS['timezone'] ?? 'UTC');
63
64
// Set header properties
65
header('Content-type: text/html; charset=utf-8');
66
header('Cache-Control: no-cache, no-store, must-revalidate');
67
error_reporting(E_ERROR);
68
69
// --------------------------------- //
70
71
// Prepare POST variables
72
$post_type = filter_input(INPUT_POST, 'type', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
73
$post_login = filter_input(INPUT_POST, 'login', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
74
$post_data = filter_input(INPUT_POST, 'data', FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_FLAG_NO_ENCODE_QUOTES);
75
76
if ($post_type === 'identify_user') {
77
    //--------
78
    // NORMAL IDENTICATION STEP
79
    //--------
80
81
    // Ensure Complexity levels are translated
82
    defineComplexity();
83
84
    // Identify the user through Teampass process
85
    identifyUser($post_data, $SETTINGS);
86
87
    // ---
88
    // ---
89
    // ---
90
} elseif ($post_type === 'get2FAMethods') {
91
    //--------
92
    // Get MFA methods
93
    //--------
94
    //
95
96
    // Encrypt data to return
97
    echo json_encode([
98
        'ret' => prepareExchangedData(
99
            [
100
                'agses' => isKeyExistingAndEqual('agses_authentication_enabled', 1, $SETTINGS) === true ? true : false,
101
                'google' => isKeyExistingAndEqual('google_authentication', 1, $SETTINGS) === true ? true : false,
102
                'yubico' => isKeyExistingAndEqual('yubico_authentication', 1, $SETTINGS) === true ? true : false,
103
                'duo' => isKeyExistingAndEqual('duo', 1, $SETTINGS) === true ? true : false,
104
            ],
105
            'encode'
106
        ),
107
        'key' => $session->get('key'),
108
    ]);
109
    return false;
110
} elseif ($post_type === 'initiateSSOLogin') {
111
    //--------
112
    // Do initiateSSOLogin
113
    //--------
114
    //
115
116
    // Création d'une instance du contrôleur
117
    $OAuth2 = new OAuth2Controller($SETTINGS);
118
119
    // Redirection vers Azure pour l'authentification
120
    $OAuth2->redirect();
121
122
    // Encrypt data to return
123
    echo json_encode([
124
        'key' => $session->get('key'),
125
    ]);
126
    return false;
127
}
128
129
/**
130
 * Complete authentication of user through Teampass
131
 *
132
 * @param string $sentData Credentials
133
 * @param array $SETTINGS Teampass settings
134
 *
135
 * @return bool
136
 */
137
function identifyUser(string $sentData, array $SETTINGS): bool
138
{
139
    $antiXss = new AntiXSS();
140
    $session = SessionManager::getSession();
141
    $request = SymfonyRequest::createFromGlobals();
142
    $lang = new Language($session->get('user-language') ?? 'english');
143
    $session = SessionManager::getSession();
144
145
    // Prepare GET variables
146
    $sessionAdmin = $session->get('user-admin');
147
    $sessionPwdAttempts = $session->get('pwd_attempts');
148
    $sessionUrl = $session->get('user-initial_url');
149
    $server = [];
150
    $server['PHP_AUTH_USER'] =  $request->getUser();
151
    $server['PHP_AUTH_PW'] = $request->getPassword();
152
    
153
    // decrypt and retreive data in JSON format
154
    if ($session->get('key') === null) {
155
        $dataReceived = $sentData;
156
    } else {
157
        $dataReceived = prepareExchangedData(
158
            $sentData,
159
            'decode',
160
            $session->get('key')
161
        );
162
    }
163
164
    // Base64 decode sensitive data
165
    if (isset($dataReceived['pw'])) {
166
        $dataReceived['pw'] = base64_decode($dataReceived['pw']);
167
    }
168
169
    // Check if Duo auth is in progress and pass the pw and login back to the standard login process
170
    if(
171
        isKeyExistingAndEqual('duo', 1, $SETTINGS) === true
172
        && $dataReceived['user_2fa_selection'] === 'duo'
173
        && $session->get('user-duo_status') === 'IN_PROGRESS'
174
        && !empty($dataReceived['duo_state'])
175
    ){
176
        $key = hash('sha256', $dataReceived['duo_state']);
177
        $iv = substr(hash('sha256', $dataReceived['duo_state']), 0, 16);
178
        $duo_data_dec = openssl_decrypt(base64_decode($session->get('user-duo_data')), 'AES-256-CBC', $key, 0, $iv);
179
        // Clear the data from the Duo process to continue clean with the standard login process
180
        $session->set('user-duo_data','');
181
        if($duo_data_dec === false) {
182
            // Add failed authentication log
183
            addFailedAuthentication(filter_var($dataReceived['login'], FILTER_SANITIZE_FULL_SPECIAL_CHARS), getClientIpServer());
184
185
            echo prepareExchangedData(
186
                [
187
                    'error' => true,
188
                    'message' => $lang->get('duo_error_decrypt'),
189
                ],
190
                'encode'
191
            );
192
            return false;
193
        }
194
        $duo_data = unserialize($duo_data_dec);
195
        $dataReceived['pw'] = $duo_data['duo_pwd'];
196
        $dataReceived['login'] = $duo_data['duo_login'];
197
    }
198
199
    if(isset($dataReceived['pw']) === false || isset($dataReceived['login']) === false) {
200
        echo json_encode([
201
            'data' => prepareExchangedData(
202
                [
203
                    'error' => true,
204
                    'message' => $lang->get('ga_enter_credentials'),
205
                ],
206
                'encode'
207
            ),
208
            'key' => $session->get('key')
209
        ]);
210
        return false;
211
    }
212
213
    // prepare variables    
214
    $userCredentials = identifyGetUserCredentials(
215
        $SETTINGS,
216
        (string) $server['PHP_AUTH_USER'],
217
        (string) $server['PHP_AUTH_PW'],
218
        (string) filter_var($dataReceived['pw'], FILTER_SANITIZE_FULL_SPECIAL_CHARS),
219
        (string) filter_var($dataReceived['login'], FILTER_SANITIZE_FULL_SPECIAL_CHARS)
220
    );
221
    $username = $userCredentials['username'];
222
    $passwordClear = $userCredentials['passwordClear'];
223
224
    // DO initial checks
225
    $userInitialData = identifyDoInitialChecks(
226
        $SETTINGS,
227
        (int) $sessionPwdAttempts,
228
        (string) $username,
229
        (int) $sessionAdmin,
230
        (string) $sessionUrl,
231
        (string) filter_var($dataReceived['user_2fa_selection'], FILTER_SANITIZE_FULL_SPECIAL_CHARS)
232
    );
233
234
    // if user doesn't exist in Teampass then return error
235
    if ($userInitialData['error'] === true) {
236
        // Add log on error unless skip_anti_bruteforce flag is set to true
237
        if (empty($userInitialData['skip_anti_bruteforce'])
238
            || !$userInitialData['skip_anti_bruteforce']) {
239
240
            // Add failed authentication log
241
            addFailedAuthentication($username, getClientIpServer());
242
        }
243
244
        echo prepareExchangedData(
245
            $userInitialData['array'],
246
            'encode'
247
        );
248
        return false;
249
    }
250
251
    $userInfo = $userInitialData['userInfo'] + $dataReceived;
252
    $return = '';
253
254
    // Check if LDAP is enabled and user is in AD
255
    $userLdap = identifyDoLDAPChecks(
256
        $SETTINGS,
257
        $userInfo,
258
        (string) $username,
259
        (string) $passwordClear,
260
        (int) $sessionAdmin,
261
        (string) $sessionUrl,
262
        (int) $sessionPwdAttempts
263
    );
264
    if ($userLdap['error'] === true) {
265
        // Add failed authentication log
266
        addFailedAuthentication($username, getClientIpServer());
267
268
        // deepcode ignore ServerLeak: File and path are secured directly inside the function decryptFile()
269
        echo prepareExchangedData(
270
            $userLdap['array'],
271
            'encode'
272
        );
273
        return false;
274
    }
275
    if (isset($userLdap['user_info']) === true && (int) $userLdap['user_info']['has_been_created'] === 1) {
276
        // Add failed authentication log
277
        addFailedAuthentication($username, getClientIpServer());
278
279
        echo json_encode([
280
            'data' => prepareExchangedData(
281
                [
282
                    'error' => true,
283
                    'message' => '',
284
                    'extra' => 'ad_user_created',
285
                ],
286
                'encode'
287
            ),
288
            'key' => $session->get('key')
289
        ]);
290
        return false;
291
    }
292
293
    // Is oauth2 user exists?
294
    $userOauth2 = checkOauth2User(
295
        (array) $SETTINGS,
296
        (array) $userInfo,
297
        (string) $username,
298
        (string) $passwordClear,
299
        (int) $userLdap['user_info']['has_been_created']
300
    );
301
    if ($userOauth2['error'] === true) {
302
        $session->set('userOauth2Info', '');
303
304
        // Add failed authentication log
305
        if ($userOauth2['no_log_event'] !== true) {
306
            addFailedAuthentication($username, getClientIpServer());
307
        }
308
309
        // deepcode ignore ServerLeak: File and path are secured directly inside the function decryptFile()        
310
        echo prepareExchangedData(
311
            [
312
                'error' => true,
313
                'message' => $lang->get($userOauth2['message']),
314
                'extra' => 'oauth2_user_not_found',
315
            ],
316
            'encode'
317
        );
318
        return false;
319
    }
320
321
    // Check user and password
322
    $authResult = checkCredentials($passwordClear, $userInfo);
323
    if ($userLdap['userPasswordVerified'] === false && $userOauth2['userPasswordVerified'] === false && $authResult['authenticated'] !== true) {
324
        // Add failed authentication log
325
        addFailedAuthentication($username, getClientIpServer());
326
        echo prepareExchangedData(
327
            [
328
                'value' => '',
329
                'error' => true,
330
                'message' => $lang->get('error_bad_credentials'),
331
            ],
332
            'encode'
333
        );
334
        return false;
335
    }
336
337
    // If user was migrated, then return error to force user to wait
338
    if ($authResult['migrated'] === true && (int) $userInfo['admin'] !== 1) {
339
        echo prepareExchangedData(
340
            [
341
                'value' => '',
342
                'error' => true,
343
                'message' => $lang->get('user_encryption_ongoing'),
344
            ],
345
            'encode'
346
        );
347
        return false;
348
    }
349
350
    // Check if MFA is required
351
    if ((isOneVarOfArrayEqualToValue(
352
                [
353
                    (int) $SETTINGS['yubico_authentication'],
354
                    (int) $SETTINGS['google_authentication'],
355
                    (int) $SETTINGS['duo']
356
                ],
357
                1
358
            ) === true)
359
        && (((int) $userInfo['admin'] !== 1 && (int) $userInfo['mfa_enabled'] === 1 && $userInfo['mfa_auth_requested_roles'] === true)
360
        || ((int) $SETTINGS['admin_2fa_required'] === 1 && (int) $userInfo['admin'] === 1))
361
    ) {
362
        // Check user against MFA method if selected
363
        $userMfa = identifyDoMFAChecks(
364
            $SETTINGS,
365
            $userInfo,
366
            $dataReceived,
367
            $userInitialData,
368
            (string) $username
369
        );
370
        if ($userMfa['error'] === true) {
371
            // Add failed authentication log
372
            addFailedAuthentication($username, getClientIpServer());
373
374
            echo prepareExchangedData(
375
                [
376
                    'error' => true,
377
                    'message' => $userMfa['mfaData']['message'],
378
                    'mfaStatus' => $userMfa['mfaData']['mfaStatus'],
379
                ],
380
                'encode'
381
            );
382
            return false;
383
        } elseif ($userMfa['mfaQRCodeInfos'] === true) {
384
            // Add failed authentication log
385
            addFailedAuthentication($username, getClientIpServer());
386
387
            // Case where user has initiated Google Auth
388
            // Return QR code
389
            echo prepareExchangedData(
390
                [
391
                    'value' => $userMfa['mfaData']['value'],
392
                    'user_admin' => isset($sessionAdmin) ? (int) $sessionAdmin : 0,
393
                    'initial_url' => isset($sessionUrl) === true ? $sessionUrl : '',
394
                    'pwd_attempts' => (int) $sessionPwdAttempts,
395
                    'error' => false,
396
                    'message' => $userMfa['mfaData']['message'],
397
                    'mfaStatus' => $userMfa['mfaData']['mfaStatus'],
398
                ],
399
                'encode'
400
            );
401
            return false;
402
        } elseif ($userMfa['duo_url_ready'] === true) {
403
            // Add failed authentication log
404
            addFailedAuthentication($username, getClientIpServer());
405
406
            // Case where user has initiated Duo Auth
407
            // Return the DUO redirect URL
408
            echo prepareExchangedData(
409
                [
410
                    'user_admin' => isset($sessionAdmin) ? (int) $sessionAdmin : 0,
411
                    'initial_url' => isset($sessionUrl) === true ? $sessionUrl : '',
412
                    'pwd_attempts' => (int) $sessionPwdAttempts,
413
                    'error' => false,
414
                    'message' => $userMfa['mfaData']['message'],
415
                    'duo_url_ready' => $userMfa['mfaData']['duo_url_ready'],
416
                    'duo_redirect_url' => $userMfa['mfaData']['duo_redirect_url'],
417
                    'mfaStatus' => $userMfa['mfaData']['mfaStatus'],
418
                ],
419
                'encode'
420
            );
421
            return false;
422
        }
423
    }
424
425
    // Can connect if
426
    // 1- no LDAP mode + user enabled + pw ok
427
    // 2- LDAP mode + user enabled + ldap connection ok + user is not admin
428
    // 3- LDAP mode + user enabled + pw ok + usre is admin
429
    // This in order to allow admin by default to connect even if LDAP is activated
430
    if (canUserGetLog(
431
            $SETTINGS,
432
            (int) $userInfo['disabled'],
433
            $username,
434
            $userLdap['ldapConnection']
435
        ) === true
436
    ) {
437
        $session->set('pwd_attempts', 0);
438
439
        // Check if any unsuccessfull login tries exist
440
        handleLoginAttempts(
441
            $userInfo['id'],
442
            $userInfo['login'],
443
            $userInfo['last_connexion'],
444
            $username,
445
            $SETTINGS,
446
        );
447
448
        // Avoid unlimited session.
449
        $max_time = isset($SETTINGS['maximum_session_expiration_time']) ? (int) $SETTINGS['maximum_session_expiration_time'] : 60;
450
        $session_time = min($dataReceived['duree_session'], $max_time);
451
        $lifetime = time() + ($session_time * 60);
452
453
        // Save old key
454
        $old_key = $session->get('key');
455
456
        // Good practice: reset PHPSESSID and key after successful authentication
457
        $session->migrate();
458
        $session->set('key', generateQuickPassword(30, false));
459
460
        // Save account in SESSION
461
        $session->set('user-login', stripslashes($username));
462
        $session->set('user-name', empty($userInfo['name']) === false ? stripslashes($userInfo['name']) : '');
463
        $session->set('user-lastname', empty($userInfo['lastname']) === false ? stripslashes($userInfo['lastname']) : '');
464
        $session->set('user-id', (int) $userInfo['id']);
465
        $session->set('user-admin', (int) $userInfo['admin']);
466
        $session->set('user-manager', (int) $userInfo['gestionnaire']);
467
        $session->set('user-can_manage_all_users', $userInfo['can_manage_all_users']);
468
        $session->set('user-read_only', $userInfo['read_only']);
469
        $session->set('user-last_pw_change', $userInfo['last_pw_change']);
470
        $session->set('user-last_pw', $userInfo['last_pw']);
471
        $session->set('user-force_relog', $userInfo['force-relog']);
472
        $session->set('user-can_create_root_folder', $userInfo['can_create_root_folder']);
473
        $session->set('user-email', $userInfo['email']);
474
        //$session->set('user-ga', $userInfo['ga']);
475
        $session->set('user-avatar', $userInfo['avatar']);
476
        $session->set('user-avatar_thumb', $userInfo['avatar_thumb']);
477
        $session->set('user-upgrade_needed', $userInfo['upgrade_needed']);
478
        $session->set('user-is_ready_for_usage', $userInfo['is_ready_for_usage']);
479
        $session->set('user-personal_folder_enabled', $userInfo['personal_folder']);
480
        $session->set(
481
            'user-tree_load_strategy',
482
            (isset($userInfo['treeloadstrategy']) === false || empty($userInfo['treeloadstrategy']) === true) ? 'full' : $userInfo['treeloadstrategy']
483
        );
484
        $session->set(
485
            'user-split_view_mode',
486
            (isset($userInfo['split_view_mode']) === false || empty($userInfo['split_view_mode']) === true) ? 0 : (int) $userInfo['split_view_mode']
487
        );
488
        $session->set(
489
            'user-show_subfolders',
490
            (isset($userInfo['show_subfolders']) === false || empty($userInfo['show_subfolders']) === true) ? 0 : (int) $userInfo['show_subfolders']
491
        );
492
        $session->set('user-language', $userInfo['user_language']);
493
        $session->set('user-timezone', $userInfo['usertimezone']);
494
        $session->set('user-keys_recovery_time', $userInfo['keys_recovery_time']);
495
        
496
        // manage session expiration
497
        $session->set('user-session_duration', (int) $lifetime);
498
499
        // User signature keys
500
        $returnKeys = prepareUserEncryptionKeys($userInfo, $passwordClear, $SETTINGS);
501
        $session->set('user-public_key', $returnKeys['public_key']);
502
        
503
        // Did user has his AD password changed
504
        if (null !== $session->get('user-private_key_recovered') && $session->get('user-private_key_recovered') === true) {
505
            $session->set('user-private_key', $session->get('user-private_key'));
506
        } else {
507
            $session->set('user-private_key', $returnKeys['private_key_clear']);
508
        }
509
510
        // Update keys in DB if needed (for transparent recovery migration)
511
        if (!empty($returnKeys['update_keys_in_db'])) {
512
            DB::update(
513
                prefixTable('users'),
514
                $returnKeys['update_keys_in_db'],
515
                'id = %i',
516
                $userInfo['id']
517
            );
518
        }
519
520
        // API key
521
        $session->set(
522
            'user-api_key',
523
            empty($userInfo['api_key']) === false ? base64_decode(decryptUserObjectKey($userInfo['api_key'], $returnKeys['private_key_clear'])) : '',
524
        );
525
        
526
        $session->set('user-special', $userInfo['special']);
527
        $session->set('user-auth_type', $userInfo['auth_type']);
528
529
        // check feedback regarding user password validity
530
        $return = checkUserPasswordValidity(
531
            $userInfo,
532
            (int) $session->get('user-num_days_before_exp'),
533
            (int) $session->get('user-last_pw_change'),
534
            $SETTINGS
535
        );
536
        $session->set('user-validite_pw', $return['validite_pw']);
537
        $session->set('user-last_pw_change', $return['last_pw_change']);
538
        $session->set('user-num_days_before_exp', $return['numDaysBeforePwExpiration']);
539
        $session->set('user-force_relog', $return['user_force_relog']);
540
        
541
        $session->set('user-last_connection', empty($userInfo['last_connexion']) === false ? (int) $userInfo['last_connexion'] : (int) time());
542
        $session->set('user-latest_items', empty($userInfo['latest_items']) === false ? explode(';', $userInfo['latest_items']) : []);
543
        $session->set('user-favorites', empty($userInfo['favourites']) === false ? explode(';', $userInfo['favourites']) : []);
544
        $session->set('user-accessible_folders', empty($userInfo['groupes_visibles']) === false ? explode(';', $userInfo['groupes_visibles']) : []);
545
        $session->set('user-no_access_folders', empty($userInfo['groupes_interdits']) === false ? explode(';', $userInfo['groupes_interdits']) : []);
546
        
547
        // User's roles
548
        if (strpos($userInfo['fonction_id'] !== NULL ? (string) $userInfo['fonction_id'] : '', ',') !== -1) {
549
            // Convert , to ;
550
            $userInfo['fonction_id'] = str_replace(',', ';', (string) $userInfo['fonction_id']);
551
            DB::update(
552
                prefixTable('users'),
553
                [
554
                    'fonction_id' => $userInfo['fonction_id'],
555
                ],
556
                'id = %i',
557
                $session->get('user-id')
558
            );
559
        }
560
        // Append with roles from AD groups
561
        if (is_null($userInfo['roles_from_ad_groups']) === false) {
562
            $userInfo['fonction_id'] = empty($userInfo['fonction_id'])  === true ? $userInfo['roles_from_ad_groups'] : $userInfo['fonction_id']. ';' . $userInfo['roles_from_ad_groups'];
563
        }
564
        // store
565
        $session->set('user-roles', $userInfo['fonction_id']);
566
        $session->set('user-roles_array', array_unique(array_filter(explode(';', $userInfo['fonction_id']))));
567
        
568
        // build array of roles
569
        $session->set('user-pw_complexity', 0);
570
        $session->set('system-array_roles', []);
571
        if (count($session->get('user-roles_array')) > 0) {
572
            $rolesList = DB::query(
573
                'SELECT id, title, complexity
574
                FROM ' . prefixTable('roles_title') . '
575
                WHERE id IN %li',
576
                $session->get('user-roles_array')
577
            );
578
            $excludeUser = isset($SETTINGS['exclude_user']) ? str_contains($session->get('user-login'), $SETTINGS['exclude_user']) : false;
579
            $adjustPermissions = ($session->get('user-id') >= 1000000 && !$excludeUser && (isset($SETTINGS['admin_needle']) || isset($SETTINGS['manager_needle']) || isset($SETTINGS['tp_manager_needle']) || isset($SETTINGS['read_only_needle'])));
580
            if ($adjustPermissions) {
581
                $userInfo['admin'] = $userInfo['gestionnaire'] = $userInfo['can_manage_all_users'] = $userInfo['read_only'] = 0;
582
            }
583
            foreach ($rolesList as $role) {
584
                SessionManager::addRemoveFromSessionAssociativeArray(
585
                    'system-array_roles',
586
                    [
587
                        'id' => $role['id'],
588
                        'title' => $role['title'],
589
                    ],
590
                    'add'
591
                );
592
                
593
                if ($adjustPermissions) {
594
                    if (isset($SETTINGS['admin_needle']) && str_contains($role['title'], $SETTINGS['admin_needle'])) {
595
                        $userInfo['gestionnaire'] = $userInfo['can_manage_all_users'] = $userInfo['read_only'] = 0;
596
                        $userInfo['admin'] = 1;
597
                    }    
598
                    if (isset($SETTINGS['manager_needle']) && str_contains($role['title'], $SETTINGS['manager_needle'])) {
599
                        $userInfo['admin'] = $userInfo['can_manage_all_users'] = $userInfo['read_only'] = 0;
600
                        $userInfo['gestionnaire'] = 1;
601
                    }
602
                    if (isset($SETTINGS['tp_manager_needle']) && str_contains($role['title'], $SETTINGS['tp_manager_needle'])) {
603
                        $userInfo['admin'] = $userInfo['gestionnaire'] = $userInfo['read_only'] = 0;
604
                        $userInfo['can_manage_all_users'] = 1;
605
                    }
606
                    if (isset($SETTINGS['read_only_needle']) && str_contains($role['title'], $SETTINGS['read_only_needle'])) {
607
                        $userInfo['admin'] = $userInfo['gestionnaire'] = $userInfo['can_manage_all_users'] = 0;
608
                        $userInfo['read_only'] = 1;
609
                    }
610
                }
611
612
                // get highest complexity
613
                if ($session->get('user-pw_complexity') < (int) $role['complexity']) {
614
                    $session->set('user-pw_complexity', (int) $role['complexity']);
615
                }
616
            }
617
            if ($adjustPermissions) {
618
                $session->set('user-admin', (int) $userInfo['admin']);
619
                $session->set('user-manager', (int) $userInfo['gestionnaire']);
620
                $session->set('user-can_manage_all_users',(int)  $userInfo['can_manage_all_users']);
621
                $session->set('user-read_only', (int) $userInfo['read_only']);
622
                DB::update(
623
                    prefixTable('users'),
624
                    [
625
                        'admin' => $userInfo['admin'],
626
                        'gestionnaire' => $userInfo['gestionnaire'],
627
                        'can_manage_all_users' => $userInfo['can_manage_all_users'],
628
                        'read_only' => $userInfo['read_only'],
629
                    ],
630
                    'id = %i',
631
                    $session->get('user-id')
632
                );
633
            }
634
        }
635
636
        // Set some settings
637
        $SETTINGS['update_needed'] = '';
638
639
        // Update table
640
        DB::update(
641
            prefixTable('users'),
642
            array_merge(
643
                [
644
                    'key_tempo' => $session->get('key'),
645
                    'last_connexion' => time(),
646
                    'timestamp' => time(),
647
                    'disabled' => 0,
648
                    'session_end' => $session->get('user-session_duration'),
649
                    'user_ip' => $dataReceived['client'],
650
                ],
651
                $returnKeys['update_keys_in_db']
652
            ),
653
            'id=%i',
654
            $userInfo['id']
655
        );
656
        
657
        // Get user's rights
658
        if ($userLdap['user_initial_creation_through_external_ad'] === true || $userOauth2['retExternalAD']['has_been_created'] === 1) {
659
            // is new LDAP user. Show only his personal folder
660
            if ($SETTINGS['enable_pf_feature'] === '1') {
661
                $session->set('user-personal_visible_folders', [$userInfo['id']]);
662
                $session->set('user-personal_folders', [$userInfo['id']]);
663
            } else {
664
                $session->set('user-personal_visible_folders', []);
665
                $session->set('user-personal_folders', []);
666
            }
667
            $session->set('user-roles_array', []);
668
            $session->set('user-read_only_folders', []);
669
            $session->set('user-list_folders_limited', []);
670
            $session->set('system-list_folders_editable_by_role', []);
671
            $session->set('system-list_restricted_folders_for_items', []);
672
            $session->set('user-nb_folders', 1);
673
            $session->set('user-nb_roles', 1);
674
        } else {
675
            identifyUserRights(
676
                $userInfo['groupes_visibles'],
677
                $session->get('user-no_access_folders'),
678
                $userInfo['admin'],
679
                $userInfo['fonction_id'],
680
                $SETTINGS
681
            );
682
        }
683
        // Get some more elements
684
        $session->set('system-screen_height', $dataReceived['screenHeight']);
685
686
        // Get last seen items
687
        $session->set('user-nb_roles', 0);
688
        foreach ($session->get('user-latest_items') as $item) {
689
            if (! empty($item)) {
690
                $dataLastItems = DB::queryFirstRow(
0 ignored issues
show
The assignment to $dataLastItems is dead and can be removed.
Loading history...
691
                    'SELECT id,label,id_tree
692
                    FROM ' . prefixTable('items') . '
693
                    WHERE id=%i',
694
                    $item
695
                );
696
            }
697
        }
698
699
        // Get cahce tree info
700
        $cacheTreeData = DB::queryFirstRow(
701
            'SELECT visible_folders
702
            FROM ' . prefixTable('cache_tree') . '
703
            WHERE user_id=%i',
704
            (int) $session->get('user-id')
705
        );
706
        if (DB::count() > 0 && empty($cacheTreeData['visible_folders']) === true) {
707
            $session->set('user-cache_tree', '');
708
            // Prepare new task
709
            DB::insert(
710
                prefixTable('background_tasks'),
711
                array(
712
                    'created_at' => time(),
713
                    'process_type' => 'user_build_cache_tree',
714
                    'arguments' => json_encode([
715
                        'user_id' => (int) $session->get('user-id'),
716
                    ], JSON_HEX_QUOT | JSON_HEX_TAG),
717
                    'updated_at' => null,
718
                    'finished_at' => null,
719
                    'output' => null,
720
                )
721
            );
722
        } else {
723
            $session->set('user-cache_tree', $cacheTreeData['visible_folders']);
724
        }
725
726
        // send back the random key
727
        $return = $dataReceived['randomstring'];
728
        // Send email
729
        if (
730
            isKeyExistingAndEqual('enable_send_email_on_user_login', 1, $SETTINGS) === true
731
            && (int) $sessionAdmin !== 1
732
        ) {
733
            // get all Admin users
734
            $val = DB::queryFirstRow('SELECT email FROM ' . prefixTable('users') . " WHERE admin = %i and email != ''", 1);
735
            if (DB::count() > 0) {
736
                // Add email to table
737
                prepareSendingEmail(
738
                    $lang->get('email_subject_on_user_login'),
739
                    str_replace(
740
                        [
741
                            '#tp_user#',
742
                            '#tp_date#',
743
                            '#tp_time#',
744
                        ],
745
                        [
746
                            ' ' . $session->get('user-login') . ' (IP: ' . getClientIpServer() . ')',
747
                            date($SETTINGS['date_format'], (int) $session->get('user-last_connection')),
748
                            date($SETTINGS['time_format'], (int) $session->get('user-last_connection')),
749
                        ],
750
                        $lang->get('email_body_on_user_login')
751
                    ),
752
                    $val['email'],
753
                    $lang->get('administrator')
754
                );
755
            }
756
        }
757
        
758
        // Ensure Complexity levels are translated
759
        defineComplexity();
760
        echo prepareExchangedData(
761
            [
762
                'value' => $return,
763
                'user_id' => $session->get('user-id') !== null ? $session->get('user-id') : '',
764
                'user_admin' => null !== $session->get('user-admin') ? $session->get('user-admin') : 0,
765
                'initial_url' => $antiXss->xss_clean($sessionUrl),
766
                'pwd_attempts' => 0,
767
                'error' => false,
768
                'message' => $session->has('user-upgrade_needed') && (int) $session->get('user-upgrade_needed') && (int) $session->get('user-upgrade_needed') === 1 ? 'ask_for_otc' : '',
769
                'first_connection' => $session->get('user-validite_pw') === 0 ? true : false,
770
                'password_complexity' => TP_PW_COMPLEXITY[$session->get('user-pw_complexity')][1],
771
                'password_change_expected' => $userInfo['special'] === 'password_change_expected' ? true : false,
772
                'private_key_conform' => $session->get('user-id') !== null
773
                    && empty($session->get('user-private_key')) === false
774
                    && $session->get('user-private_key') !== 'none' ? true : false,
775
                'session_key' => $session->get('key'),
776
                'can_create_root_folder' => null !== $session->get('user-can_create_root_folder') ? (int) $session->get('user-can_create_root_folder') : '',
777
                'upgrade_needed' => isset($userInfo['upgrade_needed']) === true ? (int) $userInfo['upgrade_needed'] : 0,
778
                'special' => isset($userInfo['special']) === true ? (int) $userInfo['special'] : 0,
779
                'split_view_mode' => isset($userInfo['split_view_mode']) === true ? (int) $userInfo['split_view_mode'] : 0,
780
                'show_subfolders' => isset($userInfo['show_subfolders']) === true ? (int) $userInfo['show_subfolders'] : 0,
781
                'validite_pw' => $session->get('user-validite_pw') !== null ? $session->get('user-validite_pw') : '',
782
                'num_days_before_exp' => $session->get('user-num_days_before_exp') !== null ? (int) $session->get('user-num_days_before_exp') : '',
783
            ],
784
            'encode',
785
            $old_key
786
        );
787
    
788
        return true;
789
790
    } elseif ((int) $userInfo['disabled'] === 1) {
791
        // User and password is okay but account is locked
792
        echo prepareExchangedData(
793
            [
794
                'value' => $return,
795
                'user_id' => $session->get('user-id') !== null ? (int) $session->get('user-id') : '',
796
                'user_admin' => null !== $session->get('user-admin') ? $session->get('user-admin') : 0,
797
                'initial_url' => isset($sessionUrl) === true ? $sessionUrl : '',
798
                'pwd_attempts' => 0,
799
                'error' => 'user_is_locked',
800
                'message' => $lang->get('account_is_locked'),
801
                'first_connection' => $session->get('user-validite_pw') === 0 ? true : false,
802
                'password_complexity' => TP_PW_COMPLEXITY[$session->get('user-pw_complexity')][1],
803
                'password_change_expected' => $userInfo['special'] === 'password_change_expected' ? true : false,
804
                'private_key_conform' => $session->has('user-private_key') && null !== $session->get('user-private_key')
805
                    && empty($session->get('user-private_key')) === false
806
                    && $session->get('user-private_key') !== 'none' ? true : false,
807
                'session_key' => $session->get('key'),
808
                'can_create_root_folder' => null !== $session->get('user-can_create_root_folder') ? (int) $session->get('user-can_create_root_folder') : '',
809
            ],
810
            'encode'
811
        );
812
        return false;
813
    }
814
815
    echo prepareExchangedData(
816
        [
817
            'value' => $return,
818
            'user_id' => $session->get('user-id') !== null ? (int) $session->get('user-id') : '',
819
            'user_admin' => null !== $session->get('user-admin') ? $session->get('user-admin') : 0,
820
            'initial_url' => isset($sessionUrl) === true ? $sessionUrl : '',
821
            'pwd_attempts' => (int) $sessionPwdAttempts,
822
            'error' => true,
823
            'message' => $lang->get('error_not_allowed_to_authenticate'),
824
            'first_connection' => $session->get('user-validite_pw') === 0 ? true : false,
825
            'password_complexity' => TP_PW_COMPLEXITY[$session->get('user-pw_complexity')][1],
826
            'password_change_expected' => $userInfo['special'] === 'password_change_expected' ? true : false,
827
            'private_key_conform' => $session->get('user-id') !== null
828
                    && empty($session->get('user-private_key')) === false
829
                    && $session->get('user-private_key') !== 'none' ? true : false,
830
            'session_key' => $session->get('key'),
831
            'can_create_root_folder' => null !== $session->get('user-can_create_root_folder') ? (int) $session->get('user-can_create_root_folder') : '',
832
        ],
833
        'encode'
834
    );
835
    return false;
836
}
837
838
/**
839
 * Check if any unsuccessfull login tries exist
840
 *
841
 * @param int       $userInfoId
842
 * @param string    $userInfoLogin
843
 * @param string    $userInfoLastConnection
844
 * @param string    $username
845
 * @param array     $SETTINGS
846
 * @return array
847
 */
848
function handleLoginAttempts(
849
    $userInfoId,
850
    $userInfoLogin,
851
    $userInfoLastConnection,
852
    $username,
853
    $SETTINGS
854
) : array
855
{
856
    $rows = DB::query(
857
        'SELECT date
858
        FROM ' . prefixTable('log_system') . "
859
        WHERE field_1 = %s
860
        AND type = 'failed_auth'
861
        AND label = 'password_is_not_correct'
862
        AND date >= %s AND date < %s",
863
        $userInfoLogin,
864
        $userInfoLastConnection,
865
        time()
866
    );
867
    $arrAttempts = [];
868
    if (DB::count() > 0) {
869
        foreach ($rows as $record) {
870
            array_push(
871
                $arrAttempts,
872
                date($SETTINGS['date_format'] . ' ' . $SETTINGS['time_format'], (int) $record['date'])
873
            );
874
        }
875
    }
876
    
877
878
    // Log into DB the user's connection
879
    if (isKeyExistingAndEqual('log_connections', 1, $SETTINGS) === true) {
880
        logEvents($SETTINGS, 'user_connection', 'connection', (string) $userInfoId, stripslashes($username));
881
    }
882
883
    return [
884
        'attemptsList' => $arrAttempts,
885
        'attemptsCount' => count($rows),
886
    ];
887
}
888
889
890
/**
891
 * Can you user get logged into main page
892
 *
893
 * @param array     $SETTINGS
894
 * @param int       $userInfoDisabled
895
 * @param string    $username
896
 * @param bool      $ldapConnection
897
 *
898
 * @return boolean
899
 */
900
function canUserGetLog(
901
    $SETTINGS,
902
    $userInfoDisabled,
903
    $username,
904
    $ldapConnection
905
) : bool
906
{
907
    include_once $SETTINGS['cpassman_dir'] . '/sources/main.functions.php';
908
909
    if ((int) $userInfoDisabled === 1) {
910
        return false;
911
    }
912
913
    if (isKeyExistingAndEqual('ldap_mode', 0, $SETTINGS) === true) {
914
        return true;
915
    }
916
    
917
    if (isKeyExistingAndEqual('ldap_mode', 1, $SETTINGS) === true 
918
        && (
919
            ($ldapConnection === true && $username !== 'admin')
920
            || $username === 'admin'
921
        )
922
    ) {
923
        return true;
924
    }
925
926
    if (isKeyExistingAndEqual('ldap_and_local_authentication', 1, $SETTINGS) === true
927
        && isset($SETTINGS['ldap_mode']) === true && in_array($SETTINGS['ldap_mode'], ['1', '2']) === true
928
    ) {
929
        return true;
930
    }
931
932
    return false;
933
}
934
935
/**
936
 * 
937
 * Prepare user keys
938
 * 
939
 * @param array $userInfo   User account information
940
 * @param string $passwordClear
941
 *
942
 * @return array
943
 */
944
function prepareUserEncryptionKeys($userInfo, $passwordClear, array $SETTINGS = []) : array
945
{
946
    if (is_null($userInfo['private_key']) === true || empty($userInfo['private_key']) === true || $userInfo['private_key'] === 'none') {
947
        // No keys have been generated yet
948
        // Create them
949
        $userKeys = generateUserKeys($passwordClear, $SETTINGS);
950
951
        $updateData = [
952
            'public_key' => $userKeys['public_key'],
953
            'private_key' => $userKeys['private_key'],
954
        ];
955
956
        // Add transparent recovery data if available
957
        if (isset($userKeys['user_seed'])) {
958
            $updateData['user_derivation_seed'] = $userKeys['user_seed'];
959
            $updateData['private_key_backup'] = $userKeys['private_key_backup'];
960
            $updateData['key_integrity_hash'] = $userKeys['key_integrity_hash'];
961
            $updateData['last_pw_change'] = time();
962
        }
963
964
        return [
965
            'public_key' => $userKeys['public_key'],
966
            'private_key_clear' => $userKeys['private_key_clear'],
967
            'update_keys_in_db' => $updateData,
968
        ];
969
    }
970
971
    if ($userInfo['special'] === 'generate-keys') {
972
        return [
973
            'public_key' => $userInfo['public_key'],
974
            'private_key_clear' => '',
975
            'update_keys_in_db' => [],
976
        ];
977
    }
978
979
    // Don't perform this in case of special login action
980
    if ($userInfo['special'] === 'otc_is_required_on_next_login' || $userInfo['special'] === 'user_added_from_ad') {
981
        return [
982
            'public_key' => $userInfo['public_key'],
983
            'private_key_clear' => '',
984
            'update_keys_in_db' => [],
985
        ];
986
    }
987
988
    // Try to uncrypt private key with current password
989
    try {
990
        $privateKeyClear = decryptPrivateKey($passwordClear, $userInfo['private_key']);
991
        
992
        // If user has seed but no backup, create it on first successful login
993
        if (!empty($userInfo['user_derivation_seed']) && empty($userInfo['private_key_backup'])) {
994
            if (defined('LOG_TO_SERVER') && LOG_TO_SERVER === true) {
995
                error_log('TEAMPASS Transparent Recovery - Creating backup for user ' . ($userInfo['login'] ?? 'unknown'));
996
            }
997
998
            $derivedKey = deriveBackupKey($userInfo['user_derivation_seed'], $userInfo['public_key'], $SETTINGS);
999
            $cipher = new Crypt_AES();
1000
            $cipher->setPassword($derivedKey);
1001
            $privateKeyBackup = base64_encode($cipher->encrypt(base64_decode($privateKeyClear)));
1002
1003
            // Generate integrity hash
1004
            $integrityHash = null;
0 ignored issues
show
The assignment to $integrityHash is dead and can be removed.
Loading history...
1005
            $serverSecret = getServerSecret();
1006
            $integrityHash = generateKeyIntegrityHash($userInfo['user_derivation_seed'], $userInfo['public_key'], $serverSecret);
1007
1008
            return [
1009
                'public_key' => $userInfo['public_key'],
1010
                'private_key_clear' => $privateKeyClear,
1011
                'update_keys_in_db' => [
1012
                    'private_key_backup' => $privateKeyBackup,
1013
                    'key_integrity_hash' => $integrityHash,
1014
                    'last_pw_change' => time(),
1015
                ],
1016
            ];
1017
        }
1018
1019
        return [
1020
            'public_key' => $userInfo['public_key'],
1021
            'private_key_clear' => $privateKeyClear,
1022
            'update_keys_in_db' => [],
1023
        ];
1024
1025
    } catch (Exception $e) {
1026
        // Decryption failed - try transparent recovery
1027
        if (!empty($userInfo['user_derivation_seed'])
1028
            && !empty($userInfo['private_key_backup'])) {
1029
1030
            $recovery = attemptTransparentRecovery($userInfo, $passwordClear, $SETTINGS);
1031
1032
            if ($recovery['success']) {
1033
                return [
1034
                    'public_key' => $userInfo['public_key'],
1035
                    'private_key_clear' => $recovery['private_key_clear'],
1036
                    'update_keys_in_db' => [],
1037
                ];
1038
            }
1039
        }
1040
1041
        // Recovery failed or not available - return empty
1042
        return [
1043
            'public_key' => $userInfo['public_key'],
1044
            'private_key_clear' => '',
1045
            'update_keys_in_db' => [],
1046
        ];
1047
    }
1048
}
1049
1050
1051
/**
1052
 * CHECK PASSWORD VALIDITY
1053
 * Don't take into consideration if LDAP in use
1054
 * 
1055
 * @param array $userInfo User account information
1056
 * @param int $numDaysBeforePwExpiration Number of days before password expiration
1057
 * @param int $lastPwChange Last password change
1058
 * @param array $SETTINGS Teampass settings
1059
 *
1060
 * @return array
1061
 */
1062
function checkUserPasswordValidity(array $userInfo, int $numDaysBeforePwExpiration, int $lastPwChange, array $SETTINGS)
1063
{
1064
    if (isKeyExistingAndEqual('ldap_mode', 1, $SETTINGS) === true && $userInfo['auth_type'] !== 'local') {
1065
        return [
1066
            'validite_pw' => true,
1067
            'last_pw_change' => $userInfo['last_pw_change'],
1068
            'user_force_relog' => '',
1069
            'numDaysBeforePwExpiration' => '',
1070
        ];
1071
    }
1072
    
1073
    if (isset($userInfo['last_pw_change']) === true) {
1074
        if ((int) $SETTINGS['pw_life_duration'] === 0) {
1075
            return [
1076
                'validite_pw' => true,
1077
                'last_pw_change' => '',
1078
                'user_force_relog' => 'infinite',
1079
                'numDaysBeforePwExpiration' => '',
1080
            ];
1081
        } elseif ((int) $SETTINGS['pw_life_duration'] > 0) {
1082
            $numDaysBeforePwExpiration = (int) $SETTINGS['pw_life_duration'] - round(
1083
                (mktime(0, 0, 0, (int) date('m'), (int) date('d'), (int) date('y')) - $userInfo['last_pw_change']) / (24 * 60 * 60)
1084
            );
1085
            return [
1086
                'validite_pw' => $numDaysBeforePwExpiration <= 0 ? false : true,
1087
                'last_pw_change' => $userInfo['last_pw_change'],
1088
                'user_force_relog' => 'infinite',
1089
                'numDaysBeforePwExpiration' => (int) $numDaysBeforePwExpiration,
1090
            ];
1091
        } else {
1092
            return [
1093
                'validite_pw' => false,
1094
                'last_pw_change' => '',
1095
                'user_force_relog' => '',
1096
                'numDaysBeforePwExpiration' => '',
1097
            ];
1098
        }
1099
    } else {
1100
        return [
1101
            'validite_pw' => false,
1102
            'last_pw_change' => '',
1103
            'user_force_relog' => '',
1104
            'numDaysBeforePwExpiration' => '',
1105
        ];
1106
    }
1107
}
1108
1109
1110
/**
1111
 * Authenticate a user through AD/LDAP.
1112
 *
1113
 * @param string $username      Username
1114
 * @param array $userInfo       User account information
1115
 * @param string $passwordClear Password
1116
 * @param array $SETTINGS       Teampass settings
1117
 *
1118
 * @return array
1119
 */
1120
function authenticateThroughAD(string $username, array $userInfo, string $passwordClear, array $SETTINGS): array
1121
{
1122
    $session = SessionManager::getSession();
1123
    $lang = new Language($session->get('user-language') ?? 'english');
1124
    
1125
    try {
1126
        // Get LDAP connection and handler
1127
        $ldapHandler = initializeLdapConnection($SETTINGS);
1128
        
1129
        // Authenticate user
1130
        $authResult = authenticateUser($username, $passwordClear, $ldapHandler, $SETTINGS, $lang);
1131
        if ($authResult['error']) {
1132
            return $authResult;
1133
        }
1134
        
1135
        $userADInfos = $authResult['user_info'];
1136
        
1137
        // Verify account expiration
1138
        if (isAccountExpired($userADInfos)) {
1139
            return [
1140
                'error' => true,
1141
                'message' => $lang->get('error_ad_user_expired'),
1142
            ];
1143
        }
1144
        
1145
        // Handle user creation if needed
1146
        if ($userInfo['ldap_user_to_be_created']) {
1147
            $userInfo = handleNewUser($username, $passwordClear, $userADInfos, $userInfo, $SETTINGS, $lang);
1148
        }
1149
        
1150
        // Get and handle user groups
1151
        $userGroupsData = getUserGroups($userADInfos, $ldapHandler, $SETTINGS);
1152
        handleUserADGroups($username, $userInfo, $userGroupsData['userGroups'], $SETTINGS);
1153
        
1154
        // Finalize authentication
1155
        finalizeAuthentication($userInfo, $passwordClear, $SETTINGS);
1156
        
1157
        return [
1158
            'error' => false,
1159
            'message' => '',
1160
            'user_info' => $userInfo,
1161
        ];
1162
        
1163
    } catch (Exception $e) {
1164
        return [
1165
            'error' => true,
1166
            'message' => "Error: " . $e->getMessage(),
1167
        ];
1168
    }
1169
}
1170
1171
/**
1172
 * Initialize LDAP connection based on type
1173
 * 
1174
 * @param array $SETTINGS Teampass settings
1175
 * @return array Contains connection and type-specific handler
1176
 * @throws Exception
1177
 */
1178
function initializeLdapConnection(array $SETTINGS): array
1179
{
1180
    $ldapExtra = new LdapExtra($SETTINGS);
1181
    $ldapConnection = $ldapExtra->establishLdapConnection();
1182
    
1183
    switch ($SETTINGS['ldap_type']) {
1184
        case 'ActiveDirectory':
1185
            return [
1186
                'connection' => $ldapConnection,
1187
                'handler' => new ActiveDirectoryExtra(),
1188
                'type' => 'ActiveDirectory'
1189
            ];
1190
        case 'OpenLDAP':
1191
            return [
1192
                'connection' => $ldapConnection,
1193
                'handler' => new OpenLdapExtra(),
1194
                'type' => 'OpenLDAP'
1195
            ];
1196
        default:
1197
            throw new Exception("Unsupported LDAP type: " . $SETTINGS['ldap_type']);
1198
    }
1199
}
1200
1201
/**
1202
 * Authenticate user against LDAP
1203
 * 
1204
 * @param string $username Username
1205
 * @param string $passwordClear Password
1206
 * @param array $ldapHandler LDAP connection and handler
1207
 * @param array $SETTINGS Teampass settings
1208
 * @param Language $lang Language instance
1209
 * @return array Authentication result
1210
 */
1211
function authenticateUser(string $username, string $passwordClear, array $ldapHandler, array $SETTINGS, Language $lang): array
1212
{
1213
    try {
1214
        $userAttribute = $SETTINGS['ldap_user_attribute'] ?? 'samaccountname';
1215
        $userADInfos = $ldapHandler['connection']->query()
1216
            ->where($userAttribute, '=', $username)
1217
            ->firstOrFail();
1218
        
1219
        // Verify user status for ActiveDirectory
1220
        if ($ldapHandler['type'] === 'ActiveDirectory' && !$ldapHandler['handler']->userIsEnabled((string) $userADInfos['dn'], $ldapHandler['connection'])) {
1221
            return [
1222
                'error' => true,
1223
                'message' => "Error: User is not enabled"
1224
            ];
1225
        }
1226
        
1227
        // Attempt authentication
1228
        $authIdentifier = $ldapHandler['type'] === 'ActiveDirectory' 
1229
            ? $userADInfos['userprincipalname'][0] 
1230
            : $userADInfos['dn'];
1231
            
1232
        if (!$ldapHandler['connection']->auth()->attempt($authIdentifier, $passwordClear)) {
1233
            return [
1234
                'error' => true,
1235
                'message' => "Error: User is not authenticated"
1236
            ];
1237
        }
1238
        
1239
        return [
1240
            'error' => false,
1241
            'user_info' => $userADInfos
1242
        ];
1243
        
1244
    } catch (\LdapRecord\Query\ObjectNotFoundException $e) {
1245
        return [
1246
            'error' => true,
1247
            'message' => $lang->get('error_bad_credentials')
1248
        ];
1249
    }
1250
}
1251
1252
/**
1253
 * Check if user account is expired
1254
 * 
1255
 * @param array $userADInfos User AD information
1256
 * @return bool
1257
 */
1258
function isAccountExpired(array $userADInfos): bool
1259
{
1260
    return (isset($userADInfos['shadowexpire'][0]) && (int) $userADInfos['shadowexpire'][0] === 1)
1261
        || (isset($userADInfos['accountexpires'][0]) 
1262
            && (int) $userADInfos['accountexpires'][0] < time() 
1263
            && (int) $userADInfos['accountexpires'][0] !== 0);
1264
}
1265
1266
/**
1267
 * Handle creation of new user
1268
 * 
1269
 * @param string $username Username
1270
 * @param string $passwordClear Password
1271
 * @param array $userADInfos User AD information
1272
 * @param array $userInfo User information
1273
 * @param array $SETTINGS Teampass settings
1274
 * @param Language $lang Language instance
1275
 * @return array User information
1276
 */
1277
function handleNewUser(string $username, string $passwordClear, array $userADInfos, array $userInfo, array $SETTINGS, Language $lang): array
1278
{
1279
    $userInfo = externalAdCreateUser(
1280
        $username,
1281
        $passwordClear,
1282
        $userADInfos['mail'][0],
1283
        $userADInfos['givenname'][0],
1284
        $userADInfos['sn'][0],
1285
        'ldap',
1286
        [],
1287
        $SETTINGS
1288
    );
1289
1290
    handleUserKeys(
1291
        (int) $userInfo['id'],
1292
        $passwordClear,
1293
        (int) ($SETTINGS['maximum_number_of_items_to_treat'] ?? NUMBER_ITEMS_IN_BATCH),
1294
        uniqidReal(20),
1295
        true,
1296
        true,
1297
        true,
1298
        false,
1299
        $lang->get('email_body_user_config_2')
1300
    );
1301
1302
    $userInfo['has_been_created'] = 1;
1303
    return $userInfo;
1304
}
1305
1306
/**
1307
 * Get user groups based on LDAP type
1308
 * 
1309
 * @param array $userADInfos User AD information
1310
 * @param array $ldapHandler LDAP connection and handler
1311
 * @param array $SETTINGS Teampass settings
1312
 * @return array User groups
1313
 */
1314
function getUserGroups(array $userADInfos, array $ldapHandler, array $SETTINGS): array
1315
{
1316
    $dnAttribute = $SETTINGS['ldap_user_dn_attribute'] ?? 'distinguishedname';
1317
    
1318
    if ($ldapHandler['type'] === 'ActiveDirectory') {
1319
        return $ldapHandler['handler']->getUserADGroups(
1320
            $userADInfos[$dnAttribute][0],
1321
            $ldapHandler['connection'],
1322
            $SETTINGS
1323
        );
1324
    }
1325
    
1326
    if ($ldapHandler['type'] === 'OpenLDAP') {
1327
        return $ldapHandler['handler']->getUserADGroups(
1328
            $userADInfos['dn'],
1329
            $ldapHandler['connection'],
1330
            $SETTINGS
1331
        );
1332
    }
1333
    
1334
    throw new Exception("Unsupported LDAP type: " . $ldapHandler['type']);
1335
}
1336
1337
/**
1338
 * Permits to update the user's AD groups with mapping roles
1339
 *
1340
 * @param string $username
1341
 * @param array $userInfo
1342
 * @param array $groups
1343
 * @param array $SETTINGS
1344
 * @return void
1345
 */
1346
function handleUserADGroups(string $username, array $userInfo, array $groups, array $SETTINGS): void
1347
{
1348
    if (isset($SETTINGS['enable_ad_users_with_ad_groups']) === true && (int) $SETTINGS['enable_ad_users_with_ad_groups'] === 1) {
1349
        // Get user groups from AD
1350
        $user_ad_groups = [];
1351
        foreach($groups as $group) {
1352
            //print_r($group);
1353
            // get relation role id for AD group
1354
            $role = DB::queryFirstRow(
1355
                'SELECT lgr.role_id
1356
                FROM ' . prefixTable('ldap_groups_roles') . ' AS lgr
1357
                WHERE lgr.ldap_group_id = %s',
1358
                $group
1359
            );
1360
            if (DB::count() > 0) {
1361
                array_push($user_ad_groups, $role['role_id']); 
1362
            }
1363
        }
1364
        
1365
        // save
1366
        if (count($user_ad_groups) > 0) {
1367
            $user_ad_groups = implode(';', $user_ad_groups);
1368
            DB::update(
1369
                prefixTable('users'),
1370
                [
1371
                    'roles_from_ad_groups' => $user_ad_groups,
1372
                ],
1373
                'id = %i',
1374
                $userInfo['id']
1375
            );
1376
1377
            $userInfo['roles_from_ad_groups'] = $user_ad_groups;
1378
        } else {
1379
            DB::update(
1380
                prefixTable('users'),
1381
                [
1382
                    'roles_from_ad_groups' => null,
1383
                ],
1384
                'id = %i',
1385
                $userInfo['id']
1386
            );
1387
1388
            $userInfo['roles_from_ad_groups'] = [];
1389
        }
1390
    } else {
1391
        // Delete all user's AD groups
1392
        DB::update(
1393
            prefixTable('users'),
1394
            [
1395
                'roles_from_ad_groups' => null,
1396
            ],
1397
            'id = %i',
1398
            $userInfo['id']
1399
        );
1400
    }
1401
}
1402
1403
/**
1404
 * Permits to finalize the authentication process.
1405
 *
1406
 * @param array $userInfo
1407
 * @param string $passwordClear
1408
 * @param array $SETTINGS
1409
 */
1410
function finalizeAuthentication(
1411
    array $userInfo,
1412
    string $passwordClear,
1413
    array $SETTINGS
1414
): void
1415
{
1416
    $passwordManager = new PasswordManager();
1417
    
1418
    // Migrate password if needed
1419
    $result  = $passwordManager->migratePassword(
1420
        $userInfo['pw'],
1421
        $passwordClear,
1422
        (int) $userInfo['id']
1423
    );
1424
1425
    // Use the new hashed password if migration was successful
1426
    $hashedPassword = $result['hashedPassword'];
1427
    
1428
    if (empty($userInfo['pw']) === true || $userInfo['special'] === 'user_added_from_ad') {
1429
        // 2 cases are managed here:
1430
        // Case where user has never been connected then erase current pwd with the ldap's one
1431
        // Case where user has been added from LDAP and never being connected to TP
1432
        DB::update(
1433
            prefixTable('users'),
1434
            [
1435
                'pw' => $passwordManager->hashPassword($passwordClear),
1436
            ],
1437
            'id = %i',
1438
            $userInfo['id']
1439
        );
1440
    } elseif ($passwordManager->verifyPassword($hashedPassword, $passwordClear) === false) {
1441
        // Case where user is auth by LDAP but his password in Teampass is not synchronized
1442
        // For example when user has changed his password in AD.
1443
        // So we need to update it in Teampass and handle private key re-encryption
1444
        DB::update(
1445
            prefixTable('users'),
1446
            [
1447
                'pw' => $passwordManager->hashPassword($passwordClear),
1448
            ],
1449
            'id = %i',
1450
            $userInfo['id']
1451
        );
1452
1453
        // Try transparent recovery for automatic re-encryption
1454
        handleExternalPasswordChange((int) $userInfo['id'], $passwordClear, $userInfo, $SETTINGS);
1455
    }
1456
}
1457
1458
/**
1459
 * Undocumented function.
1460
 *
1461
 * @param string $username      User name
1462
 * @param string $passwordClear User password in clear
1463
 * @param array $retLDAP       Received data from LDAP
1464
 * @param array $SETTINGS      Teampass settings
1465
 *
1466
 * @return array
1467
 */
1468
function externalAdCreateUser(
1469
    string $login,
1470
    string $passwordClear,
1471
    string $userEmail,
1472
    string $userName,
1473
    string $userLastname,
1474
    string $authType,
1475
    array $userGroups,
1476
    array $SETTINGS
1477
): array
1478
{
1479
    // Generate user keys pair with transparent recovery support
1480
    $userKeys = generateUserKeys($passwordClear, $SETTINGS);
1481
1482
    // Create password hash
1483
    $passwordManager = new PasswordManager();
1484
    $hashedPassword = $passwordManager->hashPassword($passwordClear);
1485
    
1486
    // If any groups provided, add user to them
1487
    if (count($userGroups) > 0) {
1488
        $groupIds = [];
1489
        foreach ($userGroups as $group) {
1490
            // Check if exists in DB
1491
            $groupData = DB::queryFirstRow(
1492
                'SELECT id
1493
                FROM ' . prefixTable('roles_title') . '
1494
                WHERE title = %s',
1495
                $group["displayName"]
1496
            );
1497
1498
            if (DB::count() > 0) {
1499
                array_push($groupIds, $groupData['id']);
1500
            }
1501
        }
1502
        $userGroups = implode(';', $groupIds);
1503
    } else {
1504
        $userGroups = '';
1505
    }
1506
    
1507
    if (empty($userGroups) && !empty($SETTINGS['oauth_selfregistered_user_belongs_to_role'])) {
1508
        $userGroups = $SETTINGS['oauth_selfregistered_user_belongs_to_role'];
1509
    }
1510
1511
    // Prepare user data
1512
    $userData = [
1513
        'login' => (string) $login,
1514
        'pw' => (string) $hashedPassword,
1515
        'email' => (string) $userEmail,
1516
        'name' => (string) $userName,
1517
        'lastname' => (string) $userLastname,
1518
        'admin' => '0',
1519
        'gestionnaire' => '0',
1520
        'can_manage_all_users' => '0',
1521
        'personal_folder' => $SETTINGS['enable_pf_feature'] === '1' ? '1' : '0',
1522
        'groupes_interdits' => '',
1523
        'groupes_visibles' => '',
1524
        'fonction_id' => $userGroups,
1525
        'last_pw_change' => (int) time(),
1526
        'user_language' => (string) $SETTINGS['default_language'],
1527
        'encrypted_psk' => '',
1528
        'isAdministratedByRole' => $authType === 'ldap' ?
1529
            (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)
1530
            : (
1531
                $authType === 'oauth2' ?
1532
                (isset($SETTINGS['oauth_new_user_is_administrated_by']) === true && empty($SETTINGS['oauth_new_user_is_administrated_by']) === false ? $SETTINGS['oauth_new_user_is_administrated_by'] : 0)
1533
                : 0
1534
            ),
1535
        'public_key' => $userKeys['public_key'],
1536
        'private_key' => $userKeys['private_key'],
1537
        'special' => 'none',
1538
        'auth_type' => $authType,
1539
        'otp_provided' => '1',
1540
        'is_ready_for_usage' => '0',
1541
        'created_at' => time(),
1542
    ];
1543
1544
    // Add transparent recovery fields if available
1545
    if (isset($userKeys['user_seed'])) {
1546
        $userData['user_derivation_seed'] = $userKeys['user_seed'];
1547
        $userData['private_key_backup'] = $userKeys['private_key_backup'];
1548
        $userData['key_integrity_hash'] = $userKeys['key_integrity_hash'];
1549
        $userData['last_pw_change'] = time();
1550
    }
1551
1552
    // Insert user in DB
1553
    DB::insert(prefixTable('users'), $userData);
1554
    $newUserId = DB::insertId();
1555
1556
    // Create the API key
1557
    DB::insert(
1558
        prefixTable('api'),
1559
        array(
1560
            'type' => 'user',
1561
            'user_id' => $newUserId,
1562
            'value' => encryptUserObjectKey(base64_encode(base64_encode(uniqidReal(39))), $userKeys['public_key']),
1563
            'timestamp' => time(),
1564
            'allowed_to_read' => 1,
1565
            'allowed_folders' => '',
1566
            'enabled' => 0,
1567
        )
1568
    );
1569
1570
    // Create personnal folder
1571
    if (isKeyExistingAndEqual('enable_pf_feature', 1, $SETTINGS) === true) {
1572
        DB::insert(
1573
            prefixTable('nested_tree'),
1574
            [
1575
                'parent_id' => '0',
1576
                'title' => $newUserId,
1577
                'bloquer_creation' => '0',
1578
                'bloquer_modification' => '0',
1579
                'personal_folder' => '1',
1580
                'categories' => '',
1581
            ]
1582
        );
1583
        // Rebuild tree
1584
        $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
1585
        $tree->rebuild();
1586
    }
1587
1588
1589
    return [
1590
        'error' => false,
1591
        'message' => '',
1592
        'proceedIdentification' => true,
1593
        'user_initial_creation_through_external_ad' => true,
1594
        'id' => $newUserId,
1595
        'oauth2_login_ongoing' => true,
1596
    ];
1597
}
1598
1599
/**
1600
 * Undocumented function.
1601
 *
1602
 * @param string                $username     Username
1603
 * @param array                 $userInfo     Result of query
1604
 * @param string|array|resource $dataReceived DataReceived
1605
 * @param array                 $SETTINGS     Teampass settings
1606
 *
1607
 * @return array
1608
 */
1609
function googleMFACheck(string $username, array $userInfo, $dataReceived, array $SETTINGS): array
1610
{
1611
    $session = SessionManager::getSession();    
1612
    $lang = new Language($session->get('user-language') ?? 'english');
1613
1614
    if (
1615
        isset($dataReceived['GACode']) === true
1616
        && empty($dataReceived['GACode']) === false
1617
    ) {
1618
        $sessionAdmin = $session->get('user-admin');
1619
        $sessionUrl = $session->get('user-initial_url');
1620
        $sessionPwdAttempts = $session->get('pwd_attempts');
1621
        // create new instance
1622
        $tfa = new TwoFactorAuth($SETTINGS['ga_website_name']);
1623
        // Init
1624
        $firstTime = [];
1625
        // now check if it is the 1st time the user is using 2FA
1626
        if ($userInfo['ga_temporary_code'] !== 'none' && $userInfo['ga_temporary_code'] !== 'done') {
1627
            if ($userInfo['ga_temporary_code'] !== $dataReceived['GACode']) {
1628
                return [
1629
                    'error' => true,
1630
                    'message' => $lang->get('ga_bad_code'),
1631
                    'proceedIdentification' => false,
1632
                    'ga_bad_code' => true,
1633
                    'firstTime' => $firstTime,
1634
                ];
1635
            }
1636
1637
            // If first time with MFA code
1638
            $proceedIdentification = false;
1639
            
1640
            // generate new QR
1641
            $new_2fa_qr = $tfa->getQRCodeImageAsDataUri(
1642
                'Teampass - ' . $username,
1643
                $userInfo['ga']
1644
            );
1645
            // clear temporary code from DB
1646
            DB::update(
1647
                prefixTable('users'),
1648
                [
1649
                    'ga_temporary_code' => 'done',
1650
                ],
1651
                'id=%i',
1652
                $userInfo['id']
1653
            );
1654
            $firstTime = [
1655
                'value' => '<img src="' . $new_2fa_qr . '">',
1656
                'user_admin' => isset($sessionAdmin) ? (int) $sessionAdmin : '',
1657
                'initial_url' => isset($sessionUrl) === true ? $sessionUrl : '',
1658
                'pwd_attempts' => (int) $sessionPwdAttempts,
1659
                'message' => $lang->get('ga_flash_qr_and_login'),
1660
                'mfaStatus' => 'ga_temporary_code_correct',
1661
            ];
1662
        } else {
1663
            // verify the user GA code
1664
            if ($tfa->verifyCode($userInfo['ga'], $dataReceived['GACode'])) {
1665
                $proceedIdentification = true;
1666
            } else {
1667
                return [
1668
                    'error' => true,
1669
                    'message' => $lang->get('ga_bad_code'),
1670
                    'proceedIdentification' => false,
1671
                    'ga_bad_code' => true,
1672
                    'firstTime' => $firstTime,
1673
                ];
1674
            }
1675
        }
1676
    } else {
1677
        return [
1678
            'error' => true,
1679
            'message' => $lang->get('ga_bad_code'),
1680
            'proceedIdentification' => false,
1681
            'ga_bad_code' => true,
1682
            'firstTime' => [],
1683
        ];
1684
    }
1685
1686
    return [
1687
        'error' => false,
1688
        'message' => '',
1689
        'proceedIdentification' => $proceedIdentification,
1690
        'firstTime' => $firstTime,
1691
    ];
1692
}
1693
1694
1695
/**
1696
 * Perform DUO checks
1697
 *
1698
 * @param string $username
1699
 * @param string|array|resource $dataReceived
1700
 * @param array $SETTINGS
1701
 * @return array
1702
 */
1703
function duoMFACheck(
1704
    string $username,
1705
    $dataReceived,
1706
    array $SETTINGS
1707
): array
1708
{
1709
    $session = SessionManager::getSession();
1710
    $lang = new Language($session->get('user-language') ?? 'english');
1711
1712
    $sessionPwdAttempts = $session->get('pwd_attempts');
1713
    $saved_state = null !== $session->get('user-duo_state') ? $session->get('user-duo_state') : '';
1714
    $duo_status = null !== $session->get('user-duo_status') ? $session->get('user-duo_status') : '';
1715
1716
    // Ensure state and login are set
1717
    if (
1718
        (empty($saved_state) || empty($dataReceived['login']) || !isset($dataReceived['duo_state']) || empty($dataReceived['duo_state']))
1719
        && $duo_status === 'IN_PROGRESS'
1720
        && $dataReceived['duo_status'] !== 'start_duo_auth'
1721
    ) {
1722
        return [
1723
            'error' => true,
1724
            'message' => $lang->get('duo_no_data'),
1725
            'pwd_attempts' => (int) $sessionPwdAttempts,
1726
            'proceedIdentification' => false,
1727
        ];
1728
    }
1729
1730
    // Ensure state matches from initial request
1731
    if ($duo_status === 'IN_PROGRESS' && $dataReceived['duo_state'] !== $saved_state) {
1732
        $session->set('user-duo_state', '');
1733
        $session->set('user-duo_status', '');
1734
1735
        // We did not received a proper Duo state
1736
        return [
1737
            'error' => true,
1738
            'message' => $lang->get('duo_error_state'),
1739
            'pwd_attempts' => (int) $sessionPwdAttempts,
1740
            'proceedIdentification' => false,
1741
        ];
1742
    }
1743
1744
    return [
1745
        'error' => false,
1746
        'pwd_attempts' => (int) $sessionPwdAttempts,
1747
        'saved_state' => $saved_state,
1748
        'duo_status' => $duo_status,
1749
    ];
1750
}
1751
1752
1753
/**
1754
 * Create the redirect URL or check if the DUO Universal prompt was completed successfully.
1755
 *
1756
 * @param string                $username               Username
1757
 * @param string|array|resource $dataReceived           DataReceived
1758
 * @param array                 $sessionPwdAttempts     Nb of pwd attempts
1759
 * @param array                 $saved_state            Saved state
1760
 * @param array                 $duo_status             Duo status
1761
 * @param array                 $SETTINGS               Teampass settings
1762
 *
1763
 * @return array
1764
 */
1765
function duoMFAPerform(
1766
    string $username,
1767
    $dataReceived,
1768
    int $sessionPwdAttempts,
1769
    string $saved_state,
1770
    string $duo_status,
1771
    array $SETTINGS
1772
): array
1773
{
1774
    $session = SessionManager::getSession();
1775
    $lang = new Language($session->get('user-language') ?? 'english');
1776
1777
    try {
1778
        $duo_client = new Client(
1779
            $SETTINGS['duo_ikey'],
1780
            $SETTINGS['duo_skey'],
1781
            $SETTINGS['duo_host'],
1782
            $SETTINGS['cpassman_url'].'/'.DUO_CALLBACK
1783
        );
1784
    } catch (DuoException $e) {
1785
        return [
1786
            'error' => true,
1787
            'message' => $lang->get('duo_config_error'),
1788
            'debug_message' => $e->getMessage(),
1789
            'pwd_attempts' => (int) $sessionPwdAttempts,
1790
            'proceedIdentification' => false,
1791
        ];
1792
    }
1793
        
1794
    try {
1795
        $duo_error = $lang->get('duo_error_secure');
1796
        $duo_failmode = "none";
1797
        $duo_client->healthCheck();
1798
    } catch (DuoException $e) {
1799
        //Not implemented Duo Failmode in case the Duo services are not available
1800
        /*if ($SETTINGS['duo_failmode'] == "safe") {
1801
            # If we're failing open, errors in 2FA still allow for success
1802
            $duo_error = $lang->get('duo_error_failopen');
1803
            $duo_failmode = "safe";
1804
        } else {
1805
            # Duo has failed and is unavailable, redirect user to the login page
1806
            $duo_error = $lang->get('duo_error_secure');
1807
            $duo_failmode = "secure";
1808
        }*/
1809
        return [
1810
            'error' => true,
1811
            'message' => $duo_error . $lang->get('duo_error_check_config'),
1812
            'pwd_attempts' => (int) $sessionPwdAttempts,
1813
            'debug_message' => $e->getMessage(),
1814
            'proceedIdentification' => false,
1815
        ];
1816
    }
1817
    
1818
    // Check if no one played with the javascript
1819
    if ($duo_status !== 'IN_PROGRESS' && $dataReceived['duo_status'] === 'start_duo_auth') {
1820
        # Create the Duo URL to send the user to
1821
        try {
1822
            $duo_state = $duo_client->generateState();
1823
            $duo_redirect_url = $duo_client->createAuthUrl($username, $duo_state);
1824
        } catch (DuoException $e) {
1825
            return [
1826
                'error' => true,
1827
                'message' => $duo_error . $lang->get('duo_error_url'),
1828
                'pwd_attempts' => (int) $sessionPwdAttempts,
1829
                'debug_message' => $e->getMessage(),
1830
                'proceedIdentification' => false,
1831
            ];
1832
        }
1833
        
1834
        // Somethimes Duo return success but fail to return a URL, double check if the URL has been created
1835
        if (!empty($duo_redirect_url) && filter_var($duo_redirect_url,FILTER_SANITIZE_URL)) {
1836
            // Since Duo Universal requires a redirect, let's store some info when the user get's back after completing the Duo prompt
1837
            $key = hash('sha256', $duo_state);
1838
            $iv = substr(hash('sha256', $duo_state), 0, 16);
1839
            $duo_data = serialize([
1840
                'duo_login' => $username,
1841
                'duo_pwd' => $dataReceived['pw'],
1842
            ]);
1843
            $duo_data_enc = openssl_encrypt($duo_data, 'AES-256-CBC', $key, 0, $iv);
1844
            $session->set('user-duo_state', $duo_state);
1845
            $session->set('user-duo_data', base64_encode($duo_data_enc));
1846
            $session->set('user-duo_status', 'IN_PROGRESS');
1847
            $session->set('user-login', $username);
1848
            
1849
            // If we got here we can reset the password attempts
1850
            $session->set('pwd_attempts', 0);
1851
            
1852
            return [
1853
                'error' => false,
1854
                'message' => '',
1855
                'proceedIdentification' => false,
1856
                'duo_url_ready' => true,
1857
                'duo_redirect_url' => $duo_redirect_url,
1858
                'duo_failmode' => $duo_failmode,
1859
            ];
1860
        } else {
1861
            return [
1862
                'error' => true,
1863
                'message' => $duo_error . $lang->get('duo_error_url'),
1864
                'pwd_attempts' => (int) $sessionPwdAttempts,
1865
                'proceedIdentification' => false,
1866
            ];
1867
        }
1868
    } elseif ($duo_status === 'IN_PROGRESS' && $dataReceived['duo_code'] !== '') {
1869
        try {
1870
            // Check if the Duo code received is valid
1871
            $decoded_token = $duo_client->exchangeAuthorizationCodeFor2FAResult($dataReceived['duo_code'], $username);
1872
        } catch (DuoException $e) {
1873
            return [
1874
                'error' => true,
1875
                'message' => $lang->get('duo_error_decoding'),
1876
                'pwd_attempts' => (int) $sessionPwdAttempts,
1877
                'debug_message' => $e->getMessage(),
1878
                'proceedIdentification' => false,
1879
            ];
1880
        }
1881
        // return the response (which should be the user name)
1882
        if ($decoded_token['preferred_username'] === $username) {
1883
            $session->set('user-duo_status', 'COMPLET');
1884
            $session->set('user-duo_state','');
1885
            $session->set('user-duo_data','');
1886
            $session->set('user-login', $username);
1887
1888
            return [
1889
                'error' => false,
1890
                'message' => '',
1891
                'proceedIdentification' => true,
1892
                'authenticated_username' => $decoded_token['preferred_username']
1893
            ];
1894
        } else {
1895
            // Something wrong, username from the original Duo request is different than the one received now
1896
            $session->set('user-duo_status','');
1897
            $session->set('user-duo_state','');
1898
            $session->set('user-duo_data','');
1899
1900
            return [
1901
                'error' => true,
1902
                'message' => $lang->get('duo_login_mismatch'),
1903
                'pwd_attempts' => (int) $sessionPwdAttempts,
1904
                'proceedIdentification' => false,
1905
            ];
1906
        }
1907
    }
1908
    // If we are here something wrong
1909
    $session->set('user-duo_status','');
1910
    $session->set('user-duo_state','');
1911
    $session->set('user-duo_data','');
1912
    return [
1913
        'error' => true,
1914
        'message' => $lang->get('duo_login_mismatch'),
1915
        'pwd_attempts' => (int) $sessionPwdAttempts,
1916
        'proceedIdentification' => false,
1917
    ];
1918
}
1919
1920
/**
1921
 * Undocumented function.
1922
 *
1923
 * @param string                $passwordClear Password in clear
1924
 * @param array|string          $userInfo      Array of user data
1925
 *
1926
 * @return arrau
0 ignored issues
show
The type arrau was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1927
 */
1928
function checkCredentials($passwordClear, $userInfo): array
1929
{
1930
    $passwordManager = new PasswordManager();
1931
    // Migrate password if needed
1932
    $result = $passwordManager->migratePassword(
1933
        $userInfo['pw'],
1934
        $passwordClear,
1935
        (int) $userInfo['id'],
1936
        (bool) $userInfo['admin']
1937
    );
1938
1939
    if ($result['status'] === false || $passwordManager->verifyPassword($result['hashedPassword'], $passwordClear) === false) {
1940
        // Password is not correct
1941
        return [
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('authentica...e, 'migrated' => false) returns the type array<string,false> which is incompatible with the documented return type arrau.
Loading history...
1942
            'authenticated' => false,
1943
            'migrated' => false
1944
        ];
1945
    }
1946
1947
    return [
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('authentica...result['migratedUser']) returns the type array<string,mixed|true> which is incompatible with the documented return type arrau.
Loading history...
1948
        'authenticated' => true,
1949
        'migrated' => $result['migratedUser']
1950
    ];
1951
}
1952
1953
/**
1954
 * Undocumented function.
1955
 *
1956
 * @param bool   $enabled text1
1957
 * @param string $dbgFile text2
1958
 * @param string $text    text3
1959
 */
1960
function debugIdentify(bool $enabled, string $dbgFile, string $text): void
1961
{
1962
    if ($enabled === true) {
1963
        $fp = fopen($dbgFile, 'a');
1964
        if ($fp !== false) {
1965
            fwrite(
1966
                $fp,
1967
                $text
1968
            );
1969
        }
1970
    }
1971
}
1972
1973
1974
1975
function identifyGetUserCredentials(
1976
    array $SETTINGS,
1977
    string $serverPHPAuthUser,
1978
    string $serverPHPAuthPw,
1979
    string $userPassword,
1980
    string $userLogin
1981
): array
1982
{
1983
    if ((int) $SETTINGS['enable_http_request_login'] === 1
1984
        && $serverPHPAuthUser !== null
1985
        && (int) $SETTINGS['maintenance_mode'] === 1
1986
    ) {
1987
        if (strpos($serverPHPAuthUser, '@') !== false) {
1988
            return [
1989
                'username' => explode('@', $serverPHPAuthUser)[0],
1990
                'passwordClear' => $serverPHPAuthPw
1991
            ];
1992
        }
1993
        
1994
        if (strpos($serverPHPAuthUser, '\\') !== false) {
1995
            return [
1996
                'username' => explode('\\', $serverPHPAuthUser)[1],
1997
                'passwordClear' => $serverPHPAuthPw
1998
            ];
1999
        }
2000
2001
        return [
2002
            'username' => $serverPHPAuthPw,
2003
            'passwordClear' => $serverPHPAuthPw
2004
        ];
2005
    }
2006
    
2007
    return [
2008
        'username' => $userLogin,
2009
        'passwordClear' => $userPassword
2010
    ];
2011
}
2012
2013
2014
class initialChecks {
2015
    // Properties
2016
    public $login;
2017
2018
    /**
2019
     * Check if the user or his IP address is blocked due to a high number of
2020
     * failed attempts.
2021
     * 
2022
     * @param string $username - The login tried to login.
2023
     * @param string $ip - The remote address of the user.
2024
     */
2025
    public function isTooManyPasswordAttempts($username, $ip) {
2026
2027
        // Check for existing lock
2028
        $unlock_at = DB::queryFirstField(
2029
            'SELECT MAX(unlock_at)
2030
             FROM ' . prefixTable('auth_failures') . '
2031
             WHERE unlock_at > %s
2032
             AND ((source = %s AND value = %s) OR (source = %s AND value = %s))',
2033
            date('Y-m-d H:i:s', time()),
2034
            'login',
2035
            $username,
2036
            'remote_ip',
2037
            $ip
2038
        );
2039
2040
        // Account or remote address locked
2041
        if ($unlock_at) {
2042
            throw new Exception((string) $unlock_at);
2043
        }
2044
    }
2045
2046
    public function getUserInfo($login, $enable_ad_user_auto_creation, $oauth2_enabled) {
2047
        $session = SessionManager::getSession();
2048
    
2049
        // Get user info from DB
2050
        $data = DB::queryFirstRow(
2051
            'SELECT u.*, a.value AS api_key
2052
            FROM ' . prefixTable('users') . ' AS u
2053
            LEFT JOIN ' . prefixTable('api') . ' AS a ON (u.id = a.user_id)
2054
            WHERE login = %s AND deleted_at IS NULL',
2055
            $login
2056
        );
2057
    
2058
        // User doesn't exist then return error
2059
        // Except if user creation from LDAP is enabled
2060
        if (
2061
            DB::count() === 0
2062
            && !filter_var($enable_ad_user_auto_creation, FILTER_VALIDATE_BOOLEAN) 
2063
            && !filter_var($oauth2_enabled, FILTER_VALIDATE_BOOLEAN)
2064
        ) {
2065
            throw new Exception("error");
2066
        }
2067
    
2068
        // We cannot create a user with LDAP if the OAuth2 login is ongoing
2069
        $data['oauth2_login_ongoing'] = filter_var($session->get('userOauth2Info')['oauth2LoginOngoing'] ?? false, FILTER_VALIDATE_BOOLEAN) ?? false;
2070
    
2071
        $data['ldap_user_to_be_created'] = (
2072
            filter_var($enable_ad_user_auto_creation, FILTER_VALIDATE_BOOLEAN) &&
2073
            DB::count() === 0 &&
2074
            !$data['oauth2_login_ongoing']
2075
        );
2076
        $data['oauth2_user_not_exists'] = (
2077
            filter_var($oauth2_enabled, FILTER_VALIDATE_BOOLEAN) &&
2078
            DB::count() === 0 &&
2079
            $data['oauth2_login_ongoing']
2080
        );
2081
    
2082
        return $data;
2083
    }
2084
2085
    public function isMaintenanceModeEnabled($maintenance_mode, $user_admin) {
2086
        if ((int) $maintenance_mode === 1 && (int) $user_admin === 0) {
2087
            throw new Exception(
2088
                "error" 
2089
            );
2090
        }
2091
    }
2092
2093
    public function is2faCodeRequired(
2094
        $yubico,
2095
        $ga,
2096
        $duo,
2097
        $admin,
2098
        $adminMfaRequired,
2099
        $mfa,
2100
        $userMfaSelection,
2101
        $userMfaEnabled
2102
    ) {
2103
        if (
2104
            (empty($userMfaSelection) === true &&
2105
            isOneVarOfArrayEqualToValue(
2106
                [
2107
                    (int) $yubico,
2108
                    (int) $ga,
2109
                    (int) $duo
2110
                ],
2111
                1
2112
            ) === true)
2113
            && (((int) $admin !== 1 && $userMfaEnabled === true) || ((int) $adminMfaRequired === 1 && (int) $admin === 1))
2114
            && $mfa === true
2115
        ) {
2116
            throw new Exception(
2117
                "error" 
2118
            );
2119
        }
2120
    }
2121
2122
    public function isInstallFolderPresent($admin, $install_folder) {
2123
        if ((int) $admin === 1 && is_dir($install_folder) === true) {
2124
            throw new Exception(
2125
                "error" 
2126
            );
2127
        }
2128
    }
2129
}
2130
2131
2132
/**
2133
 * Permit to get info about user before auth step
2134
 *
2135
 * @param array $SETTINGS
2136
 * @param integer $sessionPwdAttempts
2137
 * @param string $username
2138
 * @param integer $sessionAdmin
2139
 * @param string $sessionUrl
2140
 * @param string $user2faSelection
2141
 * @param boolean $oauth2Token
2142
 * @return array
2143
 */
2144
function identifyDoInitialChecks(
2145
    $SETTINGS,
2146
    int $sessionPwdAttempts,
2147
    string $username,
2148
    int $sessionAdmin,
2149
    string $sessionUrl,
2150
    string $user2faSelection
2151
): array
2152
{
2153
    $session = SessionManager::getSession();
2154
    $checks = new initialChecks();
2155
    $enableAdUserAutoCreation = $SETTINGS['enable_ad_user_auto_creation'] ?? false;
2156
    $oauth2Enabled = $SETTINGS['oauth2_enabled'] ?? false;
2157
    $lang = new Language($session->get('user-language') ?? 'english');
2158
2159
    // Brute force management
2160
    try {
2161
        $checks->isTooManyPasswordAttempts($username, getClientIpServer());
2162
    } catch (Exception $e) {
2163
        $session->set('userOauth2Info', '');
2164
        logEvents($SETTINGS, 'failed_auth', 'user_not_exists', '', stripslashes($username), stripslashes($username));
2165
        return [
2166
            'error' => true,
2167
            'skip_anti_bruteforce' => true,
2168
            'array' => [
2169
                'value' => 'bruteforce_wait',
2170
                'error' => true,
2171
                'message' => $lang->get('bruteforce_wait') . (string) $e->getMessage(),
2172
            ]
2173
        ];
2174
    }
2175
2176
    // Check if user exists
2177
    try {
2178
        $userInfo = $checks->getUserInfo($username, $enableAdUserAutoCreation, $oauth2Enabled);
2179
    } catch (Exception $e) {
2180
        logEvents($SETTINGS, 'failed_auth', 'user_not_exists', '', stripslashes($username), stripslashes($username));
2181
        return [
2182
            'error' => true,
2183
            'array' => [
2184
                'error' => true,
2185
                'message' => $lang->get('error_bad_credentials'),
2186
            ]
2187
        ];
2188
    }
2189
2190
    // Manage Maintenance mode
2191
    try {
2192
        $checks->isMaintenanceModeEnabled(
2193
            $SETTINGS['maintenance_mode'],
2194
            $userInfo['admin']
2195
        );
2196
    } catch (Exception $e) {
2197
        return [
2198
            'error' => true,
2199
            'skip_anti_bruteforce' => true,
2200
            'array' => [
2201
                'value' => '',
2202
                'error' => 'maintenance_mode_enabled',
2203
                'message' => '',
2204
            ]
2205
        ];
2206
    }
2207
    
2208
    // user should use MFA?
2209
    $userInfo['mfa_auth_requested_roles'] = mfa_auth_requested_roles(
2210
        (string) $userInfo['fonction_id'],
2211
        is_null($SETTINGS['mfa_for_roles']) === true ? '' : (string) $SETTINGS['mfa_for_roles']
2212
    );
2213
2214
    // Check if 2FA code is requested
2215
    try {
2216
        $checks->is2faCodeRequired(
2217
            $SETTINGS['yubico_authentication'],
2218
            $SETTINGS['google_authentication'],
2219
            $SETTINGS['duo'],
2220
            $userInfo['admin'],
2221
            $SETTINGS['admin_2fa_required'],
2222
            $userInfo['mfa_auth_requested_roles'],
2223
            $user2faSelection,
2224
            $userInfo['mfa_enabled']
2225
        );
2226
    } catch (Exception $e) {
2227
        return [
2228
            'error' => true,
2229
            'array' => [
2230
                'value' => '2fa_not_set',
2231
                'user_admin' => (int) $sessionAdmin,
2232
                'initial_url' => $sessionUrl,
2233
                'pwd_attempts' => (int) $sessionPwdAttempts,
2234
                'error' => '2fa_not_set',
2235
                'message' => $lang->get('select_valid_2fa_credentials'),
2236
            ]
2237
        ];
2238
    }
2239
    // If admin user then check if folder install exists
2240
    // if yes then refuse connection
2241
    try {
2242
        $checks->isInstallFolderPresent(
2243
            $userInfo['admin'],
2244
            '../install'
2245
        );
2246
    } catch (Exception $e) {
2247
        return [
2248
            'error' => true,
2249
            'array' => [
2250
                'value' => '',
2251
                'user_admin' => $sessionAdmin,
2252
                'initial_url' => $sessionUrl,
2253
                'pwd_attempts' => (int) $sessionPwdAttempts,
2254
                'error' => true,
2255
                'message' => $lang->get('remove_install_folder'),
2256
            ]
2257
        ];
2258
    }
2259
2260
    // Check if  migration of password hash has previously failed
2261
    //cleanFailedAuthRecords($userInfo);
2262
2263
    // Return some usefull information about user
2264
    return [
2265
        'error' => false,
2266
        'user_mfa_mode' => $user2faSelection,
2267
        'userInfo' => $userInfo,
2268
    ];
2269
}
2270
2271
function cleanFailedAuthRecords(array $userInfo) : void
2272
{
2273
    // Clean previous failed attempts
2274
    $failedTasks = DB::query(
2275
        'SELECT increment_id
2276
        FROM ' . prefixTable('background_tasks') . '
2277
        WHERE process_type = %s
2278
        AND JSON_EXTRACT(arguments, "$.new_user_id") = %i
2279
        AND status = %s',
2280
        'create_user_keys',
2281
        $userInfo['id'],
2282
        'failed'
2283
    );
2284
2285
    // Supprimer chaque tâche échouée et ses sous-tâches
2286
    foreach ($failedTasks as $task) {
2287
        $incrementId = $task['increment_id'];
2288
2289
        // Supprimer les sous-tâches associées
2290
        DB::delete(
2291
            prefixTable('background_subtasks'),
2292
            'sub_task_in_progress = %i',
2293
            $incrementId
2294
        );
2295
2296
        // Supprimer la tâche principale
2297
        DB::delete(
2298
            prefixTable('background_tasks'),
2299
            'increment_id = %i',
2300
            $incrementId
2301
        );
2302
    }
2303
}
2304
2305
function identifyDoLDAPChecks(
2306
    $SETTINGS,
2307
    $userInfo,
2308
    string $username,
2309
    string $passwordClear,
2310
    int $sessionAdmin,
2311
    string $sessionUrl,
2312
    int $sessionPwdAttempts
2313
): array
2314
{
2315
    $session = SessionManager::getSession();
2316
    $lang = new Language($session->get('user-language') ?? 'english');
2317
2318
    // Prepare LDAP connection if set up
2319
    if ((int) $SETTINGS['ldap_mode'] === 1
2320
        && $username !== 'admin'
2321
        && ((string) $userInfo['auth_type'] === 'ldap' || $userInfo['ldap_user_to_be_created'] === true)
2322
    ) {
2323
        $retLDAP = authenticateThroughAD(
2324
            $username,
2325
            $userInfo,
2326
            $passwordClear,
2327
            $SETTINGS
2328
        );
2329
        if ($retLDAP['error'] === true) {
2330
            return [
2331
                'error' => true,
2332
                'array' => [
2333
                    'value' => '',
2334
                    'user_admin' => $sessionAdmin,
2335
                    'initial_url' => $sessionUrl,
2336
                    'pwd_attempts' => (int) $sessionPwdAttempts,
2337
                    'error' => true,
2338
                    'message' => $lang->get('error_bad_credentials'),
2339
                ]
2340
            ];
2341
        }
2342
        return [
2343
            'error' => false,
2344
            'retLDAP' => $retLDAP,
2345
            'ldapConnection' => true,
2346
            'userPasswordVerified' => true,
2347
        ];
2348
    }
2349
2350
    // return if no addmin
2351
    return [
2352
        'error' => false,
2353
        'retLDAP' => [],
2354
        'ldapConnection' => false,
2355
        'userPasswordVerified' => false,
2356
    ];
2357
}
2358
2359
2360
function shouldUserAuthWithOauth2(
2361
    array $SETTINGS,
2362
    array $userInfo,
2363
    string $username
2364
): array
2365
{
2366
    // Security issue without this return if an user auth_type == oauth2 and
2367
    // oauth2 disabled : we can login as a valid user by using hashUserId(username)
2368
    // as password in the login the form.
2369
    if ((int) $SETTINGS['oauth2_enabled'] !== 1 && filter_var($userInfo['oauth2_login_ongoing'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) === true) {
2370
        return [
2371
            'error' => true,
2372
            'message' => 'user_not_allowed_to_auth_to_teampass_app',
2373
            'oauth2Connection' => false,
2374
            'userPasswordVerified' => false,
2375
        ];
2376
    }
2377
2378
    // Prepare Oauth2 connection if set up
2379
    if ($username !== 'admin') {
2380
        // User has started to auth with oauth2
2381
        if ((bool) $userInfo['oauth2_login_ongoing'] === true) {
2382
            // Case where user exists in Teampass password login type
2383
            if ((string) $userInfo['auth_type'] === 'ldap' || (string) $userInfo['auth_type'] === 'local') {
2384
                // Add transparent recovery data if available
2385
                /*if (isset($userKeys['user_seed'])) {
2386
                    // Recrypt user private key with oauth2
2387
                    recryptUserPrivateKeyWithOauth2(
2388
                        (int) $userInfo['id'],
2389
                        $userKeys['user_seed'],
2390
                        $userKeys['public_key']
2391
                    );
2392
                }*/
2393
                    error_log('Switch user ' . $username . ' auth_type to oauth2');
2394
                // Update user in database:
2395
                DB::update(
2396
                    prefixTable('users'),
2397
                    array(
2398
                        //'special' => 'recrypt-private-key',
2399
                        'auth_type' => 'oauth2',
2400
                    ),
2401
                    'id = %i',
2402
                    $userInfo['id']
2403
                );
2404
                // Update session auth type
2405
                $session = SessionManager::getSession();
2406
                $session->set('user-auth_type', 'oauth2');
2407
                // Accept login request
2408
                return [
2409
                    'error' => false,
2410
                    'message' => '',
2411
                    'oauth2Connection' => true,
2412
                    'userPasswordVerified' => true,
2413
                ];
2414
            } elseif ((string) $userInfo['auth_type'] === 'oauth2' || (bool) $userInfo['oauth2_login_ongoing'] === true) {
2415
                // OAuth2 login request on OAuth2 user account.
2416
                return [
2417
                    'error' => false,
2418
                    'message' => '',
2419
                    'oauth2Connection' => true,
2420
                    'userPasswordVerified' => true,
2421
                ];
2422
            } else {
2423
                // Case where auth_type is not managed
2424
                return [
2425
                    'error' => true,
2426
                    'message' => 'user_not_allowed_to_auth_to_teampass_app',
2427
                    'oauth2Connection' => false,
2428
                    'userPasswordVerified' => false,
2429
                ];
2430
            }
2431
        } else {
2432
            // User has started to auth the normal way
2433
            if ((string) $userInfo['auth_type'] === 'oauth2') {
2434
                // Case where user exists in Teampass but not allowed to auth with Oauth2
2435
                return [
2436
                    'error' => true,
2437
                    'message' => 'error_bad_credentials',
2438
                    'oauth2Connection' => false,
2439
                    'userPasswordVerified' => false,
2440
                ];
2441
            }
2442
        }
2443
    }
2444
2445
    // return if no addmin
2446
    return [
2447
        'error' => false,
2448
        'message' => '',
2449
        'oauth2Connection' => false,
2450
        'userPasswordVerified' => false,
2451
    ];
2452
}
2453
2454
function checkOauth2User(
2455
    array $SETTINGS,
2456
    array $userInfo,
2457
    string $username,
2458
    string $passwordClear,
2459
    int $userLdapHasBeenCreated
2460
): array
2461
{
2462
    // Is oauth2 user in Teampass?
2463
    if ((int) $SETTINGS['oauth2_enabled'] === 1
2464
        && $username !== 'admin'
2465
        && filter_var($userInfo['oauth2_user_not_exists'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) === true
2466
        && (int) $userLdapHasBeenCreated === 0
2467
    ) {
2468
        // Is allowed to self register with oauth2?
2469
        if (empty($SETTINGS['oauth_self_register_groups'])) {
2470
            // No self registration is allowed
2471
            return [
2472
                'error' => true,
2473
                'message' => 'error_bad_credentials',
2474
            ];
2475
        } else {
2476
            // Self registration is allowed
2477
            // Create user in Teampass
2478
            $userInfo['oauth2_user_to_be_created'] = true;
2479
            return createOauth2User(
2480
                $SETTINGS,
2481
                $userInfo,
2482
                $username,
2483
                $passwordClear,
2484
                true
2485
            );
2486
        }
2487
    
2488
    } elseif (isset($userInfo['id']) === true && empty($userInfo['id']) === false) {
2489
        // User is in construction, please wait for email
2490
        if (
2491
            isset($userInfo['is_ready_for_usage']) && (int) $userInfo['is_ready_for_usage'] !== 1 && 
2492
            $userInfo['ongoing_process_id'] !== null && (int) $userInfo['ongoing_process_id'] >= 0
2493
        ) {
2494
            // Check if the creation of user keys has failed
2495
            $errorMessage = checkIfUserKeyCreationFailed((int) $userInfo['id']);
2496
            if (!is_null($errorMessage)) {
2497
                // Refresh user info to permit retry
2498
                DB::update(
2499
                    prefixTable('users'),
2500
                    array(
2501
                        'is_ready_for_usage' => 1,
2502
                        'otp_provided' => 1,
2503
                        'ongoing_process_id' => NULL,
2504
                        'special' => 'none',
2505
                    ),
2506
                    'id = %i',
2507
                    $userInfo['id']
2508
                );
2509
2510
                // Prepare task to create user keys again
2511
                handleUserKeys(
2512
                    (int) $userInfo['id'],
2513
                    (string) $passwordClear,
2514
                    (int) NUMBER_ITEMS_IN_BATCH,
2515
                    '',
2516
                    true,
2517
                    true,
2518
                    true,
2519
                    false,
2520
                    'email_body_user_config_4',
2521
                    true,
2522
                    '',
2523
                    '',
2524
                );
2525
2526
                // Return error message
2527
                return [
2528
                    'error' => true,
2529
                    'message' => $errorMessage,
2530
                    'no_log_event' => true
2531
                ];
2532
            } else {
2533
                return [
2534
                    'error' => true,
2535
                    'message' => 'account_in_construction_please_wait_email',
2536
                    'no_log_event' => true
2537
                ];
2538
            }
2539
        }
2540
2541
        // CCheck if user should use oauth2
2542
        $ret = shouldUserAuthWithOauth2(
2543
            $SETTINGS,
2544
            $userInfo,
2545
            $username
2546
        );
2547
        if ($ret['error'] === true) {
2548
            return [
2549
                'error' => true,
2550
                'message' => $ret['message'],
2551
            ];
2552
        }
2553
2554
        // login/password attempt on a local account:
2555
        // Return to avoid overwrite of user password that can allow a user
2556
        // to steal a local account.
2557
        if (!$ret['oauth2Connection'] || !$ret['userPasswordVerified']) {
2558
            return [
2559
                'error' => false,
2560
                'message' => $ret['message'],
2561
                'ldapConnection' => false,
2562
                'userPasswordVerified' => false,        
2563
            ];
2564
        }
2565
2566
        // Oauth2 user already exists and authenticated
2567
        $userInfo['has_been_created'] = 0;
2568
        $passwordManager = new PasswordManager();
2569
2570
        // Update user hash un database if needed
2571
        if (!$passwordManager->verifyPassword($userInfo['pw'], $passwordClear)) {
2572
            DB::update(
2573
                prefixTable('users'),
2574
                [
2575
                    'pw' => $passwordManager->hashPassword($passwordClear),
2576
                ],
2577
                'id = %i',
2578
                $userInfo['id']
2579
            );
2580
2581
            // Transparent recovery: handle external OAuth2 password change
2582
            handleExternalPasswordChange(
2583
                (int) $userInfo['id'],
2584
                $passwordClear,
2585
                $userInfo,
2586
                $SETTINGS
2587
            );
2588
        }
2589
2590
        return [
2591
            'error' => false,
2592
            'retExternalAD' => $userInfo,
2593
            'oauth2Connection' => $ret['oauth2Connection'],
2594
            'userPasswordVerified' => $ret['userPasswordVerified'],
2595
        ];
2596
    }
2597
2598
    // return if no admin
2599
    return [
2600
        'error' => false,
2601
        'retLDAP' => [],
2602
        'ldapConnection' => false,
2603
        'userPasswordVerified' => false,
2604
    ];
2605
}
2606
2607
/**
2608
 * Check if a "create_user_keys" task failed for the given user_id.
2609
 *
2610
 * @param int $userId The user ID to check.
2611
 * @return string|null Returns an error message in English if a failed task is found, otherwise null.
2612
 */
2613
function checkIfUserKeyCreationFailed(int $userId): ?string
2614
{
2615
    // Find the latest "create_user_keys" task for the given user_id
2616
    $latestTask = DB::queryFirstRow(
2617
        'SELECT arguments, status FROM ' . prefixTable('background_tasks') . '
2618
        WHERE process_type = %s
2619
        AND arguments LIKE %s
2620
        ORDER BY increment_id DESC
2621
        LIMIT 1',
2622
        'create_user_keys', '%"new_user_id":' . $userId . '%'
2623
    );
2624
2625
    // If a failed task is found, return an error message
2626
    if ($latestTask && $latestTask['status'] === 'failed') {
2627
        return "The creation of user keys for user ID {$userId} failed. Please contact your administrator to check the background tasks log for more details.";
2628
    }
2629
2630
    // No failed task found for this user_id
2631
    return null;
2632
}
2633
2634
2635
/* * Create the user in Teampass
2636
 *
2637
 * @param array $SETTINGS
2638
 * @param array $userInfo
2639
 * @param string $username
2640
 * @param string $passwordClear
2641
 *
2642
 * @return array
2643
 */
2644
function createOauth2User(
2645
    array $SETTINGS,
2646
    array $userInfo,
2647
    string $username,
2648
    string $passwordClear,
2649
    bool $userSelfRegister = false
2650
): array
2651
{
2652
    // Prepare creating the new oauth2 user in Teampass
2653
    if ((int) $SETTINGS['oauth2_enabled'] === 1
2654
        && $username !== 'admin'
2655
        && filter_var($userInfo['oauth2_user_to_be_created'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) === true
2656
    ) {
2657
        $session = SessionManager::getSession();    
2658
        $lang = new Language($session->get('user-language') ?? 'english');
2659
2660
        // Prepare user groups
2661
        foreach ($userInfo['groups'] as $key => $group) {
2662
            // Check if the group is in the list of groups allowed to self register
2663
            // If the group is in the list, we remove it
2664
            if ($userSelfRegister === true && $group["displayName"] === $SETTINGS['oauth_self_register_groups']) {
2665
                unset($userInfo['groups'][$key]);
2666
            }
2667
        }
2668
        // Rebuild indexes
2669
        $userInfo['groups'] = array_values($userInfo['groups'] ?? []);
2670
        
2671
        // Create Oauth2 user if not exists and tasks enabled
2672
        $ret = externalAdCreateUser(
2673
            $username,
2674
            $passwordClear,
2675
            $userInfo['mail'] ?? '',
2676
            is_null($userInfo['givenname']) ? (is_null($userInfo['givenName']) ? '' : $userInfo['givenName']) : $userInfo['givenname'],
2677
            is_null($userInfo['surname']) ? '' : $userInfo['surname'],
2678
            'oauth2',
2679
            is_null($userInfo['groups']) ? [] : $userInfo['groups'],
2680
            $SETTINGS
2681
        );
2682
        $userInfo = array_merge($userInfo, $ret);
2683
2684
        // prepapre background tasks for item keys generation  
2685
        handleUserKeys(
2686
            (int) $userInfo['id'],
2687
            (string) $passwordClear,
2688
            (int) (isset($SETTINGS['maximum_number_of_items_to_treat']) === true ? $SETTINGS['maximum_number_of_items_to_treat'] : NUMBER_ITEMS_IN_BATCH),
2689
            uniqidReal(20),
2690
            true,
2691
            true,
2692
            true,
2693
            false,
2694
            $lang->get('email_body_user_config_2'),
2695
        );
2696
2697
        // Complete $userInfo
2698
        $userInfo['has_been_created'] = 1;
2699
2700
        if (WIP === true) error_log("--- USER CREATED ---");
2701
2702
        return [
2703
            'error' => false,
2704
            'retExternalAD' => $userInfo,
2705
            'oauth2Connection' => true,
2706
            'userPasswordVerified' => true,
2707
        ];
2708
    
2709
    } elseif (isset($userInfo['id']) === true && empty($userInfo['id']) === false) {
2710
        // CHeck if user should use oauth2
2711
        $ret = shouldUserAuthWithOauth2(
2712
            $SETTINGS,
2713
            $userInfo,
2714
            $username
2715
        );
2716
        if ($ret['error'] === true) {
2717
            return [
2718
                'error' => true,
2719
                'message' => $ret['message'],
2720
            ];
2721
        }
2722
2723
        // login/password attempt on a local account:
2724
        // Return to avoid overwrite of user password that can allow a user
2725
        // to steal a local account.
2726
        if (!$ret['oauth2Connection'] || !$ret['userPasswordVerified']) {
2727
            return [
2728
                'error' => false,
2729
                'message' => $ret['message'],
2730
                'ldapConnection' => false,
2731
                'userPasswordVerified' => false,        
2732
            ];
2733
        }
2734
2735
        // Oauth2 user already exists and authenticated
2736
        if (WIP === true) error_log("--- USER AUTHENTICATED ---");
2737
        $userInfo['has_been_created'] = 0;
2738
2739
        $passwordManager = new PasswordManager();
2740
2741
        // Update user hash un database if needed
2742
        if (!$passwordManager->verifyPassword($userInfo['pw'], $passwordClear)) {
2743
            DB::update(
2744
                prefixTable('users'),
2745
                [
2746
                    'pw' => $passwordManager->hashPassword($passwordClear),
2747
                ],
2748
                'id = %i',
2749
                $userInfo['id']
2750
            );
2751
        }
2752
2753
        return [
2754
            'error' => false,
2755
            'retExternalAD' => $userInfo,
2756
            'oauth2Connection' => $ret['oauth2Connection'],
2757
            'userPasswordVerified' => $ret['userPasswordVerified'],
2758
        ];
2759
    }
2760
2761
    // return if no admin
2762
    return [
2763
        'error' => false,
2764
        'retLDAP' => [],
2765
        'ldapConnection' => false,
2766
        'userPasswordVerified' => false,
2767
    ];
2768
}
2769
2770
function identifyDoMFAChecks(
2771
    $SETTINGS,
2772
    $userInfo,
2773
    $dataReceived,
2774
    $userInitialData,
2775
    string $username
2776
): array
2777
{
2778
    $session = SessionManager::getSession();
2779
    $lang = new Language($session->get('user-language') ?? 'english');
2780
    
2781
    switch ($userInitialData['user_mfa_mode']) {
2782
        case 'google':
2783
            $ret = googleMFACheck(
2784
                $username,
2785
                $userInfo,
2786
                $dataReceived,
2787
                $SETTINGS
2788
            );
2789
            if ($ret['error'] !== false) {
2790
                logEvents($SETTINGS, 'failed_auth', 'wrong_mfa_code', '', stripslashes($username), stripslashes($username));
2791
                return [
2792
                    'error' => true,
2793
                    'mfaData' => $ret,
2794
                    'mfaQRCodeInfos' => false,
2795
                ];
2796
            }
2797
2798
            return [
2799
                'error' => false,
2800
                'mfaData' => $ret['firstTime'],
2801
                'mfaQRCodeInfos' => $userInitialData['user_mfa_mode'] === 'google'
2802
                && count($ret['firstTime']) > 0 ? true : false,
2803
            ];
2804
        
2805
        case 'duo':
2806
            // Prepare Duo connection if set up
2807
            $checks = duoMFACheck(
2808
                $username,
2809
                $dataReceived,
2810
                $SETTINGS
2811
            );
2812
2813
            if ($checks['error'] === true) {
2814
                return [
2815
                    'error' => true,
2816
                    'mfaData' => $checks,
2817
                    'mfaQRCodeInfos' => false,
2818
                ];
2819
            }
2820
2821
            // If we are here
2822
            // Do DUO authentication
2823
            $ret = duoMFAPerform(
2824
                $username,
2825
                $dataReceived,
2826
                $checks['pwd_attempts'],
2827
                $checks['saved_state'],
2828
                $checks['duo_status'],
2829
                $SETTINGS
2830
            );
2831
2832
            if ($ret['error'] !== false) {
2833
                logEvents($SETTINGS, 'failed_auth', 'bad_duo_mfa', '', stripslashes($username), stripslashes($username));
2834
                $session->set('user-duo_status','');
2835
                $session->set('user-duo_state','');
2836
                $session->set('user-duo_data','');
2837
                return [
2838
                    'error' => true,
2839
                    'mfaData' => $ret,
2840
                    'mfaQRCodeInfos' => false,
2841
                ];
2842
            } else if ($ret['duo_url_ready'] === true){
2843
                return [
2844
                    'error' => false,
2845
                    'mfaData' => $ret,
2846
                    'duo_url_ready' => true,
2847
                    'mfaQRCodeInfos' => false,
2848
                ];
2849
            } else if ($ret['error'] === false) {
2850
                return [
2851
                    'error' => false,
2852
                    'mfaData' => $ret,
2853
                    'mfaQRCodeInfos' => false,
2854
                ];
2855
            }
2856
            break;
2857
        
2858
        default:
2859
            logEvents($SETTINGS, 'failed_auth', 'wrong_mfa_code', '', stripslashes($username), stripslashes($username));
2860
            return [
2861
                'error' => true,
2862
                'mfaData' => ['message' => $lang->get('wrong_mfa_code')],
2863
                'mfaQRCodeInfos' => false,
2864
            ];
2865
    }
2866
2867
    // If something went wrong, let's catch and return an error
2868
    logEvents($SETTINGS, 'failed_auth', 'wrong_mfa_code', '', stripslashes($username), stripslashes($username));
2869
    return [
2870
        'error' => true,
2871
        'mfaData' => ['message' => $lang->get('wrong_mfa_code')],
2872
        'mfaQRCodeInfos' => false,
2873
    ];
2874
}
2875
2876
function identifyDoAzureChecks(
2877
    array $SETTINGS,
2878
    $userInfo,
2879
    string $username
2880
): array
2881
{
2882
    $session = SessionManager::getSession();
2883
    $lang = new Language($session->get('user-language') ?? 'english');
2884
2885
    logEvents($SETTINGS, 'failed_auth', 'wrong_mfa_code', '', stripslashes($username), stripslashes($username));
2886
    return [
2887
        'error' => true,
2888
        'mfaData' => ['message' => $lang->get('wrong_mfa_code')],
2889
        'mfaQRCodeInfos' => false,
2890
    ];
2891
}
2892
2893
/**
2894
 * Add a failed authentication attempt to the database.
2895
 * If the number of failed attempts exceeds the limit, a lock is triggered.
2896
 * 
2897
 * @param string $source - The source of the failed attempt (login or remote_ip).
2898
 * @param string $value  - The value for this source (username or IP address).
2899
 * @param int    $limit  - The failure attempt limit after which the account/IP
2900
 *                         will be locked.
2901
 */
2902
function handleFailedAttempts($source, $value, $limit) {
2903
    // Count failed attempts from this source
2904
    $count = DB::queryFirstField(
2905
        'SELECT COUNT(*)
2906
        FROM ' . prefixTable('auth_failures') . '
2907
        WHERE source = %s AND value = %s',
2908
        $source,
2909
        $value
2910
    );
2911
2912
    // Add this attempt
2913
    $count++;
2914
2915
    // Calculate unlock time if number of attempts exceeds limit
2916
    $unlock_at = $count >= $limit
2917
        ? date('Y-m-d H:i:s', time() + (($count - $limit + 1) * 600))
2918
        : NULL;
2919
2920
    // Unlock account one time code
2921
    $unlock_code = ($count >= $limit && $source === 'login')
2922
        ? generateQuickPassword(30, false)
2923
        : NULL;
2924
2925
    // Insert the new failure into the database
2926
    DB::insert(
2927
        prefixTable('auth_failures'),
2928
        [
2929
            'source' => $source,
2930
            'value' => $value,
2931
            'unlock_at' => $unlock_at,
2932
            'unlock_code' => $unlock_code,
2933
        ]
2934
    );
2935
2936
    if ($unlock_at !== null && $source === 'login') {
2937
        $configManager = new ConfigManager();
2938
        $SETTINGS = $configManager->getAllSettings();
2939
        $lang = new Language($SETTINGS['default_language']);
2940
2941
        // Get user email
2942
        $userInfos = DB::queryFirstRow(
2943
            'SELECT email, name
2944
             FROM '.prefixTable('users').'
2945
             WHERE login = %s',
2946
             $value
2947
        );
2948
2949
        // No valid email address for user
2950
        if (!$userInfos || !filter_var($userInfos['email'], FILTER_VALIDATE_EMAIL))
2951
            return;
2952
2953
        $unlock_url = $SETTINGS['cpassman_url'].'/self-unlock.php?login='.$value.'&otp='.$unlock_code;
2954
2955
        sendMailToUser(
2956
            $userInfos['email'],
2957
            $lang->get('bruteforce_reset_mail_body'),
2958
            $lang->get('bruteforce_reset_mail_subject'),
2959
            [
2960
                '#name#' => $userInfos['name'],
2961
                '#reset_url#' => $unlock_url,
2962
                '#unlock_at#' => $unlock_at,
2963
            ],
2964
            true
2965
        );
2966
    }
2967
}
2968
2969
/**
2970
 * Add failed authentication attempts for both user login and IP address.
2971
 * This function will check the number of attempts for both the username and IP,
2972
 * and will trigger a lock if the number exceeds the defined limits.
2973
 * It also deletes logs older than 24 hours.
2974
 * 
2975
 * @param string $username - The username that was attempted to login.
2976
 * @param string $ip       - The IP address from which the login attempt was made.
2977
 */
2978
function addFailedAuthentication($username, $ip) {
2979
    $user_limit = 10;
2980
    $ip_limit = 30;
2981
2982
    // Remove old logs (more than 24 hours)
2983
    DB::delete(
2984
        prefixTable('auth_failures'),
2985
        'date < %s AND (unlock_at < %s OR unlock_at IS NULL)',
2986
        date('Y-m-d H:i:s', time() - (24 * 3600)),
2987
        date('Y-m-d H:i:s', time())
2988
    );
2989
2990
    // Add attempts in database
2991
    handleFailedAttempts('login', $username, $user_limit);
2992
    handleFailedAttempts('remote_ip', $ip, $ip_limit);
2993
}
2994