. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ namespace Espo\Core\Authentication\AuthToken; use Espo\Core\Name\Field; use Espo\ORM\EntityCollection; use Espo\ORM\EntityManager; use Espo\ORM\Name\Attribute; use Espo\ORM\Repository\RDBRepository; use Espo\Entities\AuthToken as AuthTokenEntity; use Random\RandomException; use RuntimeException; /** * A default auth token manager. Auth tokens are stored in database. * Consider creating a custom implementation if you need to store auth tokens * in another storage. E.g. a single Redis data store can be utilized with * multiple Espo replicas (for scalability purposes). * Defined at metadata > app > containerServices > authTokenManager. * * @implements Manager */ class EspoManager implements Manager { /** @var RDBRepository */ private RDBRepository $repository; private const int TOKEN_RANDOM_LENGTH = 16; public function __construct(EntityManager $entityManager) { $this->repository = $entityManager->getRDBRepositoryByClass(AuthTokenEntity::class); } public function get(string $token): ?AuthToken { return $this->repository ->select([ Attribute::ID, 'isActive', 'token', 'secret', 'userId', 'portalId', 'hash', AuthTokenEntity::FIELD_PASSWORD_VERSION, Field::CREATED_AT, 'lastAccess', Field::MODIFIED_AT, ]) ->where(['token' => $token]) ->findOne(); } public function create(Data $data): AuthToken { $authToken = $this->repository->getNew(); $authToken ->setUserId($data->getUserId()) ->setPortalId($data->getPortalId()) ->setPasswordVersion($data->getPasswordVersion()) ->setIpAddress($data->getIpAddress()) ->setToken($this->generateToken()) ->setLastAccessNow(); if ($data->toCreateSecret()) { $authToken->setSecret($this->generateToken()); } $this->validate($authToken); $this->repository->save($authToken); return $authToken; } /** * @param AuthTokenEntity $authToken */ public function inactivate(AuthToken $authToken): void { $this->validateNotChanged($authToken); $authToken->setIsActive(false); $this->repository->save($authToken); } public function renew(AuthToken $authToken): void { $this->validateNotChanged($authToken); if ($authToken->isNew()) { throw new RuntimeException("Can renew only not new auth token."); } $authToken->setLastAccessNow(); $this->repository->save($authToken); } public function inactiveOther(AuthToken $authToken): void { foreach ($this->findConcurrent($authToken) as $other) { $this->inactivate($other); } } private function validate(AuthToken $authToken): void { if (!$authToken->getToken()) { throw new RuntimeException("Empty token."); } if (!$authToken->getUserId()) { throw new RuntimeException("Empty user ID."); } } private function validateNotChanged(AuthTokenEntity $authToken): void { if ( $authToken->isAttributeChanged('token') || $authToken->isAttributeChanged('secret') || $authToken->isAttributeChanged('hash') || $authToken->isAttributeChanged('userId') || $authToken->isAttributeChanged('portalId') ) { throw new RuntimeException("Auth token was changed."); } } private function generateToken(): string { $length = self::TOKEN_RANDOM_LENGTH; try { return bin2hex(random_bytes($length)); } catch (RandomException $e) { throw new RuntimeException(previous: $e); } } /** * @return EntityCollection */ private function findConcurrent(AuthTokenEntity $authToken): EntityCollection { return $this->repository ->select([Attribute::ID]) ->where([ AuthTokenEntity::ATTR_USER_ID => $authToken->getUserId(), AuthTokenEntity::FIELD_IS_ACTIVE => true, Attribute::ID . '!=' => $authToken->getId(), ]) ->find(); } }