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