Issues (15)

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 (1 issue)

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