Passed
Pull Request — master (#4822)
by Nils
06:23
created

adjustUserPermissionsByRole()   B

Complexity

Conditions 9
Paths 16

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 12
nc 16
nop 2
dl 0
loc 20
rs 8.0555
c 0
b 0
f 0
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
/**
131
 * Complete authentication of user through Teampass
132
 *
133
 * @param string $sentData Credentials
134
 * @param array $SETTINGS Teampass settings
135
 *
136
 * @return bool
137
 */
138
function identifyUser(string $sentData, array $SETTINGS): bool
139
{
140
    $authContext = new AuthenticationContext($sentData, $SETTINGS);
141
    
142
    try {
143
        // Step 1: Prepare and validate input data
144
        $credentials = $authContext->prepareCredentials();
145
        if (!$credentials) {
146
            return false;
147
        }
148
149
        // Step 2: Initial user checks
150
        $userInitialData = $authContext->performInitialChecks($credentials);
151
        if (!$userInitialData) {
152
            return false;
153
        }
154
155
        // Step 3: External authentication (LDAP/OAuth2)
156
        $externalAuthResult = $authContext->performExternalAuthentication($credentials, $userInitialData);
157
        if (!$externalAuthResult) {
158
            return false;
159
        }
160
161
        // Step 4: Credential verification
162
        if (!$authContext->verifyCredentials($credentials, $userInitialData, $externalAuthResult)) {
163
            return false;
164
        }
165
166
        // Step 5: MFA checks
167
        $mfaResult = $authContext->performMFAChecks($credentials, $userInitialData);
168
        if ($mfaResult !== true) {
169
            return $mfaResult === 'continue' ? false : $mfaResult;
170
        }
171
172
        // Step 6: Final login checks and session setup
173
        return $authContext->finalizeAuthentication($credentials, $userInitialData, $externalAuthResult);
174
175
    } catch (AuthenticationException $e) {
176
        $authContext->logFailedAuthentication($credentials['username'] ?? '');
177
        $authContext->sendErrorResponse($e->getMessage(), $e->getExtra());
178
        return false;
179
    }
180
}
181
182
/**
183
 * Authentication context class to handle the authentication process
184
 */
185
class AuthenticationContext
186
{
187
    private $sentData;
188
    private $settings;
189
    private $session;
190
    private $request;
191
    private $lang;
192
    private $antiXss;
193
194
    public function __construct(string $sentData, array $settings)
195
    {
196
        $this->sentData = $sentData;
197
        $this->settings = $settings;
198
        $this->session = SessionManager::getSession();
199
        $this->request = SymfonyRequest::createFromGlobals();
200
        $this->lang = new Language($this->session->get('user-language') ?? 'english');
201
        $this->antiXss = new AntiXSS();
202
    }
203
204
    /**
205
     * Prepare and validate credentials from sent data
206
     */
207
    public function prepareCredentials(): ?array
208
    {
209
        $server = [
210
            'PHP_AUTH_USER' => $this->request->getUser(),
211
            'PHP_AUTH_PW' => $this->request->getPassword()
212
        ];
213
214
        // Decrypt data
215
        $dataReceived = $this->decryptSentData();
216
        if (!$dataReceived) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dataReceived of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

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