Issues (27)

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