1: | <?php |
2: | |
3: | |
4: | |
5: | |
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: | |
20: | |
21: | |
22: | |
23: | |
24: | |
25: | |
26: | |
27: | class Module extends \Aurora\System\Module\AbstractModule |
28: | { |
29: | public static $VerifyState = false; |
30: | |
31: | private $oWebAuthn = null; |
32: | |
33: | |
34: | |
35: | |
36: | protected $oUsedDevicesManager = null; |
37: | |
38: | |
39: | |
40: | |
41: | public static function getInstance() |
42: | { |
43: | return parent::getInstance(); |
44: | } |
45: | |
46: | |
47: | |
48: | |
49: | public static function Decorator() |
50: | { |
51: | return parent::Decorator(); |
52: | } |
53: | |
54: | |
55: | |
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: | |
92: | ); |
93: | } |
94: | |
95: | |
96: | |
97: | |
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: | |
110: | |
111: | |
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: | |
172: | |
173: | |
174: | |
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: | |
203: | |
204: | |
205: | |
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: | |
245: | |
246: | |
247: | |
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: | |
295: | |
296: | |
297: | |
298: | |
299: | |
300: | |
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: | |
340: | |
341: | |
342: | |
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: | |
375: | |
376: | |
377: | |
378: | |
379: | |
380: | |
381: | |
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: | |
429: | |
430: | |
431: | |
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: | |
463: | |
464: | |
465: | |
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) |
491: | ->setBlocks(2) |
492: | ->setChars(4) |
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: | |
549: | |
550: | |
551: | |
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: | |
586: | |
587: | |
588: | |
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: | |
632: | |
633: | |
634: | |
635: | |
636: | |
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: | |
687: | |
688: | |
689: | |
690: | |
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: | |
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: | |
744: | |
745: | |
746: | |
747: | |
748: | |
749: | |
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: | |
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: | |
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: | |
814: | |
815: | |
816: | |
817: | |
818: | |
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: | |
855: | |
856: | |
857: | |
858: | |
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: | |
894: | |
895: | |
896: | |
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: | |
981: | |
982: | public function SaveDevice($DeviceId, $DeviceName) |
983: | { |
984: | return $this->Decorator()->SetDeviceName($DeviceId, $DeviceName); |
985: | } |
986: | |
987: | |
988: | |
989: | |
990: | |
991: | |
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: | |
1013: | |
1014: | |
1015: | |
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; |
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: | |
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: | |