/home/ivoiecob/email.hirewise-va.com/modules/TwoFactorAuth/Module.php
<?php
/**
* This code is licensed under AGPLv3 license or Afterlogic Software License
* if commercial version of the product was purchased.
* For full statements of the licenses see LICENSE-AFTERLOGIC and LICENSE-AGPL3 files.
*/
namespace Aurora\Modules\TwoFactorAuth;
use Aurora\Modules\Core\Models\User;
use Aurora\Modules\TwoFactorAuth\Models\UsedDevice;
use Aurora\Modules\TwoFactorAuth\Models\WebAuthnKey;
use Aurora\System\Api;
use PragmaRX\Recovery\Recovery;
use lbuchs\WebAuthn;
use Aurora\Modules\Core\Module as CoreModule;
/**
* @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
* @license https://afterlogic.com/products/common-licensing Afterlogic Software License
* @copyright Copyright (c) 2023, Afterlogic Corp.
*
* @property Settings $oModuleSettings
*
* @package Modules
*/
class Module extends \Aurora\System\Module\AbstractModule
{
public static $VerifyState = false;
private $oWebAuthn = null;
/**
* @var Manager $oUsedDevicesManager
*/
protected $oUsedDevicesManager = null;
/**
* @return Module
*/
public static function getInstance()
{
return parent::getInstance();
}
/**
* @return Module
*/
public static function Decorator()
{
return parent::Decorator();
}
/**
* @return Settings
*/
public function getModuleSettings()
{
return $this->oModuleSettings;
}
public function init()
{
\Aurora\System\Router::getInstance()->registerArray(
self::GetName(),
[
'assetlinks' => [$this, 'EntryAssetlinks'],
'verify-security-key' => [$this, 'EntryVerifySecurityKey'],
]
);
$this->subscribeEvent('Core::Authenticate::after', array($this, 'onAfterAuthenticate'));
$this->subscribeEvent('Core::SetAuthDataAndGetAuthToken::after', array($this, 'onAfterSetAuthDataAndGetAuthToken'), 10);
$this->subscribeEvent('Core::Logout::before', array($this, 'onBeforeLogout'));
$this->subscribeEvent('Core::DeleteUser::after', array($this, 'onAfterDeleteUser'));
$this->subscribeEvent('System::RunEntry::before', array($this, 'onBeforeRunEntry'));
$this->oWebAuthn = new WebAuthn\WebAuthn(
'WebAuthn Library',
$this->oHttp->GetHost(),
[
'android-key',
'android-safetynet',
'apple',
'fido-u2f',
'none',
'packed',
'tpm'
],
false
// array_merge($this->oModuleSettings->FacetIds, [$this->oHttp->GetScheme().'://'.$this->oHttp->GetHost(true, false)])
);
}
/**
*
* @return Manager
*/
public function getUsedDevicesManager()
{
if ($this->oUsedDevicesManager === null) {
$this->oUsedDevicesManager = new Manager($this);
}
return $this->oUsedDevicesManager;
}
/**
* Obtains list of module settings for authenticated user.
*
* @return array
*/
public function GetSettings()
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
$bAllowUsedDevices = $this->oModuleSettings->AllowUsedDevices;
$aSettings = [
'AllowBackupCodes' => $this->oModuleSettings->AllowBackupCodes,
'AllowSecurityKeys' => $this->oModuleSettings->AllowSecurityKeys,
'AllowAuthenticatorApp' => $this->oModuleSettings->AllowAuthenticatorApp,
'AllowUsedDevices' => $bAllowUsedDevices,
'TrustDevicesForDays' => $bAllowUsedDevices ? $this->oModuleSettings->TrustDevicesForDays : 0,
];
$oUser = Api::getAuthenticatedUser();
if ($oUser && $oUser->isNormalOrTenant()) {
$bShowRecommendationToConfigure = $this->oModuleSettings->ShowRecommendationToConfigure;
if ($bShowRecommendationToConfigure) {
$bShowRecommendationToConfigure = $oUser->getExtendedProp($this->GetName() . '::ShowRecommendationToConfigure');
}
$bAuthenticatorAppEnabled = $this->oModuleSettings->AllowAuthenticatorApp && $oUser->getExtendedProp($this->GetName() . '::Secret') ? true : false;
$aWebAuthKeysInfo = $this->oModuleSettings->AllowSecurityKeys ? $this->_getWebAuthKeysInfo($oUser) : [];
$iBackupCodesCount = 0;
if ($bAuthenticatorAppEnabled || count($aWebAuthKeysInfo) > 0) {
$sBackupCodes = \Aurora\System\Utils::DecryptValue($oUser->getExtendedProp($this->GetName() . '::BackupCodes'));
$aBackupCodes = empty($sBackupCodes) ? [] : json_decode($sBackupCodes);
$aNotUsedBackupCodes = array_filter($aBackupCodes, function ($sCode) {
return !empty($sCode);
});
$iBackupCodesCount = count($aNotUsedBackupCodes);
}
$aSettings = array_merge($aSettings, [
'ShowRecommendationToConfigure' => $bShowRecommendationToConfigure,
'WebAuthKeysInfo' => $aWebAuthKeysInfo,
'AuthenticatorAppEnabled' => $bAuthenticatorAppEnabled,
'BackupCodesCount' => $iBackupCodesCount,
]);
}
return $aSettings;
}
public function UpdateSettings($ShowRecommendationToConfigure)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
if ($this->oModuleSettings->ShowRecommendationToConfigure) {
$oUser = Api::getAuthenticatedUser();
if ($oUser && $oUser->isNormalOrTenant()) {
$oUser->setExtendedProp($this->GetName() . '::ShowRecommendationToConfigure', $ShowRecommendationToConfigure);
return $oUser->save();
}
}
return false;
}
/**
* Obtains user settings. Method is allowed for superadmin only.
*
* @param int $UserId
* @return array|null
*/
public function GetUserSettings($UserId)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
if ($this->oModuleSettings->AllowAuthenticatorApp) {
$oUser = Api::getUserById($UserId);
if ($oUser instanceof User && $oUser->isNormalOrTenant()) {
Api::checkUserAccess($oUser);
$iWebAuthnKeyCount = WebAuthnKey::where('UserId', $oUser->Id)->count();
return [
'TwoFactorAuthEnabled' => !empty($oUser->getExtendedProp($this->GetName() . '::Secret')) || $iWebAuthnKeyCount > 0
];
}
}
return null;
}
public function onAfterDeleteUser($aArgs, &$mResult)
{
if ($mResult) {
UsedDevice::where('UserId', $aArgs['UserId'])->delete();
}
}
/**
* Disables two factor authentication for specified user. Method is allowed for superadmin only.
*
* @param int $UserId
* @return boolean
*/
public function DisableUserTwoFactorAuth($UserId)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::TenantAdmin);
if (!$this->oModuleSettings->AllowAuthenticatorApp) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$oUser = Api::getUserById($UserId);
if ($oUser instanceof User && $oUser->isNormalOrTenant()) {
Api::checkUserAccess($oUser);
$oUser->setExtendedProp($this->GetName() . '::Secret', '');
$oUser->setExtendedProp($this->GetName() . '::IsEncryptedSecret', false);
$oUser->setExtendedProp($this->GetName() . '::Challenge', '');
$aWebAuthnKeys = WebAuthnKey::where('UserId', $oUser->Id)->get();
$bResult = true;
foreach ($aWebAuthnKeys as $oWebAuthnKey) {
$bResult = $bResult && $oWebAuthnKey->delete();
}
$oUser->setExtendedProp($this->GetName() . '::BackupCodes', '');
$oUser->setExtendedProp($this->GetName() . '::BackupCodesTimestamp', '');
$bResult = $bResult && \Aurora\Modules\Core\Module::Decorator()->UpdateUserObject($oUser);
$bResult = $bResult && $this->getUsedDevicesManager()->revokeTrustFromAllDevices($oUser);
return $bResult;
}
return false;
}
/**
* Verifies user's password and returns Secret and QR-code
*
* @param string $Password
* @return bool|array
*/
public function RegisterAuthenticatorAppBegin($Password)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
if (!$this->oModuleSettings->AllowAuthenticatorApp) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$oUser = Api::getAuthenticatedUser();
if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
if (empty($Password)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
}
if (!CoreModule::Decorator()->VerifyPassword($Password)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$oGoogle = new \PHPGangsta_GoogleAuthenticator();
$sSecret = '';
if ($oUser->getExtendedProp($this->GetName() . '::Secret')) {
$sSecret = $oUser->getExtendedProp($this->GetName() . '::Secret');
if ($oUser->getExtendedProp($this->GetName() . '::IsEncryptedSecret')) {
$sSecret = \Aurora\System\Utils::DecryptValue($sSecret);
}
} else {
$sSecret = $oGoogle->createSecret();
}
$sServerName = !empty($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : $_SERVER['HTTP_HOST'];
if (!empty($sServerName)) {
$sServerName = "(" . $sServerName . ")";
}
$sQRCodeName = $oUser->PublicId . $sServerName;
return [
'Secret' => $sSecret,
'QRCodeName' => $sQRCodeName,
'Enabled' => $oUser->getExtendedProp($this->GetName() . '::Secret') ? true : false
];
}
/**
* Verifies user's Code and saves Secret in case of success
*
* @param string $Password
* @param string $Code
* @param string $Secret
* @return boolean
* @throws \Aurora\System\Exceptions\ApiException
*/
public function RegisterAuthenticatorAppFinish($Password, $Code, $Secret)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
if (!$this->oModuleSettings->AllowAuthenticatorApp) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$oUser = Api::getAuthenticatedUser();
if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
if (empty($Password) || empty($Code) || empty($Secret)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
}
if (!CoreModule::Decorator()->VerifyPassword($Password)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$bResult = false;
$iClockTolerance = $this->oModuleSettings->ClockTolerance;
$oGoogle = new \PHPGangsta_GoogleAuthenticator();
$oStatus = $oGoogle->verifyCode($Secret, $Code, $iClockTolerance);
if ($oStatus === true) {
$oUser->setExtendedProp($this->GetName() . '::Secret', \Aurora\System\Utils::EncryptValue($Secret));
$oUser->setExtendedProp($this->GetName() . '::IsEncryptedSecret', true);
\Aurora\Modules\Core\Module::Decorator()->UpdateUserObject($oUser);
$bResult = true;
}
return $bResult;
}
/**
* Verifies user's Password and disables TwoFactorAuth in case of success
*
* @param string $Password
* @return bool
*/
public function DisableAuthenticatorApp($Password)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
if (!$this->oModuleSettings->AllowAuthenticatorApp) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$oUser = Api::getAuthenticatedUser();
if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
if (empty($Password)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
}
if (!CoreModule::Decorator()->VerifyPassword($Password)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$oUser->setExtendedProp($this->GetName() . '::Secret', "");
$oUser->setExtendedProp($this->GetName() . '::IsEncryptedSecret', false);
$bResult = \Aurora\Modules\Core\Module::Decorator()->UpdateUserObject($oUser);
$this->_removeAllDataWhenAllSecondFactorsDisabled($oUser);
return $bResult;
}
/**
* Verifies Authenticator code and returns AuthToken in case of success
*
* @param string $Code
* @param string $Login
* @param string $Password
* @return bool|array
* @throws \Aurora\System\Exceptions\ApiException
*/
public function VerifyAuthenticatorAppCode($Code, $Login, $Password)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
if (!$this->oModuleSettings->AllowAuthenticatorApp) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
if (empty($Code) || empty($Login) || empty($Password)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
}
self::$VerifyState = true;
$mAuthenticateResult = \Aurora\Modules\Core\Module::Decorator()->Authenticate($Login, $Password);
self::$VerifyState = false;
if (!$mAuthenticateResult || !is_array($mAuthenticateResult) || !isset($mAuthenticateResult['token'])) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AuthError);
}
$oUser = Api::getUserById((int) $mAuthenticateResult['id']);
if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$mResult = false;
if ($oUser->getExtendedProp($this->GetName() . '::Secret')) {
$sSecret = $oUser->getExtendedProp($this->GetName() . '::Secret');
if ($oUser->getExtendedProp($this->GetName() . '::IsEncryptedSecret')) {
$sSecret = \Aurora\System\Utils::DecryptValue($sSecret);
}
$oGoogle = new \PHPGangsta_GoogleAuthenticator();
$iClockTolerance = $this->oModuleSettings->ClockTolerance;
$oStatus = $oGoogle->verifyCode($sSecret, $Code, $iClockTolerance);
if ($oStatus) {
$mResult = \Aurora\Modules\Core\Module::Decorator()->SetAuthDataAndGetAuthToken($mAuthenticateResult);
}
} else {
throw new \Aurora\System\Exceptions\ApiException(Enums\ErrorCodes::SecretNotSet);
}
return $mResult;
}
/**
* Verifies user's password and returns backup codes generated earlier.
*
* @param string $Password
* @return array|boolean
*/
public function GetBackupCodes($Password)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
if (!$this->oModuleSettings->AllowBackupCodes) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$oUser = Api::getAuthenticatedUser();
if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
if (empty($Password)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
}
if (!CoreModule::Decorator()->VerifyPassword($Password)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$sBackupCodes = \Aurora\System\Utils::DecryptValue($oUser->getExtendedProp($this->GetName() . '::BackupCodes'));
return [
'Datetime' => $oUser->getExtendedProp($this->GetName() . '::BackupCodesTimestamp'),
'Codes' => empty($sBackupCodes) ? [] : json_decode($sBackupCodes)
];
}
/**
* Verifies user's password, generates backup codes and returns them.
*
* @param string $Password
* @return array|boolean
*/
public function GenerateBackupCodes($Password)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
if (!$this->oModuleSettings->AllowBackupCodes) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$oUser = Api::getAuthenticatedUser();
if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
if (empty($Password)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
}
if (!CoreModule::Decorator()->VerifyPassword($Password)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$oRecovery = new Recovery();
$aCodes = $oRecovery
->setCount(10) // Generate 10 codes
->setBlocks(2) // Every code must have 2 blocks
->setChars(4) // Each block must have 4 chars
->setBlockSeparator(' ')
->uppercase()
->toArray();
$oUser->setExtendedProp($this->GetName() . '::BackupCodes', \Aurora\System\Utils::EncryptValue(json_encode($aCodes)));
$oUser->setExtendedProp($this->GetName() . '::BackupCodesTimestamp', time());
\Aurora\Modules\Core\Module::Decorator()->UpdateUserObject($oUser);
return [
'Datetime' => $oUser->getExtendedProp($this->GetName() . '::BackupCodesTimestamp'),
'Codes' => $aCodes,
];
}
public function VerifyBackupCode($BackupCode, $Login, $Password)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
if (!$this->oModuleSettings->AllowBackupCodes) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
if (empty($BackupCode) || empty($Login) || empty($Password)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
}
self::$VerifyState = true;
$mAuthenticateResult = \Aurora\Modules\Core\Module::Decorator()->Authenticate($Login, $Password);
self::$VerifyState = false;
if (!$mAuthenticateResult || !is_array($mAuthenticateResult) || !isset($mAuthenticateResult['token'])) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AuthError);
}
$oUser = Api::getUserById((int) $mAuthenticateResult['id']);
if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$mResult = false;
$sBackupCodes = \Aurora\System\Utils::DecryptValue($oUser->getExtendedProp($this->GetName() . '::BackupCodes'));
$aBackupCodes = empty($sBackupCodes) ? [] : json_decode($sBackupCodes);
$sTrimmed = preg_replace('/\s+/', '', $BackupCode);
$sPrepared = substr_replace($sTrimmed, ' ', 4, 0);
$index = array_search($sPrepared, $aBackupCodes);
if ($index !== false) {
$aBackupCodes[$index] = '';
$oUser->setExtendedProp($this->GetName() . '::BackupCodes', \Aurora\System\Utils::EncryptValue(json_encode($aBackupCodes)));
\Aurora\Modules\Core\Module::Decorator()->UpdateUserObject($oUser);
$mResult = \Aurora\Modules\Core\Module::Decorator()->SetAuthDataAndGetAuthToken($mAuthenticateResult);
}
return $mResult;
}
/**
* Checks if User has TwoFactorAuth enabled and return UserId instead of AuthToken
*
* @param array $aArgs
* @param array $mResult
*/
public function onAfterAuthenticate($aArgs, &$mResult)
{
if (!self::$VerifyState && $mResult && is_array($mResult) && isset($mResult['token'])) {
$oUser = Api::getUserById((int) $mResult['id']);
if ($oUser instanceof User) {
$bHasSecurityKey = false;
if ($this->oModuleSettings->AllowSecurityKeys) {
$iWebAuthnKeyCount = WebAuthnKey::where('UserId', $oUser->Id)->count();
$bHasSecurityKey = $iWebAuthnKeyCount > 0;
}
$bHasAuthenticatorApp = false;
if ($this->oModuleSettings->AllowAuthenticatorApp) {
$bHasAuthenticatorApp = !!(!empty($oUser->getExtendedProp($this->GetName() . '::Secret')));
}
$bDeviceTrusted = ($bHasAuthenticatorApp || $bHasAuthenticatorApp) ? $this->getUsedDevicesManager()->checkDeviceAfterAuthenticate($oUser) : false;
if (($bHasSecurityKey || $bHasAuthenticatorApp) && !$bDeviceTrusted) {
$mResult = [
'TwoFactorAuth' => [
'HasAuthenticatorApp' => $bHasAuthenticatorApp,
'HasSecurityKey' => $bHasSecurityKey,
'HasBackupCodes' => $this->oModuleSettings->AllowBackupCodes && !empty($oUser->getExtendedProp($this->GetName() . '::BackupCodes'))
]
];
}
}
}
}
/**
* Verifies user's password and returns arguments for security key registration.
*
* @param string $Password
* @return array|boolean
*/
public function RegisterSecurityKeyBegin($Password)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
if (!$this->oModuleSettings->AllowSecurityKeys) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$oUser = Api::getAuthenticatedUser();
if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
if (empty($Password)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
}
if (!CoreModule::Decorator()->VerifyPassword($Password)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$oCreateArgs = $this->oWebAuthn->getCreateArgs(
\base64_encode($oUser->UUID),
$oUser->PublicId,
$oUser->PublicId,
90,
false,
'discouraged',
true,
[]
);
$oCreateArgs->publicKey->user->id = \base64_encode($oCreateArgs->publicKey->user->id->getBinaryString());
$oCreateArgs->publicKey->challenge = \base64_encode($oCreateArgs->publicKey->challenge->getBinaryString());
$oUser->setExtendedProp($this->GetName() . '::Challenge', $oCreateArgs->publicKey->challenge);
$oUser->save();
return $oCreateArgs;
}
/**
* Verifies user's password and finishes security key registration.
*
* @param array $Attestation
* @param string $Password
* @return boolean
* @throws \Aurora\System\Exceptions\ApiException
*/
public function RegisterSecurityKeyFinish($Attestation, $Password)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
if (!$this->oModuleSettings->AllowSecurityKeys) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$oUser = Api::getAuthenticatedUser();
if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
if (empty($Password) || empty($Attestation)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
}
if (!CoreModule::Decorator()->VerifyPassword($Password)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$data = $this->oWebAuthn->processCreate(
\base64_decode($Attestation['clientDataJSON']),
\base64_decode($Attestation['attestationObject']),
\base64_decode($oUser->getExtendedProp($this->GetName() . '::Challenge')),
false
);
$data->credentialId = \base64_encode($data->credentialId);
$data->AAGUID = \base64_encode($data->AAGUID);
$sEncodedSecurityKeyData = \json_encode($data);
if ($sEncodedSecurityKeyData === false) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::UnknownError, null, json_last_error_msg());
} else {
$oWebAuthnKey = new WebAuthnKey();
$oWebAuthnKey->UserId = $oUser->Id;
$oWebAuthnKey->KeyData = $sEncodedSecurityKeyData;
$oWebAuthnKey->CreationDateTime = time();
if ($oWebAuthnKey->save()) {
return $oWebAuthnKey->Id;
}
}
return false;
}
/**
* Authenticates user and returns arguments for security key verification.
*
* @param string $Login
* @param string $Password
* @return array|boolean
*/
public function VerifySecurityKeyBegin($Login, $Password)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
if (!$this->oModuleSettings->AllowSecurityKeys) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
self::$VerifyState = true;
$mAuthenticateResult = \Aurora\Modules\Core\Module::Decorator()->Authenticate($Login, $Password);
self::$VerifyState = false;
if (!$mAuthenticateResult || !is_array($mAuthenticateResult) || !isset($mAuthenticateResult['token'])) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AuthError);
}
$oUser = Api::getUserById((int) $mAuthenticateResult['id']);
if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$mGetArgs = false;
$aIds = [];
$aWebAuthnKeys = WebAuthnKey::where('UserId', $oUser->Id)->get();
foreach ($aWebAuthnKeys as $oWebAuthnKey) {
/** @var WebAuthnKey $oWebAuthnKey */
$oKeyData = \json_decode($oWebAuthnKey->KeyData);
$aIds[] = \base64_decode($oKeyData->credentialId);
}
if (count($aIds) > 0) {
$mGetArgs = $this->oWebAuthn->getGetArgs(
$aIds,
90
);
$mGetArgs->publicKey->challenge = \base64_encode($mGetArgs->publicKey->challenge->getBinaryString());
if (is_array($mGetArgs->publicKey->allowCredentials)) {
foreach ($mGetArgs->publicKey->allowCredentials as $key => $val) {
$val->id = \base64_encode($val->id->getBinaryString());
$mGetArgs->publicKey->allowCredentials[$key] = $val;
}
}
$oUser->setExtendedProp($this->GetName() . '::Challenge', $mGetArgs->publicKey->challenge);
$oUser->save();
}
return $mGetArgs;
}
/**
* Authenticates user and finishes security key verification.
*
* @param string $Login
* @param string $Password
* @param array $Attestation
* @return boolean
* @throws \Aurora\System\Exceptions\ApiException
*/
public function VerifySecurityKeyFinish($Login, $Password, $Attestation)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
if (!$this->oModuleSettings->AllowSecurityKeys) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
self::$VerifyState = true;
$mAuthenticateResult = \Aurora\Modules\Core\Module::Decorator()->Authenticate($Login, $Password);
self::$VerifyState = false;
if (!$mAuthenticateResult || !is_array($mAuthenticateResult) || !isset($mAuthenticateResult['token'])) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AuthError);
}
$oUser = Api::getUserById((int) $mAuthenticateResult['id']);
if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$mResult = true;
$clientDataJSON = base64_decode($Attestation['clientDataJSON']);
$authenticatorData = base64_decode($Attestation['authenticatorData']);
$signature = base64_decode($Attestation['signature']);
$id = base64_decode($Attestation['id']);
$credentialPublicKey = null;
$challenge = \base64_decode($oUser->getExtendedProp($this->GetName() . '::Challenge'));
$aWebAuthnKeys = WebAuthnKey::where('UserId', $oUser->Id)->get();
$oWebAuthnKey = null;
foreach ($aWebAuthnKeys as $oWebAuthnKey) {
/** @var WebAuthnKey $oWebAuthnKey */
$oKeyData = \json_decode($oWebAuthnKey->KeyData);
if (\base64_decode($oKeyData->credentialId) === $id) {
$credentialPublicKey = $oKeyData->credentialPublicKey;
break;
}
}
if ($credentialPublicKey !== null) {
try {
// process the get request. throws WebAuthnException if it fails
$this->oWebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, null, false);
$mResult = \Aurora\Modules\Core\Module::Decorator()->SetAuthDataAndGetAuthToken($mAuthenticateResult);
if (isset($oWebAuthnKey)) {
$oWebAuthnKey->LastUsageDateTime = time();
$oWebAuthnKey->save();
}
} catch (\Exception $oEx) {
$mResult = false;
throw new \Aurora\System\Exceptions\ApiException(999, $oEx, $oEx->getMessage());
}
}
return $mResult;
}
/**
* Verifies user's password and changes security key name.
*
* @param int $KeyId
* @param string $NewName
* @param string $Password
* @return boolean
*/
public function UpdateSecurityKeyName($KeyId, $NewName, $Password)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
if (!$this->oModuleSettings->AllowSecurityKeys) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$oUser = Api::getAuthenticatedUser();
if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
if (empty($Password) || empty($KeyId) || empty($NewName)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
}
if (!CoreModule::Decorator()->VerifyPassword($Password)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$mResult = false;
$oWebAuthnKey = WebAuthnKey::where('UserId', $oUser->Id)
->where('Id', $KeyId)
->first();
if ($oWebAuthnKey instanceof WebAuthnKey) {
$oWebAuthnKey->Name = $NewName;
$mResult = $oWebAuthnKey->save();
}
return $mResult;
}
/**
* Verifies user's password and removes secutiry key.
*
* @param int $KeyId
* @param string $Password
* @return boolean
*/
public function DeleteSecurityKey($KeyId, $Password)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
if (!$this->oModuleSettings->AllowSecurityKeys) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$oUser = Api::getAuthenticatedUser();
if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
if (empty($Password) || empty($KeyId)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
}
if (!CoreModule::Decorator()->VerifyPassword($Password)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$mResult = false;
$oWebAuthnKey = WebAuthnKey::where('UserId', $oUser->Id)
->where('Id', $KeyId)
->first();
if ($oWebAuthnKey instanceof WebAuthnKey) {
$mResult = $oWebAuthnKey->delete();
$this->_removeAllDataWhenAllSecondFactorsDisabled($oUser);
}
return $mResult;
}
/**
* Verifies user's password.
*
* @param string $Password
* @return boolean
*/
public function VerifyPassword($Password)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
if (empty($Password)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
}
return CoreModule::Decorator()->VerifyPassword($Password);
}
public function EntryVerifySecurityKey()
{
$oModuleManager = Api::GetModuleManager();
$sTheme = $oModuleManager->getModuleConfigValue('CoreWebclient', 'Theme');
$oHttp = \MailSo\Base\Http::SingletonInstance();
$sLogin = $oHttp->GetQuery('login', '');
$sPassword = $oHttp->GetQuery('password', '');
$sPackageName = $oHttp->GetQuery('package_name', '');
if (empty($sLogin) || empty($sPassword)) {
return '';
}
$oGetArgs = false;
$sError = false;
try {
$oGetArgs = self::Decorator()->VerifySecurityKeyBegin($sLogin, $sPassword);
} catch (\Exception $oEx) {
$sError = $oEx->getCode() . ': ' . $oEx->getMessage();
}
$sResult = \file_get_contents($this->GetPath() . '/templates/EntryVerifySecurityKey.html');
$sResult = \strtr($sResult, array(
'{{GetArgs}}' => \Aurora\System\Managers\Response::GetJsonFromObject(null, $oGetArgs),
'{{PackageName}}' => $sPackageName,
'{{Error}}' => $sError,
'{{Description}}' => $this->i18N('HINT_INSERT_TOUCH_SECURITY_KEY'),
'{{Theme}}' => $sTheme,
));
\Aurora\Modules\CoreWebclient\Module::Decorator()->SetHtmlOutputHeaders();
@header('Cache-Control: no-cache', true);
return $sResult;
}
public function EntryAssetlinks()
{
@header('Content-Type: application/json; charset=utf-8');
@header('Cache-Control: no-cache', true);
$sPath = __DIR__ . '/assets/assetlinks.json';
$sDistPath = __DIR__ . '/assets/assetlinks.dist.json';
if (file_exists($sPath)) {
$sFileContent = file_get_contents($sPath);
} elseif (file_exists($sDistPath)) {
$sFileContent = file_get_contents($sDistPath);
} else {
$sFileContent = "[]";
}
echo $sFileContent;
}
public function TrustDevice($DeviceId, $DeviceName)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
if (!$this->oModuleSettings->AllowUsedDevices) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$authToken = Api::getAuthToken();
$oUser = Api::getAuthenticatedUser($authToken);
if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
return $this->getUsedDevicesManager()->trustDevice($oUser->Id, $DeviceId, $DeviceName, $authToken);
}
/**
* @deprecated since version 9.7.2. Use SetDeviceName instead.
*/
public function SaveDevice($DeviceId, $DeviceName)
{
return $this->Decorator()->SetDeviceName($DeviceId, $DeviceName);
}
/**
* @param string $DeviceId
* @param string $DeviceName
*
* @return boolean
*/
public function SetDeviceName($DeviceId, $DeviceName)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
if (!is_string($DeviceId) && count($DeviceId) < 4 && empty($DeviceName)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
}
$mResult = false;
if ($this->oModuleSettings->AllowUsedDevices) {
$oUser = Api::getAuthenticatedUser();
$mResult = $this->getUsedDevicesManager()->setDeviceName($oUser->Id, $DeviceId, $DeviceName);
}
return $mResult;
}
/**
* @param string $DeviceId
* @param string $DeviceCustomName
*
* @return boolean
*/
public function SetDeviceCustomName($DeviceId, $DeviceCustomName)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
if (!is_string($DeviceId) && count($DeviceId) < 4 && empty($DeviceCustomName)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
}
$mResult = false;
if ($this->oModuleSettings->AllowUsedDevices) {
$oUser = Api::getAuthenticatedUser();
$mResult = $this->getUsedDevicesManager()->setDeviceCustomName($oUser->Id, $DeviceId, $DeviceCustomName);
}
return $mResult;
}
public function GetUsedDevices()
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
if (!$this->oModuleSettings->AllowUsedDevices) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$oUser = Api::getAuthenticatedUser();
if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
return $this->getUsedDevicesManager()->getAllDevices($oUser->Id)->all();
}
public function RevokeTrustFromAllDevices()
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
if (!$this->oModuleSettings->AllowUsedDevices) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
if (!$this->getUsedDevicesManager()->isTrustedDevicesEnabled()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$oUser = Api::getAuthenticatedUser();
if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
return $this->getUsedDevicesManager()->revokeTrustFromAllDevices($oUser);
}
public function onBeforeLogout($aArgs, &$mResult)
{
$oUser = Api::getAuthenticatedUser();
if ($oUser instanceof User && $oUser->isNormalOrTenant()) {
$oUsedDevice = $this->getUsedDevicesManager()->getDeviceByAuthToken($oUser->Id, Api::getAuthToken());
if ($oUsedDevice) {
$oUsedDevice->AuthToken = '';
$oUsedDevice->save();
}
}
}
public function LogoutFromDevice($DeviceId)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
if (!$this->oModuleSettings->AllowUsedDevices) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
$oUser = Api::getAuthenticatedUser();
if (!($oUser instanceof User) || !$oUser->isNormalOrTenant()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
if (empty($DeviceId)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
}
$oUsedDevice = $this->getUsedDevicesManager()->getDevice($oUser->Id, $DeviceId);
if ($oUsedDevice && !empty($oUsedDevice->AuthToken)) {
Api::UserSession()->Delete($oUsedDevice->AuthToken);
$oUsedDevice->AuthToken = '';
$oUsedDevice->TrustTillDateTime = $oUsedDevice->CreationDateTime; // revoke trust
$oUsedDevice->save();
}
return true;
}
public function RemoveDevice($DeviceId)
{
Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
$oUser = Api::getAuthenticatedUser();
if (!$this->oModuleSettings->AllowUsedDevices && !$oUser->isAdmin()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
if (!($oUser instanceof User) || !$oUser->isNormalOrTenant() && !$oUser->isAdmin()) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
}
if (empty($DeviceId)) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
}
$oUsedDevice = $oUser->isAdmin() ? $this->getUsedDevicesManager()->getDeviceByDeviceId($DeviceId) : $this->getUsedDevicesManager()->getDevice($oUser->Id, $DeviceId);
if ($oUsedDevice) {
Api::UserSession()->Delete($oUsedDevice->AuthToken);
$oUsedDevice->delete();
}
return true;
}
protected function _getWebAuthKeysInfo($oUser)
{
$aWebAuthKeysInfo = [];
if ($oUser instanceof User && $oUser->isNormalOrTenant()) {
$aWebAuthnKeys = WebAuthnKey::where('UserId', $oUser->Id)->get();
foreach ($aWebAuthnKeys as $oWebAuthnKey) {
/** @var WebAuthnKey $oWebAuthnKey */
$aWebAuthKeysInfo[] = [
$oWebAuthnKey->Id,
$oWebAuthnKey->Name
];
}
}
return $aWebAuthKeysInfo;
}
protected function _removeAllDataWhenAllSecondFactorsDisabled($oUser)
{
$iWebAuthnKeyCount = WebAuthnKey::where('UserId', $oUser->Id)->count();
if (empty($oUser->getExtendedProp($this->GetName() . '::Secret')) && $iWebAuthnKeyCount === 0) {
$oUser->setExtendedProp($this->GetName() . '::BackupCodes', '');
$oUser->setExtendedProp($this->GetName() . '::BackupCodesTimestamp', '');
\Aurora\Modules\Core\Module::Decorator()->UpdateUserObject($oUser);
$this->getUsedDevicesManager()->revokeTrustFromAllDevices($oUser);
}
}
public function onBeforeRunEntry(&$aArgs, &$mResult)
{
$error = false;
if ($aArgs['EntryName'] === 'api' && $this->oModuleSettings->AllowUsedDevices) {
$user = \Aurora\System\Api::getAuthenticatedUser();
$authToken = \Aurora\System\Api::getAuthenticatedUserAuthToken();
if ($user && $user->isNormalOrTenant()) {
$deviceId = Api::getDeviceIdFromHeaders();
if ($deviceId) {
$usedDevice = $this->getUsedDevicesManager()->getDevice($user->Id, $deviceId);
if (!$usedDevice) {
$error = true;
} elseif ($usedDevice->AuthToken !== $authToken) {
$error = true;
}
} else {
$error = true;
}
}
}
if ($error) {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AuthError);
}
}
public function onAfterSetAuthDataAndGetAuthToken(&$aArgs, &$mResult)
{
if (is_array($mResult) && isset($mResult[\Aurora\System\Application::AUTH_TOKEN_KEY]) && $this->oModuleSettings->AllowUsedDevices) {
$deviceId = Api::getDeviceIdFromHeaders();
if ($deviceId && is_string($deviceId)) {
$sFallbackName = $_SERVER['HTTP_USER_AGENT'] ?? $_SERVER['REMOTE_ADDR'];
$sFallbackName = substr((string)explode(' ', $sFallbackName)[0], 0, 255);
$this->getUsedDevicesManager()->saveDevice(Api::getAuthenticatedUserId(), $deviceId, $sFallbackName, $mResult[\Aurora\System\Application::AUTH_TOKEN_KEY]);
} else {
throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AuthError);
}
}
}
}