1: <?php
2: /**
3: * This code is licensed under AGPLv3 license or Afterlogic Software License
4: * if commercial version of the product was purchased.
5: * For full statements of the licenses see LICENSE-AFTERLOGIC and LICENSE-AGPL3 files.
6: */
7:
8: namespace Aurora\Modules\TwoFactorAuth;
9:
10: use Aurora\Modules\Core\Models\User;
11: use Aurora\Modules\TwoFactorAuth\Models\UsedDevice;
12: use Aurora\Modules\TwoFactorAuth\Models\WebAuthnKey;
13: use Aurora\System\Api;
14: use PragmaRX\Recovery\Recovery;
15: use lbuchs\WebAuthn;
16: use Aurora\Modules\Core\Module as CoreModule;
17:
18: /**
19: * @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
20: * @license https://afterlogic.com/products/common-licensing Afterlogic Software License
21: * @copyright Copyright (c) 2023, Afterlogic Corp.
22: *
23: * @property Settings $oModuleSettings
24: *
25: * @package Modules
26: */
27: class Module extends \Aurora\System\Module\AbstractModule
28: {
29: public static $VerifyState = false;
30:
31: private $oWebAuthn = null;
32:
33: /**
34: * @var Manager $oUsedDevicesManager
35: */
36: protected $oUsedDevicesManager = null;
37:
38: /**
39: * @return Module
40: */
41: public static function getInstance()
42: {
43: return parent::getInstance();
44: }
45:
46: /**
47: * @return Module
48: */
49: public static function Decorator()
50: {
51: return parent::Decorator();
52: }
53:
54: /**
55: * @return Settings
56: */
57: public function getModuleSettings()
58: {
59: return $this->oModuleSettings;
60: }
61:
62: public function init()
63: {
64: \Aurora\System\Router::getInstance()->registerArray(
65: self::GetName(),
66: [
67: 'assetlinks' => [$this, 'EntryAssetlinks'],
68: 'verify-security-key' => [$this, 'EntryVerifySecurityKey'],
69: ]
70: );
71:
72: $this->subscribeEvent('Core::Authenticate::after', array($this, 'onAfterAuthenticate'));
73: $this->subscribeEvent('Core::SetAuthDataAndGetAuthToken::after', array($this, 'onAfterSetAuthDataAndGetAuthToken'), 10);
74: $this->subscribeEvent('Core::Logout::before', array($this, 'onBeforeLogout'));
75: $this->subscribeEvent('Core::DeleteUser::after', array($this, 'onAfterDeleteUser'));
76: $this->subscribeEvent('System::RunEntry::before', array($this, 'onBeforeRunEntry'));
77:
78: $this->oWebAuthn = new WebAuthn\WebAuthn(
79: 'WebAuthn Library',
80: $this->oHttp->GetHost(),
81: [
82: 'android-key',
83: 'android-safetynet',
84: 'apple',
85: 'fido-u2f',
86: 'none',
87: 'packed',
88: 'tpm'
89: ],
90: false
91: // array_merge($this->oModuleSettings->FacetIds, [$this->oHttp->GetScheme().'://'.$this->oHttp->GetHost(true, false)])
92: );
93: }
94:
95: /**
96: *
97: * @return Manager
98: */
99: public function getUsedDevicesManager()
100: {
101: if ($this->oUsedDevicesManager === null) {
102: $this->oUsedDevicesManager = new Manager($this);
103: }
104:
105: return $this->oUsedDevicesManager;
106: }
107:
108: /**
109: * Obtains list of module settings for authenticated user.
110: *
111: * @return array
112: */
113: public function GetSettings()
114: {
115: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
116:
117: $bAllowUsedDevices = $this->oModuleSettings->AllowUsedDevices;
118: $aSettings = [
119: 'AllowBackupCodes' => $this->oModuleSettings->AllowBackupCodes,
120: 'AllowSecurityKeys' => $this->oModuleSettings->AllowSecurityKeys,
121: 'AllowAuthenticatorApp' => $this->oModuleSettings->AllowAuthenticatorApp,
122: 'AllowUsedDevices' => $bAllowUsedDevices,
123: 'TrustDevicesForDays' => $bAllowUsedDevices ? $this->oModuleSettings->TrustDevicesForDays : 0,
124: ];
125:
126: $oUser = Api::getAuthenticatedUser();
127: if ($oUser && $oUser->isNormalOrTenant()) {
128: $bShowRecommendationToConfigure = $this->oModuleSettings->ShowRecommendationToConfigure;
129: if ($bShowRecommendationToConfigure) {
130: $bShowRecommendationToConfigure = $oUser->getExtendedProp($this->GetName() . '::ShowRecommendationToConfigure');
131: }
132:
133: $bAuthenticatorAppEnabled = $this->oModuleSettings->AllowAuthenticatorApp && $oUser->getExtendedProp($this->GetName() . '::Secret') ? true : false;
134: $aWebAuthKeysInfo = $this->oModuleSettings->AllowSecurityKeys ? $this->_getWebAuthKeysInfo($oUser) : [];
135: $iBackupCodesCount = 0;
136: if ($bAuthenticatorAppEnabled || count($aWebAuthKeysInfo) > 0) {
137: $sBackupCodes = \Aurora\System\Utils::DecryptValue($oUser->getExtendedProp($this->GetName() . '::BackupCodes'));
138: $aBackupCodes = empty($sBackupCodes) ? [] : json_decode($sBackupCodes);
139: $aNotUsedBackupCodes = array_filter($aBackupCodes, function ($sCode) {
140: return !empty($sCode);
141: });
142: $iBackupCodesCount = count($aNotUsedBackupCodes);
143: }
144:
145: $aSettings = array_merge($aSettings, [
146: 'ShowRecommendationToConfigure' => $bShowRecommendationToConfigure,
147: 'WebAuthKeysInfo' => $aWebAuthKeysInfo,
148: 'AuthenticatorAppEnabled' => $bAuthenticatorAppEnabled,
149: 'BackupCodesCount' => $iBackupCodesCount,
150: ]);
151: }
152:
153: return $aSettings;
154: }
155:
156: public function UpdateSettings($ShowRecommendationToConfigure)
157: {
158: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
159:
160: if ($this->oModuleSettings->ShowRecommendationToConfigure) {
161: $oUser = Api::getAuthenticatedUser();
162: if ($oUser && $oUser->isNormalOrTenant()) {
163: $oUser->setExtendedProp($this->GetName() . '::ShowRecommendationToConfigure', $ShowRecommendationToConfigure);
164: return $oUser->save();
165: }
166: }
167: return false;
168: }
169:
170: /**
171: * Obtains user settings. Method is allowed for superadmin only.
172: *
173: * @param int $UserId
174: * @return array|null
175: */
176: public function GetUserSettings($UserId)
177: {
178: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::TenantAdmin);
179:
180: if ($this->oModuleSettings->AllowAuthenticatorApp) {
181: $oUser = Api::getUserById($UserId);
182: if ($oUser instanceof User && $oUser->isNormalOrTenant()) {
183: Api::checkUserAccess($oUser);
184: $iWebAuthnKeyCount = WebAuthnKey::where('UserId', $oUser->Id)->count();
185: return [
186: 'TwoFactorAuthEnabled' => !empty($oUser->getExtendedProp($this->GetName() . '::Secret')) || $iWebAuthnKeyCount > 0
187: ];
188: }
189: }
190:
191: return null;
192: }
193:
194: public function onAfterDeleteUser($aArgs, &$mResult)
195: {
196: if ($mResult) {
197: UsedDevice::where('UserId', $aArgs['UserId'])->delete();
198: }
199: }
200:
201: /**
202: * Disables two factor authentication for specified user. Method is allowed for superadmin only.
203: *
204: * @param int $UserId
205: * @return boolean
206: */
207: public function DisableUserTwoFactorAuth($UserId)
208: {
209: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::TenantAdmin);
210:
211: if (!$this->oModuleSettings->AllowAuthenticatorApp) {
212: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
213: }
214:
215: $oUser = Api::getUserById($UserId);
216: if ($oUser instanceof User && $oUser->isNormalOrTenant()) {
217: Api::checkUserAccess($oUser);
218:
219: $oUser->setExtendedProp($this->GetName() . '::Secret', '');
220: $oUser->setExtendedProp($this->GetName() . '::IsEncryptedSecret', false);
221:
222: $oUser->setExtendedProp($this->GetName() . '::Challenge', '');
223: $aWebAuthnKeys = WebAuthnKey::where('UserId', $oUser->Id)->get();
224:
225: $bResult = true;
226: foreach ($aWebAuthnKeys as $oWebAuthnKey) {
227: $bResult = $bResult && $oWebAuthnKey->delete();
228: }
229:
230: $oUser->setExtendedProp($this->GetName() . '::BackupCodes', '');
231: $oUser->setExtendedProp($this->GetName() . '::BackupCodesTimestamp', '');
232: $bResult = $bResult && \Aurora\Modules\Core\Module::Decorator()->UpdateUserObject($oUser);
233:
234: $bResult = $bResult && $this->getUsedDevicesManager()->revokeTrustFromAllDevices($oUser);
235:
236: return $bResult;
237: }
238:
239:
240: return false;
241: }
242:
243: /**
244: * Verifies user's password and returns Secret and QR-code
245: *
246: * @param string $Password
247: * @return bool|array
248: */
249: public function RegisterAuthenticatorAppBegin($Password)
250: {
251: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
252:
253: if (!$this->oModuleSettings->AllowAuthenticatorApp) {
254: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
255: }
256:
257: $oUser = Api::getAuthenticatedUser();
258: if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
259: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
260: }
261:
262: if (empty($Password)) {
263: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
264: }
265:
266: if (!CoreModule::Decorator()->VerifyPassword($Password)) {
267: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
268: }
269:
270: $oGoogle = new \PHPGangsta_GoogleAuthenticator();
271: $sSecret = '';
272: if ($oUser->getExtendedProp($this->GetName() . '::Secret')) {
273: $sSecret = $oUser->getExtendedProp($this->GetName() . '::Secret');
274: if ($oUser->getExtendedProp($this->GetName() . '::IsEncryptedSecret')) {
275: $sSecret = \Aurora\System\Utils::DecryptValue($sSecret);
276: }
277: } else {
278: $sSecret = $oGoogle->createSecret();
279: }
280: $sServerName = !empty($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : $_SERVER['HTTP_HOST'];
281: if (!empty($sServerName)) {
282: $sServerName = "(" . $sServerName . ")";
283: }
284: $sQRCodeName = $oUser->PublicId . $sServerName;
285:
286: return [
287: 'Secret' => $sSecret,
288: 'QRCodeName' => $sQRCodeName,
289: 'Enabled' => $oUser->getExtendedProp($this->GetName() . '::Secret') ? true : false
290: ];
291: }
292:
293: /**
294: * Verifies user's Code and saves Secret in case of success
295: *
296: * @param string $Password
297: * @param string $Code
298: * @param string $Secret
299: * @return boolean
300: * @throws \Aurora\System\Exceptions\ApiException
301: */
302: public function RegisterAuthenticatorAppFinish($Password, $Code, $Secret)
303: {
304: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
305:
306: if (!$this->oModuleSettings->AllowAuthenticatorApp) {
307: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
308: }
309:
310: $oUser = Api::getAuthenticatedUser();
311: if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
312: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
313: }
314:
315: if (empty($Password) || empty($Code) || empty($Secret)) {
316: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
317: }
318:
319: if (!CoreModule::Decorator()->VerifyPassword($Password)) {
320: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
321: }
322:
323: $bResult = false;
324: $iClockTolerance = $this->oModuleSettings->ClockTolerance;
325: $oGoogle = new \PHPGangsta_GoogleAuthenticator();
326:
327: $oStatus = $oGoogle->verifyCode($Secret, $Code, $iClockTolerance);
328: if ($oStatus === true) {
329: $oUser->setExtendedProp($this->GetName() . '::Secret', \Aurora\System\Utils::EncryptValue($Secret));
330: $oUser->setExtendedProp($this->GetName() . '::IsEncryptedSecret', true);
331: \Aurora\Modules\Core\Module::Decorator()->UpdateUserObject($oUser);
332: $bResult = true;
333: }
334:
335: return $bResult;
336: }
337:
338: /**
339: * Verifies user's Password and disables TwoFactorAuth in case of success
340: *
341: * @param string $Password
342: * @return bool
343: */
344: public function DisableAuthenticatorApp($Password)
345: {
346: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
347:
348: if (!$this->oModuleSettings->AllowAuthenticatorApp) {
349: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
350: }
351:
352: $oUser = Api::getAuthenticatedUser();
353: if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
354: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
355: }
356:
357: if (empty($Password)) {
358: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
359: }
360:
361: if (!CoreModule::Decorator()->VerifyPassword($Password)) {
362: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
363: }
364:
365: $oUser->setExtendedProp($this->GetName() . '::Secret', "");
366: $oUser->setExtendedProp($this->GetName() . '::IsEncryptedSecret', false);
367: $bResult = \Aurora\Modules\Core\Module::Decorator()->UpdateUserObject($oUser);
368: $this->_removeAllDataWhenAllSecondFactorsDisabled($oUser);
369:
370: return $bResult;
371: }
372:
373: /**
374: * Verifies Authenticator code and returns AuthToken in case of success
375: *
376: * @param string $Code
377: * @param string $Login
378: * @param string $Password
379: * @return bool|array
380: * @throws \Aurora\System\Exceptions\ApiException
381: * @throws \Aurora\System\Exceptions\BaseException
382: */
383: public function VerifyAuthenticatorAppCode($Code, $Login, $Password)
384: {
385: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
386:
387: if (!$this->oModuleSettings->AllowAuthenticatorApp) {
388: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
389: }
390:
391: if (empty($Code) || empty($Login) || empty($Password)) {
392: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
393: }
394:
395: self::$VerifyState = true;
396: $mAuthenticateResult = \Aurora\Modules\Core\Module::Decorator()->Authenticate($Login, $Password);
397: self::$VerifyState = false;
398: if (!$mAuthenticateResult || !is_array($mAuthenticateResult) || !isset($mAuthenticateResult['token'])) {
399: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AuthError);
400: }
401:
402: $oUser = Api::getUserById((int) $mAuthenticateResult['id']);
403: if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
404: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
405: }
406:
407: $mResult = false;
408: if ($oUser->getExtendedProp($this->GetName() . '::Secret')) {
409: $sSecret = $oUser->getExtendedProp($this->GetName() . '::Secret');
410: if ($oUser->getExtendedProp($this->GetName() . '::IsEncryptedSecret')) {
411: $sSecret = \Aurora\System\Utils::DecryptValue($sSecret);
412: }
413: $oGoogle = new \PHPGangsta_GoogleAuthenticator();
414: $iClockTolerance = $this->oModuleSettings->ClockTolerance;
415: $oStatus = $oGoogle->verifyCode($sSecret, $Code, $iClockTolerance);
416: if ($oStatus) {
417: $mResult = \Aurora\Modules\Core\Module::Decorator()->SetAuthDataAndGetAuthToken($mAuthenticateResult);
418:
419: }
420: } else {
421: throw new \Aurora\System\Exceptions\BaseException(Enums\ErrorCodes::SecretNotSet);
422: }
423:
424: return $mResult;
425: }
426:
427: /**
428: * Verifies user's password and returns backup codes generated earlier.
429: *
430: * @param string $Password
431: * @return array|boolean
432: */
433: public function GetBackupCodes($Password)
434: {
435: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
436:
437: if (!$this->oModuleSettings->AllowBackupCodes) {
438: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
439: }
440:
441: $oUser = Api::getAuthenticatedUser();
442: if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
443: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
444: }
445:
446: if (empty($Password)) {
447: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
448: }
449:
450: if (!CoreModule::Decorator()->VerifyPassword($Password)) {
451: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
452: }
453:
454: $sBackupCodes = \Aurora\System\Utils::DecryptValue($oUser->getExtendedProp($this->GetName() . '::BackupCodes'));
455: return [
456: 'Datetime' => $oUser->getExtendedProp($this->GetName() . '::BackupCodesTimestamp'),
457: 'Codes' => empty($sBackupCodes) ? [] : json_decode($sBackupCodes)
458: ];
459: }
460:
461: /**
462: * Verifies user's password, generates backup codes and returns them.
463: *
464: * @param string $Password
465: * @return array|boolean
466: */
467: public function GenerateBackupCodes($Password)
468: {
469: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
470:
471: if (!$this->oModuleSettings->AllowBackupCodes) {
472: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
473: }
474:
475: $oUser = Api::getAuthenticatedUser();
476: if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
477: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
478: }
479:
480: if (empty($Password)) {
481: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
482: }
483:
484: if (!CoreModule::Decorator()->VerifyPassword($Password)) {
485: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
486: }
487:
488: $oRecovery = new Recovery();
489: $aCodes = $oRecovery
490: ->setCount(10) // Generate 10 codes
491: ->setBlocks(2) // Every code must have 2 blocks
492: ->setChars(4) // Each block must have 4 chars
493: ->setBlockSeparator(' ')
494: ->uppercase()
495: ->toArray();
496:
497: $oUser->setExtendedProp($this->GetName() . '::BackupCodes', \Aurora\System\Utils::EncryptValue(json_encode($aCodes)));
498: $oUser->setExtendedProp($this->GetName() . '::BackupCodesTimestamp', time());
499: \Aurora\Modules\Core\Module::Decorator()->UpdateUserObject($oUser);
500:
501: return [
502: 'Datetime' => $oUser->getExtendedProp($this->GetName() . '::BackupCodesTimestamp'),
503: 'Codes' => $aCodes,
504: ];
505: }
506:
507: public function VerifyBackupCode($BackupCode, $Login, $Password)
508: {
509: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
510:
511: if (!$this->oModuleSettings->AllowBackupCodes) {
512: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
513: }
514:
515: if (empty($BackupCode) || empty($Login) || empty($Password)) {
516: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
517: }
518:
519: self::$VerifyState = true;
520: $mAuthenticateResult = \Aurora\Modules\Core\Module::Decorator()->Authenticate($Login, $Password);
521: self::$VerifyState = false;
522: if (!$mAuthenticateResult || !is_array($mAuthenticateResult) || !isset($mAuthenticateResult['token'])) {
523: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AuthError);
524: }
525:
526: $oUser = Api::getUserById((int) $mAuthenticateResult['id']);
527: if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
528: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
529: }
530:
531: $mResult = false;
532: $sBackupCodes = \Aurora\System\Utils::DecryptValue($oUser->getExtendedProp($this->GetName() . '::BackupCodes'));
533: $aBackupCodes = empty($sBackupCodes) ? [] : json_decode($sBackupCodes);
534: $sTrimmed = preg_replace('/\s+/', '', $BackupCode);
535: $sPrepared = substr_replace($sTrimmed, ' ', 4, 0);
536: $index = array_search($sPrepared, $aBackupCodes);
537: if ($index !== false) {
538: $aBackupCodes[$index] = '';
539: $oUser->setExtendedProp($this->GetName() . '::BackupCodes', \Aurora\System\Utils::EncryptValue(json_encode($aBackupCodes)));
540: \Aurora\Modules\Core\Module::Decorator()->UpdateUserObject($oUser);
541: $mResult = \Aurora\Modules\Core\Module::Decorator()->SetAuthDataAndGetAuthToken($mAuthenticateResult);
542:
543: }
544: return $mResult;
545: }
546:
547: /**
548: * Checks if User has TwoFactorAuth enabled and return UserId instead of AuthToken
549: *
550: * @param array $aArgs
551: * @param array $mResult
552: */
553: public function onAfterAuthenticate($aArgs, &$mResult)
554: {
555: if (!self::$VerifyState && $mResult && is_array($mResult) && isset($mResult['token'])) {
556: $oUser = Api::getUserById((int) $mResult['id']);
557: if ($oUser instanceof User) {
558: $bHasSecurityKey = false;
559: if ($this->oModuleSettings->AllowSecurityKeys) {
560: $iWebAuthnKeyCount = WebAuthnKey::where('UserId', $oUser->Id)->count();
561: $bHasSecurityKey = $iWebAuthnKeyCount > 0;
562: }
563:
564: $bHasAuthenticatorApp = false;
565: if ($this->oModuleSettings->AllowAuthenticatorApp) {
566: $bHasAuthenticatorApp = !!(!empty($oUser->getExtendedProp($this->GetName() . '::Secret')));
567: }
568:
569: $bDeviceTrusted = ($bHasAuthenticatorApp || $bHasAuthenticatorApp) ? $this->getUsedDevicesManager()->checkDeviceAfterAuthenticate($oUser) : false;
570:
571: if (($bHasSecurityKey || $bHasAuthenticatorApp) && !$bDeviceTrusted) {
572: $mResult = [
573: 'TwoFactorAuth' => [
574: 'HasAuthenticatorApp' => $bHasAuthenticatorApp,
575: 'HasSecurityKey' => $bHasSecurityKey,
576: 'HasBackupCodes' => $this->oModuleSettings->AllowBackupCodes && !empty($oUser->getExtendedProp($this->GetName() . '::BackupCodes'))
577: ]
578: ];
579: }
580: }
581: }
582: }
583:
584: /**
585: * Verifies user's password and returns arguments for security key registration.
586: *
587: * @param string $Password
588: * @return array|boolean
589: */
590: public function RegisterSecurityKeyBegin($Password)
591: {
592: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
593:
594: if (!$this->oModuleSettings->AllowSecurityKeys) {
595: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
596: }
597:
598: $oUser = Api::getAuthenticatedUser();
599: if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
600: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
601: }
602:
603: if (empty($Password)) {
604: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
605: }
606:
607: if (!CoreModule::Decorator()->VerifyPassword($Password)) {
608: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
609: }
610:
611: $oCreateArgs = $this->oWebAuthn->getCreateArgs(
612: \base64_encode($oUser->UUID),
613: $oUser->PublicId,
614: $oUser->PublicId,
615: 90,
616: false,
617: 'discouraged',
618: true,
619: []
620: );
621:
622: $oCreateArgs->publicKey->user->id = \base64_encode($oCreateArgs->publicKey->user->id->getBinaryString());
623: $oCreateArgs->publicKey->challenge = \base64_encode($oCreateArgs->publicKey->challenge->getBinaryString());
624: $oUser->setExtendedProp($this->GetName() . '::Challenge', $oCreateArgs->publicKey->challenge);
625: $oUser->save();
626:
627: return $oCreateArgs;
628: }
629:
630: /**
631: * Verifies user's password and finishes security key registration.
632: *
633: * @param array $Attestation
634: * @param string $Password
635: * @return boolean
636: * @throws \Aurora\System\Exceptions\ApiException
637: */
638: public function RegisterSecurityKeyFinish($Attestation, $Password)
639: {
640: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
641:
642: if (!$this->oModuleSettings->AllowSecurityKeys) {
643: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
644: }
645:
646: $oUser = Api::getAuthenticatedUser();
647: if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
648: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
649: }
650:
651: if (empty($Password) || empty($Attestation)) {
652: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
653: }
654:
655: if (!CoreModule::Decorator()->VerifyPassword($Password)) {
656: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
657: }
658:
659: $data = $this->oWebAuthn->processCreate(
660: \base64_decode($Attestation['clientDataJSON']),
661: \base64_decode($Attestation['attestationObject']),
662: \base64_decode($oUser->getExtendedProp($this->GetName() . '::Challenge')),
663: false
664: );
665: $data->credentialId = \base64_encode($data->credentialId);
666: $data->AAGUID = \base64_encode($data->AAGUID);
667:
668: $sEncodedSecurityKeyData = \json_encode($data);
669: if ($sEncodedSecurityKeyData === false) {
670: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::UnknownError, null, json_last_error_msg());
671: } else {
672: $oWebAuthnKey = new WebAuthnKey();
673: $oWebAuthnKey->UserId = $oUser->Id;
674: $oWebAuthnKey->KeyData = $sEncodedSecurityKeyData;
675: $oWebAuthnKey->CreationDateTime = time();
676:
677: if ($oWebAuthnKey->save()) {
678: return $oWebAuthnKey->Id;
679: }
680: }
681:
682: return false;
683: }
684:
685: /**
686: * Authenticates user and returns arguments for security key verification.
687: *
688: * @param string $Login
689: * @param string $Password
690: * @return array|boolean
691: */
692: public function VerifySecurityKeyBegin($Login, $Password)
693: {
694: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
695:
696: if (!$this->oModuleSettings->AllowSecurityKeys) {
697: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
698: }
699:
700: self::$VerifyState = true;
701: $mAuthenticateResult = \Aurora\Modules\Core\Module::Decorator()->Authenticate($Login, $Password);
702: self::$VerifyState = false;
703: if (!$mAuthenticateResult || !is_array($mAuthenticateResult) || !isset($mAuthenticateResult['token'])) {
704: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AuthError);
705: }
706:
707: $oUser = Api::getUserById((int) $mAuthenticateResult['id']);
708: if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
709: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
710: }
711:
712: $mGetArgs = false;
713: $aIds = [];
714: $aWebAuthnKeys = WebAuthnKey::where('UserId', $oUser->Id)->get();
715:
716: foreach ($aWebAuthnKeys as $oWebAuthnKey) {
717: /** @var WebAuthnKey $oWebAuthnKey */
718: $oKeyData = \json_decode($oWebAuthnKey->KeyData);
719: $aIds[] = \base64_decode($oKeyData->credentialId);
720: }
721:
722: if (count($aIds) > 0) {
723: $mGetArgs = $this->oWebAuthn->getGetArgs(
724: $aIds,
725: 90
726: );
727: $mGetArgs->publicKey->challenge = \base64_encode($mGetArgs->publicKey->challenge->getBinaryString());
728: if (is_array($mGetArgs->publicKey->allowCredentials)) {
729: foreach ($mGetArgs->publicKey->allowCredentials as $key => $val) {
730: $val->id = \base64_encode($val->id->getBinaryString());
731: $mGetArgs->publicKey->allowCredentials[$key] = $val;
732: }
733: }
734:
735: $oUser->setExtendedProp($this->GetName() . '::Challenge', $mGetArgs->publicKey->challenge);
736: $oUser->save();
737: }
738:
739: return $mGetArgs;
740: }
741:
742: /**
743: * Authenticates user and finishes security key verification.
744: *
745: * @param string $Login
746: * @param string $Password
747: * @param array $Attestation
748: * @return boolean
749: * @throws \Aurora\System\Exceptions\ApiException
750: */
751: public function VerifySecurityKeyFinish($Login, $Password, $Attestation)
752: {
753: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
754:
755: if (!$this->oModuleSettings->AllowSecurityKeys) {
756: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
757: }
758:
759: self::$VerifyState = true;
760: $mAuthenticateResult = \Aurora\Modules\Core\Module::Decorator()->Authenticate($Login, $Password);
761: self::$VerifyState = false;
762: if (!$mAuthenticateResult || !is_array($mAuthenticateResult) || !isset($mAuthenticateResult['token'])) {
763: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AuthError);
764: }
765:
766: $oUser = Api::getUserById((int) $mAuthenticateResult['id']);
767: if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
768: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
769: }
770:
771: $mResult = true;
772: $clientDataJSON = base64_decode($Attestation['clientDataJSON']);
773: $authenticatorData = base64_decode($Attestation['authenticatorData']);
774: $signature = base64_decode($Attestation['signature']);
775: $id = base64_decode($Attestation['id']);
776: $credentialPublicKey = null;
777:
778: $challenge = \base64_decode($oUser->getExtendedProp($this->GetName() . '::Challenge'));
779:
780: $aWebAuthnKeys = WebAuthnKey::where('UserId', $oUser->Id)->get();
781:
782: $oWebAuthnKey = null;
783: foreach ($aWebAuthnKeys as $oWebAuthnKey) {
784: /** @var WebAuthnKey $oWebAuthnKey */
785: $oKeyData = \json_decode($oWebAuthnKey->KeyData);
786: if (\base64_decode($oKeyData->credentialId) === $id) {
787: $credentialPublicKey = $oKeyData->credentialPublicKey;
788: break;
789: }
790: }
791:
792: if ($credentialPublicKey !== null) {
793: try {
794: // process the get request. throws WebAuthnException if it fails
795: $this->oWebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, null, false);
796: $mResult = \Aurora\Modules\Core\Module::Decorator()->SetAuthDataAndGetAuthToken($mAuthenticateResult);
797:
798:
799: if (isset($oWebAuthnKey)) {
800: $oWebAuthnKey->LastUsageDateTime = time();
801: $oWebAuthnKey->save();
802: }
803: } catch (\Exception $oEx) {
804: $mResult = false;
805: throw new \Aurora\System\Exceptions\ApiException(999, $oEx, $oEx->getMessage());
806: }
807: }
808:
809: return $mResult;
810: }
811:
812: /**
813: * Verifies user's password and changes security key name.
814: *
815: * @param int $KeyId
816: * @param string $NewName
817: * @param string $Password
818: * @return boolean
819: */
820: public function UpdateSecurityKeyName($KeyId, $NewName, $Password)
821: {
822: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
823:
824: if (!$this->oModuleSettings->AllowSecurityKeys) {
825: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
826: }
827:
828: $oUser = Api::getAuthenticatedUser();
829: if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
830: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
831: }
832:
833: if (empty($Password) || empty($KeyId) || empty($NewName)) {
834: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
835: }
836:
837: if (!CoreModule::Decorator()->VerifyPassword($Password)) {
838: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
839: }
840:
841: $mResult = false;
842: $oWebAuthnKey = WebAuthnKey::where('UserId', $oUser->Id)
843: ->where('Id', $KeyId)
844: ->first();
845:
846: if ($oWebAuthnKey instanceof WebAuthnKey) {
847: $oWebAuthnKey->Name = $NewName;
848: $mResult = $oWebAuthnKey->save();
849: }
850: return $mResult;
851: }
852:
853: /**
854: * Verifies user's password and removes secutiry key.
855: *
856: * @param int $KeyId
857: * @param string $Password
858: * @return boolean
859: */
860: public function DeleteSecurityKey($KeyId, $Password)
861: {
862: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
863:
864: if (!$this->oModuleSettings->AllowSecurityKeys) {
865: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
866: }
867:
868: $oUser = Api::getAuthenticatedUser();
869: if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
870: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
871: }
872:
873: if (empty($Password) || empty($KeyId)) {
874: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
875: }
876:
877: if (!CoreModule::Decorator()->VerifyPassword($Password)) {
878: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
879: }
880:
881: $mResult = false;
882: $oWebAuthnKey = WebAuthnKey::where('UserId', $oUser->Id)
883: ->where('Id', $KeyId)
884: ->first();
885: if ($oWebAuthnKey instanceof WebAuthnKey) {
886: $mResult = $oWebAuthnKey->delete();
887: $this->_removeAllDataWhenAllSecondFactorsDisabled($oUser);
888: }
889: return $mResult;
890: }
891:
892: /**
893: * Verifies user's password.
894: *
895: * @param string $Password
896: * @return boolean
897: */
898: public function VerifyPassword($Password)
899: {
900: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
901:
902: if (empty($Password)) {
903: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
904: }
905:
906: return CoreModule::Decorator()->VerifyPassword($Password);
907: }
908:
909: public function EntryVerifySecurityKey()
910: {
911: $oModuleManager = Api::GetModuleManager();
912: $sTheme = $oModuleManager->getModuleConfigValue('CoreWebclient', 'Theme');
913:
914: $oHttp = \MailSo\Base\Http::SingletonInstance();
915: $sLogin = $oHttp->GetQuery('login', '');
916: $sPassword = $oHttp->GetQuery('password', '');
917: $sPackageName = $oHttp->GetQuery('package_name', '');
918: if (empty($sLogin) || empty($sPassword)) {
919: return '';
920: }
921:
922: $oGetArgs = false;
923: $sError = false;
924: try {
925: $oGetArgs = self::Decorator()->VerifySecurityKeyBegin($sLogin, $sPassword);
926: } catch (\Exception $oEx) {
927: $sError = $oEx->getCode() . ': ' . $oEx->getMessage();
928: }
929: $sResult = \file_get_contents($this->GetPath() . '/templates/EntryVerifySecurityKey.html');
930: $sResult = \strtr($sResult, array(
931: '{{GetArgs}}' => \Aurora\System\Managers\Response::GetJsonFromObject(null, $oGetArgs),
932: '{{PackageName}}' => $sPackageName,
933: '{{Error}}' => $sError,
934: '{{Description}}' => $this->i18N('HINT_INSERT_TOUCH_SECURITY_KEY'),
935: '{{Theme}}' => $sTheme,
936: ));
937: \Aurora\Modules\CoreWebclient\Module::Decorator()->SetHtmlOutputHeaders();
938: @header('Cache-Control: no-cache', true);
939: return $sResult;
940: }
941:
942: public function EntryAssetlinks()
943: {
944: @header('Content-Type: application/json; charset=utf-8');
945: @header('Cache-Control: no-cache', true);
946:
947: $sPath = __DIR__ . '/assets/assetlinks.json';
948: $sDistPath = __DIR__ . '/assets/assetlinks.dist.json';
949:
950: if (file_exists($sPath)) {
951: $sFileContent = file_get_contents($sPath);
952: } elseif (file_exists($sDistPath)) {
953: $sFileContent = file_get_contents($sDistPath);
954: } else {
955: $sFileContent = "[]";
956: }
957:
958: echo $sFileContent;
959: }
960:
961: public function TrustDevice($DeviceId, $DeviceName)
962: {
963: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
964:
965: if (!$this->oModuleSettings->AllowUsedDevices) {
966: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
967: }
968:
969: $authToken = Api::getAuthToken();
970:
971: $oUser = Api::getAuthenticatedUser($authToken);
972: if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
973: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
974: }
975:
976: return $this->getUsedDevicesManager()->trustDevice($oUser->Id, $DeviceId, $DeviceName, $authToken);
977: }
978:
979: /**
980: * @deprecated since version 9.7.2. Use SetDeviceName instead.
981: */
982: public function SaveDevice($DeviceId, $DeviceName)
983: {
984: return $this->Decorator()->SetDeviceName($DeviceId, $DeviceName);
985: }
986:
987: /**
988: * @param string $DeviceId
989: * @param string $DeviceName
990: *
991: * @return boolean
992: */
993: public function SetDeviceName($DeviceId, $DeviceName)
994: {
995: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
996:
997: if (!is_string($DeviceId) && count($DeviceId) < 4 && empty($DeviceName)) {
998: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
999: }
1000:
1001: $mResult = false;
1002:
1003: if ($this->oModuleSettings->AllowUsedDevices) {
1004: $oUser = Api::getAuthenticatedUser();
1005: $mResult = $this->getUsedDevicesManager()->setDeviceName($oUser->Id, $DeviceId, $DeviceName);
1006: }
1007:
1008: return $mResult;
1009: }
1010:
1011: /**
1012: * @param string $DeviceId
1013: * @param string $DeviceCustomName
1014: *
1015: * @return boolean
1016: */
1017: public function SetDeviceCustomName($DeviceId, $DeviceCustomName)
1018: {
1019: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
1020:
1021: if (!is_string($DeviceId) && count($DeviceId) < 4 && empty($DeviceCustomName)) {
1022: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
1023: }
1024:
1025: $mResult = false;
1026:
1027: if ($this->oModuleSettings->AllowUsedDevices) {
1028: $oUser = Api::getAuthenticatedUser();
1029: $mResult = $this->getUsedDevicesManager()->setDeviceCustomName($oUser->Id, $DeviceId, $DeviceCustomName);
1030: }
1031:
1032: return $mResult;
1033: }
1034:
1035: public function GetUsedDevices()
1036: {
1037: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
1038:
1039: if (!$this->oModuleSettings->AllowUsedDevices) {
1040: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
1041: }
1042:
1043: $oUser = Api::getAuthenticatedUser();
1044: if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
1045: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
1046: }
1047:
1048: return $this->getUsedDevicesManager()->getAllDevices($oUser->Id)->all();
1049: }
1050:
1051: public function RevokeTrustFromAllDevices()
1052: {
1053: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
1054:
1055: if (!$this->oModuleSettings->AllowUsedDevices) {
1056: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
1057: }
1058:
1059: if (!$this->getUsedDevicesManager()->isTrustedDevicesEnabled()) {
1060: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
1061: }
1062:
1063: $oUser = Api::getAuthenticatedUser();
1064: if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
1065: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
1066: }
1067:
1068: return $this->getUsedDevicesManager()->revokeTrustFromAllDevices($oUser);
1069: }
1070:
1071: public function onBeforeLogout($aArgs, &$mResult)
1072: {
1073: $oUser = Api::getAuthenticatedUser();
1074: if ($oUser instanceof User && $oUser->isNormalOrTenant()) {
1075: $oUsedDevice = $this->getUsedDevicesManager()->getDeviceByAuthToken($oUser->Id, Api::getAuthToken());
1076: if ($oUsedDevice) {
1077: $oUsedDevice->AuthToken = '';
1078: $oUsedDevice->save();
1079: }
1080: }
1081: }
1082:
1083: public function LogoutFromDevice($DeviceId)
1084: {
1085: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
1086:
1087: if (!$this->oModuleSettings->AllowUsedDevices) {
1088: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
1089: }
1090:
1091: $oUser = Api::getAuthenticatedUser();
1092: if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
1093: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
1094: }
1095:
1096: if (empty($DeviceId)) {
1097: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
1098: }
1099:
1100: $oUsedDevice = $this->getUsedDevicesManager()->getDevice($oUser->Id, $DeviceId);
1101: if ($oUsedDevice && !empty($oUsedDevice->AuthToken)) {
1102: Api::UserSession()->Delete($oUsedDevice->AuthToken);
1103: $oUsedDevice->AuthToken = '';
1104: $oUsedDevice->TrustTillDateTime = $oUsedDevice->CreationDateTime; // revoke trust
1105: $oUsedDevice->save();
1106: }
1107: return true;
1108: }
1109:
1110: public function RemoveDevice($DeviceId)
1111: {
1112: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
1113: $oUser = Api::getAuthenticatedUser();
1114: if (!$this->oModuleSettings->AllowUsedDevices && !$oUser->isAdmin()) {
1115: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
1116: }
1117:
1118: if (!($oUser instanceof User) || !$oUser->isNormalOrTenant() && !$oUser->isAdmin()) {
1119: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
1120: }
1121:
1122: if (empty($DeviceId)) {
1123: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
1124: }
1125:
1126: $oUsedDevice = $oUser->isAdmin() ? $this->getUsedDevicesManager()->getDeviceByDeviceId($DeviceId) : $this->getUsedDevicesManager()->getDevice($oUser->Id, $DeviceId);
1127: if ($oUsedDevice) {
1128: Api::UserSession()->Delete($oUsedDevice->AuthToken);
1129: $oUsedDevice->delete();
1130: }
1131: return true;
1132: }
1133:
1134: protected function _getWebAuthKeysInfo($oUser)
1135: {
1136: $aWebAuthKeysInfo = [];
1137:
1138: if ($oUser instanceof User && $oUser->isNormalOrTenant()) {
1139: $aWebAuthnKeys = WebAuthnKey::where('UserId', $oUser->Id)->get();
1140: foreach ($aWebAuthnKeys as $oWebAuthnKey) {
1141: /** @var WebAuthnKey $oWebAuthnKey */
1142: $aWebAuthKeysInfo[] = [
1143: $oWebAuthnKey->Id,
1144: $oWebAuthnKey->Name
1145: ];
1146: }
1147: }
1148:
1149: return $aWebAuthKeysInfo;
1150: }
1151:
1152: protected function _removeAllDataWhenAllSecondFactorsDisabled($oUser)
1153: {
1154: $iWebAuthnKeyCount = WebAuthnKey::where('UserId', $oUser->Id)->count();
1155: if (empty($oUser->getExtendedProp($this->GetName() . '::Secret')) && $iWebAuthnKeyCount === 0) {
1156: $oUser->setExtendedProp($this->GetName() . '::BackupCodes', '');
1157: $oUser->setExtendedProp($this->GetName() . '::BackupCodesTimestamp', '');
1158: \Aurora\Modules\Core\Module::Decorator()->UpdateUserObject($oUser);
1159:
1160: $this->getUsedDevicesManager()->revokeTrustFromAllDevices($oUser);
1161: }
1162: }
1163:
1164: public function onBeforeRunEntry(&$aArgs, &$mResult)
1165: {
1166: $error = false;
1167: if ($aArgs['EntryName'] === 'api' && $this->oModuleSettings->AllowUsedDevices) {
1168: $user = \Aurora\System\Api::getAuthenticatedUser();
1169: $authToken = \Aurora\System\Api::getAuthenticatedUserAuthToken();
1170:
1171: if ($user && $user->isNormalOrTenant()) {
1172: $deviceId = Api::getDeviceIdFromHeaders();
1173:
1174: if ($deviceId) {
1175: $usedDevice = $this->getUsedDevicesManager()->getDevice($user->Id, $deviceId);
1176:
1177: if (!$usedDevice) {
1178: $error = true;
1179: } elseif ($usedDevice->AuthToken !== $authToken) {
1180: $error = true;
1181: }
1182: } else {
1183: $error = true;
1184: }
1185: }
1186: }
1187: if ($error) {
1188: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AuthError);
1189: }
1190: }
1191:
1192: public function onAfterSetAuthDataAndGetAuthToken(&$aArgs, &$mResult)
1193: {
1194: if (is_array($mResult) && isset($mResult[\Aurora\System\Application::AUTH_TOKEN_KEY]) && $this->oModuleSettings->AllowUsedDevices) {
1195: $deviceId = Api::getDeviceIdFromHeaders();
1196: if ($deviceId && is_string($deviceId)) {
1197: $sFallbackName = $_SERVER['HTTP_USER_AGENT'] ?? $_SERVER['REMOTE_ADDR'];
1198: $sFallbackName = substr((string)explode(' ', $sFallbackName)[0], 0, 255);
1199: $this->getUsedDevicesManager()->saveDevice(Api::getAuthenticatedUserId(), $deviceId, $sFallbackName, $mResult[\Aurora\System\Application::AUTH_TOKEN_KEY]);
1200: } else {
1201: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AuthError);
1202: }
1203: }
1204: }
1205: }
1206: