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