Current Path : /var/www/www-root/data/www/monolith-realty.ru/bitrix/modules/security/lib/mfa/ |
Current File : /var/www/www-root/data/www/monolith-realty.ru/bitrix/modules/security/lib/mfa/otp.php |
<?php namespace Bitrix\Security\Mfa; use Bitrix\Main\Application; use Bitrix\Main\ArgumentOutOfRangeException; use Bitrix\Main\ArgumentTypeException; use Bitrix\Main\Config\Option; use Bitrix\Main\Localization\Loc; use Bitrix\Main\Type; use Bitrix\Main\Security\Sign\BadSignatureException; use Bitrix\Main\Security\Sign\TimeSigner; use Bitrix\Main\Security\Random; use Bitrix\Main\Text\Base32; use Bitrix\Main\Security\Mfa\OtpAlgorithm; use Bitrix\Main\Authentication\Policy; Loc::loadMessages(__FILE__); class Otp { const TYPE_HOTP = 'hotp'; const TYPE_TOTP = 'totp'; const TYPE_DEFAULT = self::TYPE_HOTP; const SECRET_LENGTH = 20; // Must be power of 5 for "nicely" App Secret view const SKIP_COOKIE = 'OTPH'; const REJECTED_KEY = 'OTP_REJECT_REASON'; const REJECT_BY_CODE = 'code'; const REJECT_BY_MANDATORY = 'mandatory'; const TAGGED_CACHE_TEMPLATE = 'USER_OTP_%d'; protected static $availableTypes = array(self::TYPE_HOTP, self::TYPE_TOTP); protected static $typeMap = array( self::TYPE_HOTP => '\Bitrix\Main\Security\Mfa\HotpAlgorithm', self::TYPE_TOTP => '\Bitrix\Main\Security\Mfa\TotpAlgorithm', ); protected $algorithmClass = null; protected array $initParams = []; protected $regenerated = false; /* @var \Bitrix\Main\Context $context */ protected $context = null; protected $userId = null; protected $userLogin = null; /* @var Policy\RulesCollection*/ protected $userGroupPolicy; protected $active = null; protected $userActive = null; protected $secret = null; protected $issuer = null; protected $label = null; protected $params = null; protected $attempts = null; protected $type = null; /** @var Type\DateTime */ protected $initialDate = null; protected $skipMandatory = null; /** @var Type\DateTime */ protected $deactivateUntil = null; /** * @param string|null $algorithm Class of needed OtpAlgorithm. */ public function __construct($algorithm = null) { if ($algorithm === null) { $this->setType(static::getDefaultType()); } else { $this->algorithmClass = $algorithm; } } /** * Return new instance for user provided by user ID * * @param int $userId User ID. * @throws ArgumentOutOfRangeException * @throws ArgumentTypeException * @return static New instance, if user does not use OTP - returning NullObject (see Otp::isActivated). */ public static function getByUser($userId) { $userId = (int) $userId; if ($userId <= 0) throw new ArgumentTypeException('userId', 'positive integer'); $userInfo = UserTable::getList(array( 'filter' => array('=USER_ID' => $userId), 'select' => array('ACTIVE', 'USER_ID', 'SECRET', 'INIT_PARAMS', 'PARAMS', 'TYPE', 'ATTEMPTS', 'INITIAL_DATE', 'SKIP_MANDATORY', 'DEACTIVATE_UNTIL', 'USER_ACTIVE' => 'USER.ACTIVE') )); $userInfo = $userInfo->fetch(); if (!$userInfo) { // OTP not available for this user $instance = new static; $instance->setUserId($userId); $instance->setActive(false); } else { $type = $userInfo['TYPE'] ?: self::TYPE_DEFAULT; $userInfo['SECRET'] = pack('H*', $userInfo['SECRET']); $userInfo['ACTIVE'] = ($userInfo['ACTIVE'] === 'Y'); $userInfo['USER_ACTIVE'] = ($userInfo['USER_ACTIVE'] === 'Y'); $userInfo['SKIP_MANDATORY'] = $userInfo['SKIP_MANDATORY'] === 'Y'; $instance = static::getByType($type); $instance->setUserInfo($userInfo); } return $instance; } /** * Return new instance with needed OtpAlgorithm type * * @param string $type Type of OtpAlgorithm (see getAvailableTypes). * @throws ArgumentOutOfRangeException * @return static New instance */ public static function getByType($type) { if (!in_array($type, static::$availableTypes)) throw new ArgumentOutOfRangeException('type', static::$availableTypes); $algo = static::$typeMap[$type]; $instance = new static($algo); $instance->setType($type); return $instance; } /** * Set new type of OtpAlgorithm * * @param string $type Type of OtpAlgorithm (see getAvailableTypes). * @throws ArgumentOutOfRangeException * @return $this */ public function setType($type) { if (!in_array($type, static::$availableTypes)) throw new ArgumentOutOfRangeException('type', static::$availableTypes); $this->algorithmClass = static::$typeMap[$type]; $this->type = $type; return $this; } /** * Sets initialization parameters for algorithms. * * @param array $params * @return $this */ public function setInitParams(array $params) { $this->initParams = $params; return $this; } /** * Returns initialization parameters for algorithms. * * @return array */ public function getInitParams(): array { return $this->initParams; } /** * Return used OtpAlgorithm type * * @return string */ public function getType() { return $this->type; } /** * Return instance of used OtpAlgorithm * * @return OtpAlgorithm */ public function getAlgorithm() { /** @var OtpAlgorithm $algorithm */ $algorithm = new $this->algorithmClass($this->getInitParams()); $algorithm->setSecret($this->getSecret()); return $algorithm; } /** * Return Provision URI according to KeyUriFormat * * @link https://code.google.com/p/google-authenticator/wiki/KeyUriFormat * @param array $opts Additional URI parameters, e.g. ['image' => 'http://example.com/my_logo.png'] . * @return string */ public function getProvisioningUri(array $opts = array()) { $issuer = $this->getIssuer(); $opts += array('issuer' => $issuer); return $this ->getAlgorithm() ->generateUri( $this->getLabel($issuer), $opts ); } /** * Reinitialize OTP (generate new secret, set default algo, etc), must be called before connect new device * * @param null $newSecret Using custom secret. * @return $this */ public function regenerate($newSecret = null) { if (!$newSecret) { $newSecret = Random::getBytes(static::SECRET_LENGTH); } $this->regenerated = true; return $this ->setType(static::getDefaultType()) ->setAttempts(0) ->setSkipMandatory(false) ->setInitialDate(new Type\DateTime) ->setDeactivateUntil(null) ->setParams(null) ->setSecret($newSecret) ->setActive(true) ; } /** * Verify provided input * * @param string $input Input received from user. * @param bool $updateParams Update or not user parameters in DB (e.g. counter for HotpAlgorithm). * @return bool True if input is valid. */ public function verify($input, $updateParams = true) { [$result, $newParams] = $this->getAlgorithm()->verify($input, $this->getParams()); if ( $updateParams && $newParams !== null && $this->isActivated() ) { $this ->setParams($newParams) ->save() ; } return $result; } /** * Check is verifying attempts reached according to group security policy * May be used for show Captcha or what ever you want * * @return bool */ public function isAttemptsReached() { $attempts = $this->getAttempts(); $maxAttempts = $this->getMaxLoginAttempts(); return ( $maxAttempts > 0 && $attempts >= $maxAttempts ); } /** * Return synchronized user params for provided inputs * * @param string $inputA First code. * @param string $inputB Second code. * @throws \Bitrix\Main\Security\OtpException * @return string */ public function getSyncParameters($inputA, $inputB) { return $this->getAlgorithm()->getSyncParameters((string) $inputA, (string) $inputB); } /** * Synchronize user params for provided inputs * Must be called after regenerate and before save! * If something went wrong - throw OtpException with valid description in message * * @param string $inputA First code. * @param string|null $inputB Second code. * @throws OtpException * @return $this */ public function syncParameters($inputA, $inputB = null) { if (!$inputA) throw new OtpException(Loc::getMessage('SECURITY_OTP_ERROR_PASS1_EMPTY')); elseif (!preg_match('/^\d{6}$/D', $inputA)) throw new OtpException(getMessage('SECURITY_OTP_ERROR_PASS1_INVALID')); if ($this->getAlgorithm()->isTwoCodeRequired()) { if (!$inputB) throw new OtpException(Loc::getMessage('SECURITY_OTP_ERROR_PASS2_EMPTY')); elseif (!preg_match('/^\d{6}$/D', $inputB)) throw new OtpException(Loc::getMessage('SECURITY_OTP_ERROR_PASS2_INVALID')); } try { $params = $this->getSyncParameters($inputA, $inputB); } catch (\Bitrix\Main\Security\OtpException) { throw new OtpException(Loc::getMessage('SECURITY_OTP_ERROR_SYNC_ERROR')); } $this->setParams($params); return $this; } /** * Save all OTP data to DB * * @throws OtpException * @return bool */ public function save() { $fields = array( 'ACTIVE' => $this->isActivated()? 'Y': 'N', 'TYPE' => $this->getType(), 'INIT_PARAMS' => $this->getInitParams(), 'ATTEMPTS' => $this->getAttempts(), 'SECRET' => $this->getHexSecret(), 'INITIAL_DATE' => $this->getInitialDate()?: new Type\DateTime, 'PARAMS' => $this->getParams(), 'SKIP_MANDATORY' => $this->isMandatorySkipped()? 'Y': 'N', 'DEACTIVATE_UNTIL' => $this->getDeactivateUntil() ); if ($this->regenerated) { if (!$this->isInitialized()) throw new OtpException('Missing OTP params, forgot to call syncParameters?'); // Clear recovery codes when we connect new device RecoveryCodesTable::clearByUser($this->getUserId()); } if ($this->isDbRecordExists()) { $result = UserTable::update($this->getUserId(), $fields); } else { $fields += array( 'USER_ID' => $this->getUserId(), ); $result = UserTable::add($fields); } $this->clearGlobalCache(); return $result->isSuccess(); } /** * Delete OTP record from DB * * @return $this */ public function delete() { UserTable::delete($this->getUserId()); return $this; } /** * Activates user's OTP. * OTP must be initialized (have secret, params, etc.) before activate * * @return $this * @throws OtpException */ public function activate() { if (!$this->isInitialized()) throw new OtpException('OTP not initialized, if your activate it - user can\'t login anymore. Do you forgot to call regenerate?'); $this ->setActive(true) ->setDeactivateUntil(null) ->save(); return $this; } /** * Deactivate user OTP for a needed number of days or forever * * @param int $days Days. 0 means "forever". * @return $this * @throws OtpException */ public function deactivate($days = 0) { if (!$this->isActivated()) throw new OtpException('Otp not activated. Do your mean deffer?'); $this->setActive(false); $this->setSkipMandatory(); if ($days <= 0) { $this->setDeactivateUntil(null); } else { $deactivateDate = Type\DateTime::createFromTimestamp(time() + $days * 86400); $this->setDeactivateUntil($deactivateDate); } $this->save(); return $this; } /** * Defer mandatory user OTP using for a needed number of days or forever * * @param int $days Days. 0 means "forever". * @return $this * @throws OtpException */ public function defer($days = 0) { if ($this->isActivated()) throw new OtpException('Otp already activated. Do your mean deactivate?'); $this->setSkipMandatory(); if ($days <= 0) { $this->setDeactivateUntil(null); } else { $deactivateDate = Type\DateTime::createFromTimestamp(time() + $days * 86400); $this->setDeactivateUntil($deactivateDate); } $this->save(); return $this; } /** * Set new user information * Mostly used for initialization from DB * Now support: * - ACTIVE: bool, activating state (see setActive) * - USER_ID: integer, User ID (see setUserId) * - ATTEMPTS: integer, Attempts counter (see setAttempts) * - SECRET: binary, User secret (see setSecret) * - PARAMS: string, User params (see setParams and getSyncParameters) * - INITIAL_DATE: Type\Date, OTP initial date (see setInitialDate) * * @param array $userInfo See above. * @return $this */ public function setUserInfo(array $userInfo) { $this ->setActive($userInfo['ACTIVE']) ->setUserActive($userInfo['USER_ACTIVE']) ->setUserId($userInfo['USER_ID']) ->setAttempts($userInfo['ATTEMPTS']) ->setSecret($userInfo['SECRET']) ->setInitParams($userInfo['INIT_PARAMS']) ->setParams($userInfo['PARAMS']) ->setSkipMandatory($userInfo['SKIP_MANDATORY']) ; // Old users haven't INITIAL_DATE and DEACTIVATE_UNTIL // ToDo: maybe it's not the best approach, think about it later if ($userInfo['INITIAL_DATE']) $this->setInitialDate($userInfo['INITIAL_DATE']); if ($userInfo['DEACTIVATE_UNTIL']) $this->setDeactivateUntil($userInfo['DEACTIVATE_UNTIL']); return $this; } /** * Set new OTP initialization date * * @param Type\DateTime $date Initialization date. * @return $this */ protected function setInitialDate(Type\DateTime $date) { $this->initialDate = $date; return $this; } /** * Returns OTP initialization date * * @return Type\DateTime */ public function getInitialDate() { return $this->initialDate; } /** * Set datetime when user OTP must activated back * * @param Type\DateTime|null $date Datetime. "null" means never. * @return $this */ protected function setDeactivateUntil($date) { $this->deactivateUntil = $date; return $this; } /** * @return Type\DateTime */ public function getDeactivateUntil() { return $this->deactivateUntil; } /** * Set if user allowed to bypass OTP mandatory using while authorization * * @param bool $isSkipped Allowed or not. * @return $this */ protected function setSkipMandatory($isSkipped = true) { $this->skipMandatory = $isSkipped; return $this; } /** * Returns true if user can skip mandatory using * * @return bool */ public function isMandatorySkipped() { return $this->skipMandatory; } /** * Returns Unix timestamp of OTP initialization date * * @return int */ protected function getInitialTimestamp() { $initialDate = $this->getInitialDate(); if (!$initialDate) return 0; return $initialDate->getTimestamp(); } /** * Set new User ID * * @param int $userId User ID. * @return $this */ protected function setUserId($userId) { $this->userId = $userId; return $this; } /** * Return used User ID * * @return int */ public function getUserId() { return (int) $this->userId; } /** * Set new activating state * * @param bool $isActive Otp is activated or not. * @return $this */ public function setActive($isActive) { $this->active = $isActive; return $this; } /** * Return is OTP activated or not * * @return bool */ public function isActivated() { return (bool) $this->active; } public function setUserActive($isActive) { $this->userActive = $isActive; return $this; } public function isUserActive() { return (bool) $this->userActive; } /** * @return bool */ public function isInitialized() { if ($this->isActivated()) { // Without "hacks" OTP can't be activated without initialization return true; } // ToDo: maybe better add new property with column? return (bool) $this->getSecret(); } /** * Set new verifying attempts count * * @param int $attemptsCount Attempts count. * @return $this */ protected function setAttempts($attemptsCount) { $this->attempts = $attemptsCount; return $this; } /** * Return verifying attempts count * * @return int */ public function getAttempts() { return (int) $this->attempts; } /** * Set new user params (e.g. counter for HotpAlgorithm) * * @see getSyncParameters * @param string $params User params. * @return $this */ protected function setParams($params) { $this->params = $params; return $this; } /** * Return user params (e.g. counter for HotpAlgorithm) * * @return string */ public function getParams() { return (string) $this->params; } /** * Return binary secret * * @return string */ public function getSecret() { return $this->secret; } /** * Return hex-encoded secret * * @return string */ public function getHexSecret() { $secret = $this->getSecret(); return bin2hex($secret); } /** * Return mobile application secret, using for manual device initialization * * @return string */ public function getAppSecret() { $secret = $this->getSecret(); $secret = Base32::encode($secret); return rtrim($secret, '='); } /** * Set new secret * * @param string $secret Binary secret. * @return $this */ public function setSecret($secret) { $this->secret = $secret; return $this; } /** * Set new secret in hex-encoded representation * * @param string $hexValue Hex-encoded secret. * @return $this */ public function setHexSecret($hexValue) { $secret = pack('H*', $hexValue); return $this->setSecret($secret); } /** * Set new mobile application secret * * @param string $value Secret. * @return $this */ public function setAppSecret($value) { $secret = Base32::decode($value); return $this->setSecret($secret); } /** * Return issuer. * If custom issuer not available - return default (see getDefaultIssuer). * * @return string */ public function getIssuer() { if ($this->issuer === null) $this->issuer = $this->getDefaultIssuer(); return $this->issuer; } /** * Set custom issuer * * @param string $issuer Issuer. * @return $this */ public function setIssuer($issuer) { $this->issuer = $issuer; return $this; } /** * Return label for issuer (if provided) * If custom label not available - generate default (see generateLabel) * * @param string|null $issuer Issuer. * @return string */ public function getLabel($issuer = null) { if ($this->label === null) $this->label = $this->generateLabel($issuer); return $this->label; } /** * Set custom label * * @param string $label Label. * @return $this */ public function setLabel($label) { $this->label = $label; return $this; } /** * Returns context of the current request. * * @return \Bitrix\Main\Context */ public function getContext() { if ($this->context === null) $this->context = Application::getInstance()->getContext(); return $this->context; } /** * Set context of the current request. * * @param \Bitrix\Main\Context $context Application context. * @return $this */ public function setContext(\Bitrix\Main\Context $context) { $this->context = $context; return $this; } /** * Set custom user login * * @param string $login Login. * @return $this */ public function setUserLogin($login) { $this->userLogin = $login; return $this; } /** * Return user login * If custom login not available it will be fetched from DB * * @return string */ public function getUserLogin() { if ($this->userLogin === null && $this->userId) { $this->userLogin = \Bitrix\Main\UserTable::query() ->addFilter('=ID', $this->getUserId()) ->addSelect('LOGIN') ->exec() ->fetch(); $this->userLogin = $this->userLogin['LOGIN']; } return $this->userLogin; } /** * Return default issuer * * @return string */ protected function getDefaultIssuer() { $host = Option::get('main', 'server_name'); if($host) { return preg_replace('#:\d+$#D', '', $host); } else { return Option::get('security', 'otp_issuer', 'Bitrix'); } } /** * Generate label, based on current host, user login and issuer (if provided) * * @param string|null $issuer Issuer. * @return string */ protected function generateLabel($issuer = null) { if ($issuer) return sprintf('%s:%s', $issuer, $this->getUserLogin()); else return $this->getUserLogin(); } /** * Return maximum verifying attempts, based on security group policy * * @return int */ protected function getMaxLoginAttempts() { if (!$this->isActivated()) return 0; return (int) $this->getPolicy()->getLoginAttempts(); } /** * Return how long (in sec)remember value are valid * * @return int */ protected function getRememberLifetime() { if (!$this->isActivated()) return 0; return ((int) $this->getPolicy()->getStoreTimeout()) * 60; } /** * Return IP mask for checks remember value * * @return string */ protected function getRememberIpMask() { if (!$this->isActivated()) return '255.255.255.255'; return $this->getPolicy()->getStoreIpMask(); } /** * Check if current user can skip OTP mandatory using. * It can skip if: * - Otp already activated * - User never login before * - User not included to mandatory rights * - The current date is included in the window initialization * * @return bool */ public function canSkipMandatory() { $result = $this->isMandatorySkipped(); if (!$result) { // Check mandatory rights $result = $this->canSkipMandatoryByRights(); } return $result; } /** * Check if current user not included to mandatory rights * * @return bool */ public function canSkipMandatoryByRights() { $targetRights = static::getMandatoryRights(); $userRights = \CAccess::getUserCodesArray($this->getUserId()); $existedRights = array_intersect($targetRights, $userRights); $result = empty($existedRights); return $result; } /** * Check if user have valid cookie for skip OTP checking ("Remember OTP on this computer") * * @return bool */ protected function canSkipByCookie() { if (Option::get('security', 'otp_allow_remember') !== 'Y') return false; $signedValue = $this->getContext()->getRequest()->getCookie(static::SKIP_COOKIE); if (!$signedValue || !is_string($signedValue)) return false; try { $signer = new TimeSigner(); $value = $signer ->setKey($this->getSecret()) ->unsign($signedValue, 'MFA_SAVE'); } catch (BadSignatureException) { return false; } return ($value === $this->getSkipCookieValue()); } /** * Generate skip value for save in cookies * Currently based on client IP and mask (see getRememberIpMask) * * @return string */ protected function getSkipCookieValue() { // ToDo: must be tied to the ID of "computer" when it will appear in the main module $rememberMask = $this->getRememberIpMask(); $userIp = $this->getContext()->getRequest()->getRemoteAddress(); return md5(ip2long($rememberMask) & ip2long($userIp)); } /** * Store new value for skip OTP checking ("Remember OTP on this computer") in cookies * * @return $this */ protected function setSkipCookie() { /** @global \CMain $APPLICATION */ global $APPLICATION; $signer = new TimeSigner(); $rememberLifetime = $this->getRememberLifetime(); $rememberLifetime += time(); $rememberValue = $this->getSkipCookieValue(); $signedValue = $signer ->setKey($this->getSecret()) ->sign($rememberValue, $rememberLifetime, 'MFA_SAVE'); $isSecure = ( Option::get('main', 'use_secure_password_cookies', 'N') === 'Y' && $this->getContext()->getRequest()->isHttps() ); $APPLICATION->set_cookie( static::SKIP_COOKIE, // $name $signedValue, // $value $rememberLifetime, // $time = false '/', // $folder = "/" false, // $domain = false $isSecure, // $secure = false true, // $spread = true false, // $name_prefix = false true // $httpOnly = false ); return $this; } /** * Check if OTP record exists in DB * * @return bool */ protected function isDbRecordExists() { return UserTable::getRowById($this->getUserId()) !== null; } /** * Return needed group security policy * * @return Policy\RulesCollection */ protected function getPolicy() { if (!$this->userGroupPolicy) { $this->userGroupPolicy = \CUser::getPolicy($this->getUserId()); } return $this->userGroupPolicy; } /** * Clear cache for this OTP in global scope * * @return $this */ protected function clearGlobalCache() { $cache_dir = '/otp/user_id/' . substr(md5($this->getUserId()), -2) . '/' . $this->getUserId() . '/'; $cache = new \CPHPCache; $cache->CleanDir($cache_dir); return $this; } /** * Most complex method, can check everything:-) * ToDo: describe after refactoring * * @param array $params Event parameters. * @return bool */ public static function verifyUser(array $params) { global $APPLICATION; if (!static::isOtpEnabled()) // OTP disabled in settings return true; $isSuccess = false; // ToDo: review and refactoring needed $otp = static::getByUser($params['USER_ID']); if (!$otp->isActivated()) { // User does not use OTP $isSuccess = true; if ( static::isMandatoryUsing() && !$otp->canSkipMandatory() ) { // Grace full period ends. We must reject authorization and defer reject reason if (!$otp->isDbRecordExists() && static::getSkipMandatoryDays()) { // If mandatory enabled and user never use OTP - let us deffer initialization $otp->defer(static::getSkipMandatoryDays()); // We forgive the user for the first time static::setDeferredParams(null); return true; } // Save a flag which indicates that an OTP is required, but user doesn't use it :-( $params[static::REJECTED_KEY] = static::REJECT_BY_MANDATORY; static::setDeferredParams($params); return false; } } else { if (!$otp->isUserActive()) { //non-active user can't log in by OTP return false; } } if (!$isSuccess) { // User skip OTP on this browser by cookie $isSuccess = $otp->canSkipByCookie(); } if (!$isSuccess) { $isCaptchaChecked = ( !$otp->isAttemptsReached() || $APPLICATION->captchaCheckCode($params['CAPTCHA_WORD'], $params['CAPTCHA_SID']) ); $isRememberNeeded = ( $params['OTP_REMEMBER'] && Option::get('security', 'otp_allow_remember') === 'Y' ); if (!$isCaptchaChecked && !$APPLICATION->NeedCAPTHA()) { // Backward compatibility with old login page $APPLICATION->SetNeedCAPTHA(true); } $isOtpPassword = (bool) preg_match('/^\d{6}$/D', $params['OTP']); $isRecoveryCode = ( static::isRecoveryCodesEnabled() && preg_match(RecoveryCodesTable::CODE_PATTERN, $params['OTP']) ); if ($isCaptchaChecked && ($isOtpPassword || $isRecoveryCode)) { if ($isOtpPassword) $isSuccess = $otp->verify($params['OTP']); elseif ($isRecoveryCode) $isSuccess = RecoveryCodesTable::useCode($otp->getUserId(), $params['OTP']); else $isSuccess = false; if (!$isSuccess) { $otp ->setAttempts($otp->getAttempts() + 1) ->save(); } else { if ($otp->getAttempts() > 0) { // Clear OTP input attempts $otp ->setAttempts(0) ->save(); } if ($isRememberNeeded && $isOtpPassword) { // If user provide otp password (not recovery codes) // Sets cookie for bypass OTP checking $otp->setSkipCookie(); } } } } if ($isSuccess) { static::setDeferredParams(null); } else { // Save a flag which indicates that a form for OTP is required $params[static::REJECTED_KEY] = static::REJECT_BY_CODE; static::setDeferredParams($params); //the OTP form will be shown on the next hit, send the event static::sendEvent($otp); //write to the log ("on" by default) if(Option::get("security", "otp_log") <> "N") { \CSecurityEvent::getInstance()->doLog("SECURITY", "SECURITY_OTP", $otp->getUserId(), ""); } } return $isSuccess; } protected static function sendEvent(Otp $otp) { $code = null; $algo = $otp->getAlgorithm(); //code value only for TOTP if($algo instanceof \Bitrix\Main\Security\Mfa\TotpAlgorithm) { //value based on the current time $timeCode = $algo->timecode(time()); $code = $algo->generateOTP($timeCode); } $eventParams = [ "userId" => $otp->getUserId(), "code" => $code, ]; $event = new \Bitrix\Main\Event("security", "onOtpRequired", $eventParams); $event->send(); } /** * Returns true if user must provide password from device * * @return bool */ public static function isOtpRequired() { return static::getDeferredParams() !== null; } /** * Returns true if user doesn't use OTP, but it required and grace full period ends * * @return bool */ public static function isOtpRequiredByMandatory() { $params = static::getDeferredParams(); if ( !$params || !isset($params[static::REJECTED_KEY]) ) { return false; } return $params[static::REJECTED_KEY] === static::REJECT_BY_MANDATORY; } /** * Return if user must provide captcha code before checking OTP password * * @return bool */ public static function isCaptchaRequired() { $params = static::getDeferredParams(); if (!$params || !isset($params['USER_ID'])) return false; $otp = static::getByUser($params['USER_ID']); return $otp && $otp->isAttemptsReached(); } /** * Return deferred params (see verifyUser) * * @return array|null */ public static function getDeferredParams() { $kernelSession = Application::getInstance()->getKernelSession(); if (isset($kernelSession['BX_SECURITY_OTP']) && is_array($kernelSession['BX_SECURITY_OTP'])) { return $kernelSession['BX_SECURITY_OTP']; } return null; } /** * Set or delete deferred params (see verifyUser) * * @param array|null $params Params, null means deleting params from storage. * @return void */ public static function setDeferredParams($params) { $kernelSession = Application::getInstance()->getKernelSession(); if ($params === null) { unset($kernelSession['BX_SECURITY_OTP']); } else { // Probably we do not need saving password in deferred params // Or need? I don't know right now... if (isset($params['PASSWORD'])) unset($params['PASSWORD']); $kernelSession['BX_SECURITY_OTP'] = $params; } } /** * Set initialization window (in days) for mandatory using checking * * @param int $days Days of initialization window. "0" means immediately (on next user authorization). * @return void */ public static function setSkipMandatoryDays($days = 2) { Option::set('security', 'otp_mandatory_skip_days', (int) $days, null); } /** * Return initialization window (in days) for mandatory using checking * * @return int */ public static function getSkipMandatoryDays() { return (int) Option::get('security', 'otp_mandatory_skip_days'); } /** * Activate or deactivate mandatory OTP using * * @param bool $isMandatory Active or not. * @return void */ public static function setMandatoryUsing($isMandatory = true) { Option::set('security', 'otp_mandatory_using', $isMandatory? 'Y': 'N', null); } /** * Return is mandatory OTP using activated * * @return bool */ public static function isMandatoryUsing() { return (Option::get('security', 'otp_mandatory_using') === 'Y'); } /** * Set user rights who must use OTP in mandatory way * * @param array $rights Needed rights. E.g. ['G1'] for administrators. * @return void */ public static function setMandatoryRights(array $rights) { Option::set('security', 'otp_mandatory_rights', serialize($rights), null); } /** * Return user rights who must use OTP in mandatory way * * @return array */ public static function getMandatoryRights() { $targetRights = Option::get('security', 'otp_mandatory_rights'); $targetRights = unserialize($targetRights, ['allowed_classes' => false]); if (!is_array($targetRights)) $targetRights = array(); return $targetRights; } /** * Set default OtpAlgorithm type * * @param string $value OtpAlgorithm type (see getAvailableTypes). * @throws ArgumentOutOfRangeException * @return void */ public static function setDefaultType($value) { if (!in_array($value, static::$availableTypes)) throw new ArgumentOutOfRangeException('value', static::$availableTypes); Option::set('security', 'otp_default_algo', $value, null); } /** * Return default OtpAlgorithm type * * @return string */ public static function getDefaultType() { return Option::get('security', 'otp_default_algo'); } /** * Return available OtpAlgorithm types * * @return array */ public static function getAvailableTypes() { return static::$availableTypes; } /** * Return available OtpAlgorithm types description * * @return array */ public static function getTypesDescription() { return array( self::TYPE_HOTP => array( 'type' => self::TYPE_HOTP, 'title' => Loc::getMessage('SECURITY_HOTP_TITLE'), 'required_two_code' => true, ), self::TYPE_TOTP => array( 'type' => self::TYPE_TOTP, 'title' => Loc::getMessage('SECURITY_TOTP_TITLE'), 'required_two_code' => false, ) ); } /** * Returns if OTP enabled * * @return bool */ public static function isOtpEnabled() { return (Option::get('security', 'otp_enabled') === 'Y'); } /** * Returns if "Recovery codes" are enabled * * @return bool */ public static function isRecoveryCodesEnabled() { return (Option::get('security', 'otp_allow_recovery_codes') === 'Y'); } }