addDependency('entityManager'); $this->addDependency('user'); $this->addDependency('injectableFactory'); $this->addDependency('metadata'); $this->addDependency('totp'); $this->addDependency('config'); } protected function getUser() { return $this->getInjection('user'); } protected function getEntityManager() { return $this->getInjection('entityManager'); } public function read(string $id) { if (!$this->getUser()->isAdmin() && $id !== $this->getUser()->id) throw new Forbidden(); $user = $this->getEntityManager()->getEntity('User', $id); if (!$user) throw new NotFound(); if (!$user->isAdmin() && !$user->isRegular()) throw new Forbidden(); $userData = $this->getEntityManager()->getRepository('UserData')->getByUserId($id); return (object) [ 'auth2FA' => $userData->get('auth2FA'), 'auth2FAMethod' => $userData->get('auth2FAMethod'), ]; } public function generate2FAData(string $id, $data) { if (!$this->getUser()->isAdmin() && $id !== $this->getUser()->id) throw new Forbidden(); $user = $this->getEntityManager()->getEntity('User', $id); if (!$user) throw new NotFound(); if (!$user->isAdmin() && !$user->isRegular()) throw new Forbidden(); $password = $data->password ?? null; if (!$password) throw new Forbidden('Passport required.'); if (!$this->getUser()->isAdmin() || $this->getUser()->id === $id) { $this->checkPassport($id, $password); } $userData = $this->getEntityManager()->getRepository('UserData')->getByUserId($id); $auth2FAMethod = $data->auth2FAMethod ?? null; if (!$auth2FAMethod) throw new BadRequest(); $className = $this->getInjection('metadata')->get( ['app', 'auth2FAMethods', $auth2FAMethod, 'implementationUserClassName'] ); if ($className) { $impl = $this->getInjection('injectableFactory')->createByClassName($className); $generatedData = $impl->generateData($userData, $data); } else { $methodName = 'generate2FAData' . $auth2FAMethod; $generatedData = $this->$methodName($userData, $data); } $userData->set($generatedData); if (!empty($data->reset)) { $userData->set('auth2FA', false); $userData->set('auth2FAMethod', null); } $this->getEntityManager()->saveEntity($userData); return $generatedData; } public function update(string $id, $data) { if (!$this->getUser()->isAdmin() && $id !== $this->getUser()->id) throw new Forbidden(); $user = $this->getEntityManager()->getEntity('User', $id); if (!$user) throw new NotFound(); if (!$user->isAdmin() && !$user->isRegular()) throw new Forbidden(); $userData = $this->getEntityManager()->getRepository('UserData')->getByUserId($id); $originalData = clone $data; $password = $originalData->password ?? null; if (!$password) throw new Forbidden('Passport required.'); if (!$this->getUser()->isAdmin() || $this->getUser()->id === $id) { $this->checkPassport($id, $password); } foreach (get_object_vars($data) as $attribute => $v) { if (!in_array($attribute, ['auth2FA', 'auth2FAMethod'])) { unset($data->$attribute); } } $userData->set($data); if (!$userData->get('auth2FA')) { $userData->set('auth2FAMethod', null); } if ($userData->get('auth2FA') && $userData->isAttributeChanged('auth2FA')) { if (!$this->getInjection('config')->get('auth2FA')) { throw new Forbidden('2FA is not enabled.'); } } if ( $userData->get('auth2FA') && $userData->get('auth2FAMethod') && ($userData->isAttributeChanged('auth2FA') || $userData->isAttributeChanged('auth2FAMethod')) ) { $auth2FAMethod = $userData->get('auth2FAMethod'); if (!in_array($auth2FAMethod, $this->getInjection('config')->get('auth2FAMethodList', []))) { throw new Forbidden('Not allowed 2FA auth method.'); } $className = $this->getInjection('metadata')->get( ['app', 'auth2FAMethods', $auth2FAMethod, 'implementationUserClassName'] ); $verifyResult = true; if ($className) { $impl = $this->getInjection('injectableFactory')->createByClassName($className); if (method_exists($impl, 'verify')) { $verifyResult = $impl->verify($userData, $originalData); } } else { $methodName = 'verify2FA' . $auth2FAMethod; if (method_exists($this, $methodName)) { $verifyResult = $this->$methodName($userData, $originalData); } } if (!$verifyResult) { throw new Forbidden('Not verified.'); } } $this->getEntityManager()->saveEntity($userData); $returnData = (object) [ 'auth2FA' => $userData->get('auth2FA'), 'auth2FAMethod' => $userData->get('auth2FAMethod'), ]; return $returnData; } protected function verify2FATotp(\Espo\Entities\UserData $userData, $data) : bool { $code = $data->code ?? null; if (!$code) return false; $code = str_replace(' ', '', trim($code)); $secret = $userData->get('auth2FATotpSecret'); return $this->getInjection('totp')->verifyCode($secret, $code); } protected function generate2FADataTotp(\Espo\Entities\UserData $userData, $data) { $secret = $this->getInjection('totp')->createSecret(); return (object) [ 'auth2FATotpSecret' => $secret, ]; } protected function checkPassport(string $id, string $password) { $passwordHash = new \Espo\Core\Utils\PasswordHash($this->getConfig()); $user = $this->getEntityManager()->getRepository('User')->where([ 'id' => $id, 'password' => $passwordHash->hash($password), ])->findOne(); if (!$user) { throw new Forbidden('Passport is incorrect.'); } } }