auth token manager
This commit is contained in:
@@ -43,6 +43,7 @@ use Espo\Core\{
|
||||
Api\ErrorOutput as ApiErrorOutput,
|
||||
Api\RequestWrapper,
|
||||
Api\ResponseWrapper,
|
||||
Authentication\AuthToken\AuthTokenManager,
|
||||
};
|
||||
|
||||
use Slim\{
|
||||
@@ -64,19 +65,22 @@ class EntryPoint implements ApplicationRunner
|
||||
protected $entityManager;
|
||||
protected $clientManager;
|
||||
protected $applicationUser;
|
||||
protected $authTokenManager;
|
||||
|
||||
public function __construct(
|
||||
InjectableFactory $injectableFactory,
|
||||
EntryPointManager $entryPointManager,
|
||||
EntityManager $entityManager,
|
||||
ClientManager $clientManager,
|
||||
ApplicationUser $applicationUser
|
||||
ApplicationUser $applicationUser,
|
||||
AuthTokenManager $authTokenManager
|
||||
) {
|
||||
$this->injectableFactory = $injectableFactory;
|
||||
$this->entryPointManager = $entryPointManager;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->clientManager = $clientManager;
|
||||
$this->applicationUser = $applicationUser;
|
||||
$this->authTokenManager = $authTokenManager;
|
||||
}
|
||||
|
||||
public function run(?StdClass $params = null)
|
||||
@@ -169,15 +173,14 @@ class EntryPoint implements ApplicationRunner
|
||||
return $_GET['portalId'];
|
||||
}
|
||||
|
||||
if (!empty($_COOKIE['auth-token'])) {
|
||||
$token = $this->entityManager
|
||||
->getRepository('AuthToken')
|
||||
->where(['token' => $_COOKIE['auth-token']])
|
||||
->findOne();
|
||||
if (empty($_COOKIE['auth-token'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($token && $token->get('portalId')) {
|
||||
return $token->get('portalId');
|
||||
}
|
||||
$authToken = $this->authTokenManager->get($_COOKIE['auth-token']);
|
||||
|
||||
if ($authToken) {
|
||||
return $authToken->getPortalId();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* 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 General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Authentication\AuthToken;
|
||||
|
||||
/**
|
||||
* An auth token record.
|
||||
*/
|
||||
interface AuthToken
|
||||
{
|
||||
/**
|
||||
* Get a token.
|
||||
*/
|
||||
public function getToken() : string;
|
||||
|
||||
/**
|
||||
* Get a user ID.
|
||||
*/
|
||||
public function getUserId() : string;
|
||||
|
||||
/**
|
||||
* Get a portal ID. If a token belongs to a specific portal.
|
||||
*/
|
||||
public function getPortalId() : ?string;
|
||||
|
||||
/**
|
||||
* Get a token secret. Secret is used as an additional security check.
|
||||
*/
|
||||
public function getSecret() : ?string;
|
||||
|
||||
/**
|
||||
* Whether a token is active.
|
||||
*/
|
||||
public function isActive() : bool;
|
||||
|
||||
/**
|
||||
* Get a password hash. If a password hash is not stored in token, then return NULL.
|
||||
* If you store auth tokens remotely it's reasonable to avoid hashes being sent.
|
||||
*/
|
||||
public function getHash() : ?string;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* 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 General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Authentication\AuthToken;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* An auth token data. Used for auth token creation.
|
||||
*/
|
||||
class AuthTokenData
|
||||
{
|
||||
private $userId;
|
||||
|
||||
private $portalId = null;
|
||||
|
||||
private $hash = null;
|
||||
|
||||
private $ipAddress = null;
|
||||
|
||||
private $createSecret = false;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function getUserId() : string
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function getPortalId() : ?string
|
||||
{
|
||||
return $this->portalId;
|
||||
}
|
||||
|
||||
public function getHash() : ?string
|
||||
{
|
||||
return $this->hash;
|
||||
}
|
||||
|
||||
public function getIpAddress() : ?string
|
||||
{
|
||||
return $this->ipAddress;
|
||||
}
|
||||
|
||||
public function toCreateSecret() : bool
|
||||
{
|
||||
return $this->createSecret;
|
||||
}
|
||||
|
||||
public static function create(array $data) : self
|
||||
{
|
||||
$object = new self();
|
||||
|
||||
$object->userId = $data['userId'] ?? null;
|
||||
$object->portalId = $data['portalId'] ?? null;
|
||||
$object->hash = $data['hash'] ?? null;
|
||||
$object->ipAddress = $data['ipAddress'] ?? null;
|
||||
$object->createSecret = $data['createSecret'] ?? false;
|
||||
|
||||
$object->validate();
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
protected function validate()
|
||||
{
|
||||
// @todo Use typed properties when php 7.4 is a min supported version.
|
||||
if (
|
||||
isset($this->userId) && !is_string($this->userId) ||
|
||||
isset($this->portalId) && !is_string($this->portalId) ||
|
||||
isset($this->hash) && !is_string($this->hash) ||
|
||||
isset($this->ipAddress) && !is_string($this->ipAddress) ||
|
||||
!is_bool($this->createSecret)
|
||||
) {
|
||||
throw new RuntimeException("Invalid data.");
|
||||
}
|
||||
|
||||
if (!$this->userId) {
|
||||
throw new RuntimeException("No user ID.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* 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 General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Authentication\AuthToken;
|
||||
|
||||
/**
|
||||
* Fetches and stores auth tokens.
|
||||
*/
|
||||
interface AuthTokenManager
|
||||
{
|
||||
/**
|
||||
* Get an auth token. If does not exist then returns NULL.
|
||||
*/
|
||||
public function get(string $token) : ?AuthToken;
|
||||
|
||||
/**
|
||||
* Create an auth token and store it.
|
||||
*/
|
||||
public function create(AuthTokenData $authTokenData) : AuthToken;
|
||||
|
||||
/**
|
||||
* Make an auth token inactive (invalid).
|
||||
*/
|
||||
public function inactivate(AuthToken $authToken);
|
||||
|
||||
/**
|
||||
* Update a last access date. An implementation can be omitted to avoid a writing operation.
|
||||
*/
|
||||
public function renew(AuthToken $authToken);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* 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 General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Authentication\AuthToken;
|
||||
|
||||
use Espo\{
|
||||
ORM\EntityManager,
|
||||
};
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
use const MCRYPT_DEV_URANDOM;
|
||||
|
||||
class EspoAuthTokenManager implements AuthTokenManager
|
||||
{
|
||||
protected $entityManager;
|
||||
|
||||
protected $repository;
|
||||
|
||||
const TOKEN_RANDOM_LENGTH = 16;
|
||||
|
||||
public function __construct(EntityManager $entityManager)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
|
||||
$this->repository = $entityManager->getRepository('AuthToken');
|
||||
}
|
||||
|
||||
public function get(string $token) : ?AuthToken
|
||||
{
|
||||
$authToken = $this->entityManager
|
||||
->getRepository('AuthToken')
|
||||
->select([
|
||||
'id',
|
||||
'isActive',
|
||||
'token',
|
||||
'secret',
|
||||
'userId',
|
||||
'portalId',
|
||||
'hash',
|
||||
'createdAt',
|
||||
'lastAccess',
|
||||
'modifiedAt',
|
||||
])
|
||||
->where([
|
||||
'token' => $token,
|
||||
])
|
||||
->findOne();
|
||||
|
||||
return $authToken;
|
||||
}
|
||||
|
||||
public function create(AuthTokenData $authTokenData) : AuthToken
|
||||
{
|
||||
$authToken = $this->repository->getNew();
|
||||
|
||||
$authToken->set([
|
||||
'userId' => $authTokenData->getUserId(),
|
||||
'portalId' => $authTokenData->getPortalId(),
|
||||
'hash' => $authTokenData->getHash(),
|
||||
'ipAddress' => $authTokenData->getIpAddress(),
|
||||
'lastAccess' => date('Y-m-d H:i:s'),
|
||||
'token' => $this->generateToken(),
|
||||
]);
|
||||
|
||||
if ($authTokenData->toCreateSecret()) {
|
||||
$authToken->set('secret', $this->generateToken());
|
||||
}
|
||||
|
||||
$this->validate($authToken);
|
||||
|
||||
$this->repository->save($authToken);
|
||||
|
||||
return $authToken;
|
||||
}
|
||||
|
||||
public function inactivate(AuthToken $authToken)
|
||||
{
|
||||
$this->validateNotChanged($authToken);
|
||||
|
||||
$authToken->set('isActive', false);
|
||||
|
||||
$this->repository->save($authToken);
|
||||
}
|
||||
|
||||
public function renew(AuthToken $authToken)
|
||||
{
|
||||
$this->validateNotChanged($authToken);
|
||||
|
||||
if ($authToken->isNew()) {
|
||||
throw new RuntimeException("Can renew only not new auth token.");
|
||||
}
|
||||
|
||||
$authToken->set('lastAccess', date('Y-m-d H:i:s'));
|
||||
|
||||
$this->repository->save($authToken);
|
||||
}
|
||||
|
||||
protected function validate(AuthToken $authToken)
|
||||
{
|
||||
if (!$authToken->getToken()) {
|
||||
throw new RuntimeException("Empty token.");
|
||||
}
|
||||
|
||||
if (!$authToken->getUserId()) {
|
||||
throw new RuntimeException("Empty user ID.");
|
||||
}
|
||||
}
|
||||
|
||||
protected function validateNotChanged(AuthToken $authToken)
|
||||
{
|
||||
if (
|
||||
$authToken->isAttributeChanged('token') ||
|
||||
$authToken->isAttributeChanged('secret') ||
|
||||
$authToken->isAttributeChanged('hash') ||
|
||||
$authToken->isAttributeChanged('userId') ||
|
||||
$authToken->isAttributeChanged('portalId')
|
||||
) {
|
||||
throw new RuntimeException("Auth token was changed.");
|
||||
}
|
||||
}
|
||||
|
||||
protected function generateToken()
|
||||
{
|
||||
$length = self::TOKEN_RANDOM_LENGTH;
|
||||
|
||||
if (function_exists('random_bytes')) {
|
||||
return bin2hex(random_bytes($length));
|
||||
}
|
||||
|
||||
if (function_exists('mcrypt_create_iv')) {
|
||||
return bin2hex(mcrypt_create_iv($length, MCRYPT_DEV_URANDOM));
|
||||
}
|
||||
|
||||
if (function_exists('openssl_random_pseudo_bytes')) {
|
||||
return bin2hex(openssl_random_pseudo_bytes($length));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,8 @@ use Espo\Core\Authentication\{
|
||||
Result,
|
||||
LoginFactory,
|
||||
TwoFactor\Factory as TwoFAFactory,
|
||||
AuthToken\AuthTokenManager,
|
||||
AuthToken\AuthTokenData,
|
||||
};
|
||||
|
||||
use Espo\Core\{
|
||||
@@ -89,7 +91,8 @@ class Authentication
|
||||
Metadata $metadata,
|
||||
EntityManagerProxy $entityManagerProxy,
|
||||
LoginFactory $authLoginFactory,
|
||||
TwoFAFactory $auth2FAFactory
|
||||
TwoFAFactory $auth2FAFactory,
|
||||
AuthTokenManager $authTokenManager
|
||||
) {
|
||||
$this->allowAnyAccess = $allowAnyAccess;
|
||||
|
||||
@@ -100,6 +103,7 @@ class Authentication
|
||||
$this->entityManager = $entityManagerProxy;
|
||||
$this->authLoginFactory = $authLoginFactory;
|
||||
$this->auth2FAFactory = $auth2FAFactory;
|
||||
$this->authTokenManager = $authTokenManager;
|
||||
}
|
||||
|
||||
protected function getDefaultAuthenticationMethod()
|
||||
@@ -160,15 +164,12 @@ class Authentication
|
||||
$authTokenIsFound = false;
|
||||
|
||||
if (!$authenticationMethod) {
|
||||
$authToken = $this->entityManager
|
||||
->getRepository('AuthToken')
|
||||
->where(['token' => $password])
|
||||
->findOne();
|
||||
$authToken = $this->authTokenManager->get($password);
|
||||
|
||||
if ($authToken && $authToken->get('secret')) {
|
||||
if ($authToken && $authToken->getSecret()) {
|
||||
$sentSecret = $request->getCookieParam('auth-token-secret');
|
||||
|
||||
if ($sentSecret !== $authToken->get('secret')) {
|
||||
if ($sentSecret !== $authToken->getSecret()) {
|
||||
$authToken = null;
|
||||
}
|
||||
}
|
||||
@@ -178,15 +179,15 @@ class Authentication
|
||||
$authTokenIsFound = true;
|
||||
}
|
||||
|
||||
if ($authToken && $authToken->get('isActive')) {
|
||||
if ($authToken && $authToken->isActive()) {
|
||||
if (!$this->allowAnyAccess) {
|
||||
if ($this->isPortal() && $authToken->get('portalId') !== $this->getPortal()->id) {
|
||||
if ($this->isPortal() && $authToken->getPortalId() !== $this->getPortal()->id) {
|
||||
$GLOBALS['log']->info("AUTH: Trying to login to portal with a token not related to portal.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$this->isPortal() && $authToken->get('portalId')) {
|
||||
if (!$this->isPortal() && $authToken->getPortalId()) {
|
||||
$GLOBALS['log']->info("AUTH: Trying to login to crm with a token related to portal.");
|
||||
|
||||
return null;
|
||||
@@ -194,8 +195,8 @@ class Authentication
|
||||
}
|
||||
|
||||
if ($this->allowAnyAccess) {
|
||||
if ($authToken->get('portalId') && !$this->isPortal()) {
|
||||
$portal = $this->entityManager->getEntity('Portal', $authToken->get('portalId'));
|
||||
if ($authToken->getPortalId() && !$this->isPortal()) {
|
||||
$portal = $this->entityManager->getEntity('Portal', $authToken->getPortalId());
|
||||
|
||||
if ($portal) {
|
||||
$this->setPortal($portal);
|
||||
@@ -265,17 +266,15 @@ class Authentication
|
||||
if (!$result->isSecondStepRequired() && $request->getHeader('Espo-Authorization')) {
|
||||
if (!$authToken) {
|
||||
$authToken = $this->createAuthToken($user, $request);
|
||||
} else {
|
||||
$this->authTokenManager->renew($authToken);
|
||||
}
|
||||
|
||||
$authToken->set('lastAccess', date('Y-m-d H:i:s'));
|
||||
|
||||
$this->entityManager->saveEntity($authToken);
|
||||
|
||||
$user->set('token', $authToken->get('token'));
|
||||
$user->set('authTokenId', $authToken->id);
|
||||
$user->set('token', $authToken->getToken());
|
||||
$user->set('authTokenId', $authToken->id ?? null);
|
||||
|
||||
if ($authLogRecord) {
|
||||
$authLogRecord->set('authTokenId', $authToken->id);
|
||||
$authLogRecord->set('authTokenId', $authToken->id ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,10 +282,11 @@ class Authentication
|
||||
$this->entityManager->saveEntity($authLogRecord);
|
||||
}
|
||||
|
||||
if ($authToken && !$authLogRecord) {
|
||||
if ($authToken && !$authLogRecord && isset($authToken->id)) {
|
||||
$authLogRecord = $this->entityManager
|
||||
->getRepository('AuthLogRecord')
|
||||
->select(['id'])->where([
|
||||
->select(['id'])
|
||||
->where([
|
||||
'authTokenId' => $authToken->id
|
||||
])
|
||||
->order('requestTime', true)
|
||||
@@ -450,38 +450,35 @@ class Authentication
|
||||
|
||||
protected function createAuthToken(User $user, Request $request) : AuthToken
|
||||
{
|
||||
$createTokenSecret = $request->getHeader('Espo-Authorization-Create-Token-Secret') === 'true';
|
||||
$createSecret = $request->getHeader('Espo-Authorization-Create-Token-Secret') === 'true';
|
||||
|
||||
if ($createTokenSecret) {
|
||||
if ($createSecret) {
|
||||
if ($this->config->get('authTokenSecretDisabled')) {
|
||||
$createTokenSecret = false;
|
||||
$createSecret = false;
|
||||
}
|
||||
}
|
||||
|
||||
$authToken = $this->entityManager->getEntity('AuthToken');
|
||||
$arrayData = [
|
||||
'hash' => $user->get('password'),
|
||||
'ipAddress' => $request->getServerParam('REMOTE_ADDR'),
|
||||
'userId' => $user->id,
|
||||
'portalId' => $this->isPortal() ? $this->getPortal()->id : null,
|
||||
'createSecret' => $createSecret,
|
||||
];
|
||||
|
||||
$token = $this->generateToken();
|
||||
$authToken = $this->authTokenManager->create(
|
||||
AuthTokenData::create($arrayData)
|
||||
);
|
||||
|
||||
$authToken->set('token', $token);
|
||||
$authToken->set('hash', $user->get('password'));
|
||||
$authToken->set('ipAddress', $request->getServerParam('REMOTE_ADDR'));
|
||||
$authToken->set('userId', $user->id);
|
||||
|
||||
if ($createTokenSecret) {
|
||||
$secret = $this->generateToken();
|
||||
$authToken->set('secret', $secret);
|
||||
|
||||
$this->setSecretInCookie($secret);
|
||||
}
|
||||
|
||||
if ($this->isPortal()) {
|
||||
$authToken->set('portalId', $this->getPortal()->id);
|
||||
if ($createSecret) {
|
||||
$this->setSecretInCookie($authToken->getSecret());
|
||||
}
|
||||
|
||||
if ($this->config->get('authTokenPreventConcurrent')) {
|
||||
$concurrentAuthTokenList = $this->entityManager
|
||||
->getRepository('AuthToken')
|
||||
->select(['id'])->where([
|
||||
->select(['id'])
|
||||
->where([
|
||||
'userId' => $user->id,
|
||||
'isActive' => true,
|
||||
])
|
||||
@@ -497,48 +494,25 @@ class Authentication
|
||||
return $authToken;
|
||||
}
|
||||
|
||||
protected function generateToken()
|
||||
{
|
||||
$length = 16;
|
||||
|
||||
if (function_exists('random_bytes')) {
|
||||
return bin2hex(random_bytes($length));
|
||||
}
|
||||
|
||||
if (function_exists('mcrypt_create_iv')) {
|
||||
return bin2hex(mcrypt_create_iv($length, \MCRYPT_DEV_URANDOM));
|
||||
}
|
||||
|
||||
if (function_exists('openssl_random_pseudo_bytes')) {
|
||||
return bin2hex(openssl_random_pseudo_bytes($length));
|
||||
}
|
||||
}
|
||||
|
||||
public function destroyAuthToken(string $token, Request $request)
|
||||
{
|
||||
$authToken = $this->entityManager
|
||||
->getRepository('AuthToken')
|
||||
->select(['id', 'isActive', 'secret'])
|
||||
->where(
|
||||
['token' => $token]
|
||||
)
|
||||
->findOne();
|
||||
$authToken = $this->authTokenManager->get($token);
|
||||
|
||||
if ($authToken) {
|
||||
$authToken->set('isActive', false);
|
||||
|
||||
$this->entityManager->saveEntity($authToken);
|
||||
|
||||
if ($authToken->get('secret')) {
|
||||
$sentSecret = $request->getCookieParam('auth-token-secret');
|
||||
|
||||
if ($sentSecret === $authToken->get('secret')) {
|
||||
$this->setSecretInCookie(null);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
if (!$authToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->authTokenManager->inactivate($authToken);
|
||||
|
||||
if ($authToken->getSecret()) {
|
||||
$sentSecret = $request->getCookieParam('auth-token-secret');
|
||||
|
||||
if ($sentSecret === $authToken->getSecret()) {
|
||||
$this->setSecretInCookie(null);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function createAuthLogRecord(
|
||||
|
||||
@@ -29,15 +29,11 @@
|
||||
|
||||
namespace Espo\Core\Authentication\Login;
|
||||
|
||||
use Espo\Entities\{
|
||||
User,
|
||||
AuthToken,
|
||||
};
|
||||
|
||||
use Espo\Core\{
|
||||
Api\Request,
|
||||
ORM\EntityManager,
|
||||
Authentication\Result,
|
||||
Authentication\AuthToken\AuthToken,
|
||||
};
|
||||
|
||||
class ApiKey implements Login
|
||||
@@ -53,11 +49,13 @@ class ApiKey implements Login
|
||||
{
|
||||
$apiKey = $request->getHeader('X-Api-Key');
|
||||
|
||||
$user = $this->entityManager->getRepository('User')->where([
|
||||
'type' => 'api',
|
||||
'apiKey' => $apiKey,
|
||||
'authMethod' => 'ApiKey',
|
||||
])->findOne();
|
||||
$user = $this->entityManager->getRepository('User')
|
||||
->where([
|
||||
'type' => 'api',
|
||||
'apiKey' => $apiKey,
|
||||
'authMethod' => 'ApiKey',
|
||||
])
|
||||
->findOne();
|
||||
|
||||
if (!$user) {
|
||||
return Result::fail();
|
||||
|
||||
@@ -29,16 +29,12 @@
|
||||
|
||||
namespace Espo\Core\Authentication\Login;
|
||||
|
||||
use Espo\Entities\{
|
||||
User,
|
||||
AuthToken,
|
||||
};
|
||||
|
||||
use Espo\Core\{
|
||||
ORM\EntityManager,
|
||||
Api\Request,
|
||||
Utils\PasswordHash,
|
||||
Authentication\Result,
|
||||
Authentication\AuthToken\AuthToken,
|
||||
};
|
||||
|
||||
class Espo implements Login
|
||||
@@ -54,26 +50,30 @@ class Espo implements Login
|
||||
|
||||
public function login(?string $username, ?string $password, ?AuthToken $authToken = null, ?Request $request = null) : Result
|
||||
{
|
||||
if (!$password) return Result::fail('Empty password');
|
||||
if (!$password) {
|
||||
return Result::fail('Empty password');
|
||||
}
|
||||
|
||||
if ($authToken) {
|
||||
$hash = $authToken->get('hash');
|
||||
$hash = $authToken->getHash();
|
||||
} else {
|
||||
$hash = $this->passwordHash->hash($password);
|
||||
}
|
||||
|
||||
$user = $this->entityManager->getRepository('User')->where([
|
||||
'userName' => $username,
|
||||
'password' => $hash,
|
||||
'type!=' => ['api', 'system'],
|
||||
])->findOne();
|
||||
$user = $this->entityManager->getRepository('User')
|
||||
->where([
|
||||
'userName' => $username,
|
||||
'password' => $hash,
|
||||
'type!=' => ['api', 'system'],
|
||||
])
|
||||
->findOne();
|
||||
|
||||
if (!$user) {
|
||||
return Result::fail();
|
||||
}
|
||||
|
||||
if ($authToken) {
|
||||
if ($user->id !== $authToken->get('userId')) {
|
||||
if ($user->id !== $authToken->getUserId()) {
|
||||
return Result::fail('User and token mismatch');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,17 +29,13 @@
|
||||
|
||||
namespace Espo\Core\Authentication\Login;
|
||||
|
||||
use Espo\Entities\{
|
||||
User,
|
||||
AuthToken,
|
||||
};
|
||||
|
||||
use Espo\Core\{
|
||||
ORM\EntityManager,
|
||||
Api\Request,
|
||||
Utils\Config,
|
||||
Utils\ApiKey,
|
||||
Authentication\Result,
|
||||
Authentication\AuthToken\AuthToken,
|
||||
};
|
||||
|
||||
class Hmac implements Login
|
||||
@@ -59,18 +55,23 @@ class Hmac implements Login
|
||||
|
||||
list($apiKey, $hash) = explode(':', $authString, 2);
|
||||
|
||||
$user = $this->entityManager->getRepository('User')->where([
|
||||
'type' => 'api',
|
||||
'apiKey' => $apiKey,
|
||||
'authMethod' => 'Hmac',
|
||||
])->findOne();
|
||||
$user = $this->entityManager->getRepository('User')
|
||||
->where([
|
||||
'type' => 'api',
|
||||
'apiKey' => $apiKey,
|
||||
'authMethod' => 'Hmac',
|
||||
])
|
||||
->findOne();
|
||||
|
||||
if (!$user) {
|
||||
return Result::fail();
|
||||
}
|
||||
|
||||
$secretKey = (new ApiKey($this->config))->getSecretKeyForUserId($user->id);
|
||||
if (!$secretKey) return null;
|
||||
|
||||
if (!$secretKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$string = $request->getMethod() . ' ' . $request->getResourcePath();
|
||||
|
||||
|
||||
@@ -31,11 +31,6 @@ namespace Espo\Core\Authentication\Login;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
use Espo\Entities\{
|
||||
User,
|
||||
AuthToken,
|
||||
};
|
||||
|
||||
use Espo\Core\{
|
||||
ORM\EntityManager,
|
||||
Api\Request,
|
||||
@@ -46,6 +41,7 @@ use Espo\Core\{
|
||||
Authentication\Result,
|
||||
Authentication\LDAP\Utils as LDAPUtils,
|
||||
Authentication\LDAP\Client as LDAPClient,
|
||||
Authentication\AuthToken\AuthToken,
|
||||
};
|
||||
|
||||
use Exception;
|
||||
@@ -122,6 +118,7 @@ class LDAP extends Espo
|
||||
|
||||
if ($isPortal) {
|
||||
$useLdapAuthForPortalUser = $this->utils->getOption('portalUserLdapAuth');
|
||||
|
||||
if (!$useLdapAuthForPortalUser) {
|
||||
return parent::login($username, $password, $authToken, $request);
|
||||
}
|
||||
@@ -232,7 +229,8 @@ class LDAP extends Espo
|
||||
return null;
|
||||
}
|
||||
|
||||
$userId = $authToken->get('userId');
|
||||
$userId = $authToken->getUserId();
|
||||
|
||||
$user = $this->entityManager->getEntity('User', $userId);
|
||||
|
||||
$tokenUsername = $user->get('userName');
|
||||
|
||||
@@ -29,14 +29,10 @@
|
||||
|
||||
namespace Espo\Core\Authentication\Login;
|
||||
|
||||
use Espo\Entities\{
|
||||
User,
|
||||
AuthToken,
|
||||
};
|
||||
|
||||
use Espo\Core\{
|
||||
Api\Request,
|
||||
Authentication\Result,
|
||||
Authentication\AuthToken\AuthToken,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,36 +29,58 @@
|
||||
|
||||
namespace Espo\Core\Console\Commands;
|
||||
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
use Espo\Core\{
|
||||
ORM\EntityManager,
|
||||
Authentication\AuthToken\AuthTokenManager,
|
||||
};
|
||||
|
||||
class AuthTokenCheck implements Command
|
||||
{
|
||||
protected $entityManager;
|
||||
protected $authTokenManager;
|
||||
|
||||
public function __construct(EntityManager $entityManager)
|
||||
public function __construct(EntityManager $entityManager, AuthTokenManager $authTokenManager)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->authTokenManager = $authTokenManager;
|
||||
}
|
||||
|
||||
public function run(array $options, array $flagList, array $argumentList) : ?string
|
||||
{
|
||||
$token = $argumentList[0] ?? null;
|
||||
if (empty($token)) return null;
|
||||
|
||||
$entityManager = $this->entityManager;
|
||||
if (empty($token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$authToken = $entityManager->getRepository('AuthToken')->where([
|
||||
'token' => $token,
|
||||
'isActive' => true,
|
||||
])->findOne();
|
||||
$authToken = $this->authTokenManager->get($token);
|
||||
|
||||
if (!$authToken) return null;
|
||||
if (!$authToken->get('userId')) return null;
|
||||
if (!$authToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$userId = $authToken->get('userId');
|
||||
if (!$authToken->isActive()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = $entityManager->getEntity('User', $userId);
|
||||
if (!$user) return null;
|
||||
if (!$authToken->getUserId()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$userId = $authToken->getUserId();
|
||||
|
||||
$user = $this->entityManager
|
||||
->getRepository('User')
|
||||
->select('id')
|
||||
->where([
|
||||
'id' => $userId,
|
||||
'isActive' => true,
|
||||
])
|
||||
->findOne();
|
||||
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $user->id;
|
||||
}
|
||||
|
||||
@@ -25,12 +25,44 @@
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Entities;
|
||||
|
||||
class AuthToken extends \Espo\Core\ORM\Entity
|
||||
use Espo\Core\{
|
||||
ORM\Entity as BaseEntity,
|
||||
Authentication\AuthToken\AuthToken as AuthTokenInterface,
|
||||
};
|
||||
|
||||
class AuthToken extends BaseEntity implements AuthTokenInterface
|
||||
{
|
||||
public function getToken() : string
|
||||
{
|
||||
return $this->get('token');
|
||||
}
|
||||
|
||||
public function getUserId() : string
|
||||
{
|
||||
return $this->get('userId');
|
||||
}
|
||||
|
||||
public function getPortalId() : ?string
|
||||
{
|
||||
return $this->get('portalId');
|
||||
}
|
||||
|
||||
public function getSecret() : ?string
|
||||
{
|
||||
return $this->get('secret');
|
||||
}
|
||||
|
||||
public function isActive() : bool
|
||||
{
|
||||
return $this->get('isActive');
|
||||
}
|
||||
|
||||
public function getHash() : ?string
|
||||
{
|
||||
return $this->get('hash');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"authTokenManager": {
|
||||
"className": "Espo\\Core\\Authentication\\AuthToken\\EspoAuthTokenManager"
|
||||
},
|
||||
"schema": {
|
||||
"className": "Espo\\Core\\Utils\\Database\\Schema\\Schema"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* 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 General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace tests\integration\Espo\Core\Formula;
|
||||
|
||||
use Espo\Core\Authentication\AuthToken\AuthTokenData;
|
||||
|
||||
class AuthTokenManagerTest extends \tests\integration\Core\BaseTestCase
|
||||
{
|
||||
public function testCreateWithSecret()
|
||||
{
|
||||
$authTokenManager = $this->getContainer()->get('authTokenManager');
|
||||
|
||||
$authTokenData = AuthTokenData::create([
|
||||
'hash' => 'test-hash',
|
||||
'ipAddress' => 'ip-address',
|
||||
'userId' => 'user-id',
|
||||
'portalId' => 'portal-id',
|
||||
'createSecret' => true,
|
||||
]);
|
||||
|
||||
$authToken = $authTokenManager->create($authTokenData);
|
||||
|
||||
$this->assertEquals($authTokenData->getHash(), $authToken->getHash());
|
||||
$this->assertEquals($authTokenData->getUserId(), $authToken->getUserId());
|
||||
$this->assertEquals($authTokenData->getPortalId(), $authToken->getPortalId());
|
||||
|
||||
$this->assertTrue($authToken->isActive());
|
||||
|
||||
$this->assertNotEmpty($authToken->getToken());
|
||||
$this->assertNotEmpty($authToken->getSecret());
|
||||
|
||||
$this->assertNotEmpty($authToken->get('lastAccess'));
|
||||
}
|
||||
|
||||
public function testCreateWithNoSecretNoPortal()
|
||||
{
|
||||
$authTokenManager = $this->getContainer()->get('authTokenManager');
|
||||
|
||||
$authTokenData = AuthTokenData::create([
|
||||
'hash' => 'test-hash',
|
||||
'ipAddress' => 'ip-address',
|
||||
'userId' => 'user-id',
|
||||
'portalId' => null,
|
||||
'createSecret' => false,
|
||||
]);
|
||||
|
||||
$authToken = $authTokenManager->create($authTokenData);
|
||||
|
||||
$this->assertEquals($authTokenData->getHash(), $authToken->getHash());
|
||||
$this->assertEquals($authTokenData->getUserId(), $authToken->getUserId());
|
||||
|
||||
$this->assertEmpty($authToken->getPortalId());
|
||||
|
||||
$this->assertNotEmpty($authToken->getToken());
|
||||
$this->assertEmpty($authToken->getSecret());
|
||||
}
|
||||
|
||||
public function testRenew()
|
||||
{
|
||||
$authTokenManager = $this->getContainer()->get('authTokenManager');
|
||||
|
||||
$authTokenData = AuthTokenData::create([
|
||||
'hash' => 'test-hash',
|
||||
'userId' => 'user-id',
|
||||
]);
|
||||
|
||||
$authToken = $authTokenManager->create($authTokenData);
|
||||
|
||||
$authToken = $authTokenManager->get($authToken->getToken());
|
||||
|
||||
$authTokenManager->renew($authToken);
|
||||
|
||||
$this->assertNotEmpty($authToken->get('lastAccess'));
|
||||
}
|
||||
|
||||
public function testInactivate()
|
||||
{
|
||||
$authTokenManager = $this->getContainer()->get('authTokenManager');
|
||||
|
||||
$authTokenData = AuthTokenData::create([
|
||||
'hash' => 'test-hash',
|
||||
'userId' => 'user-id',
|
||||
]);
|
||||
|
||||
$authToken = $authTokenManager->create($authTokenData);
|
||||
|
||||
$authToken = $authTokenManager->get($authToken->getToken());
|
||||
|
||||
$authTokenManager->inactivate($authToken);
|
||||
|
||||
$this->assertFalse($authToken->isActive());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* 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 General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace tests\unit\Espo\Core\Authentication\AuthToken;
|
||||
|
||||
use Espo\Core\Authentication\AuthToken\AuthTokenData;
|
||||
|
||||
class AuthTokenDataTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testCreate()
|
||||
{
|
||||
$authTokenData = AuthTokenData::create([
|
||||
'hash' => 'hash',
|
||||
'ipAddress' => 'ip-address',
|
||||
'userId' => 'user-id',
|
||||
'portalId' => 'portal-id',
|
||||
'createSecret' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals('hash', $authTokenData->getHash());
|
||||
$this->assertEquals('ip-address', $authTokenData->getIpAddress());
|
||||
$this->assertEquals('user-id', $authTokenData->getUserId());
|
||||
$this->assertEquals('portal-id', $authTokenData->getPortalId());
|
||||
$this->assertTrue($authTokenData->toCreateSecret());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user