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