Issues (16)

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