snapshot of https://github.com/espocrm/espocrm.git (master, 2026-09-18; blobs > 131072B omitted)
Some checks failed
Test / Test on PHP 8.3 (push) Failing after 5s
Test / Test on PHP 8.4 (push) Failing after 4s
Test / Test on PHP 8.5 (push) Failing after 3s
TypeScript static analysis / TypeScript check (push) Failing after 14s
Test Integration on MySQL / Test on PHP 8.3 (push) Failing after 2s
Test Integration on MySQL / Test on PHP 8.5 (push) Failing after 0s
Test Integration on PostgreSQL / Test on PHP 8.4 fix (push) Failing after 1s
Test Integration on PostgreSQL / Test on PHP 8.4 master (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-18 04:19:15 +05:00
commit 7cc7e6b4f3
10077 changed files with 900756 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 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\Tools\OAuthServer\League;
use Espo\Core\Session\Session;
use Espo\Tools\OAuthServer\League\Entities\AuthCodeEntity;
use Espo\Tools\OAuthServer\League\Entities\ClientEntity;
use Espo\Tools\OAuthServer\League\Entities\ScopeEntity;
use InvalidArgumentException;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use League\OAuth2\Server\RequestTypes\AuthorizationRequestInterface;
use RuntimeException;
class AuthorizationRequestStorage
{
public function __construct(
private Session $session,
) {}
public function store(string $clientId, AuthorizationRequestInterface $request): void
{
/** @noinspection PhpConditionAlreadyCheckedInspection */
if (!$request instanceof AuthorizationRequest) {
throw new InvalidArgumentException("Unsupported authorization request instance.");
}
$this->session->set(self::composeSessionKey($clientId), serialize($request));
}
public function get(string $clientId, bool $clear = false): ?AuthorizationRequest
{
$key = self::composeSessionKey($clientId);
$raw = $this->session->get($key);
if (!$raw) {
return null;
}
if ($clear) {
$this->session->clear($key);
}
$request = unserialize($raw, [
'allowed_classes' => [
AuthorizationRequest::class,
ClientEntity::class,
ScopeEntity::class,
AuthCodeEntity::class,
]
]);
if (!$request instanceof AuthorizationRequest) {
throw new RuntimeException("Unserialization error.");
}
return $request;
}
public static function composeSessionKey(string $clientId): string
{
return "oAuthServerAuthorizeRequest_" . $clientId;
}
}

View File

@@ -0,0 +1,132 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 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\Tools\OAuthServer\League;
use Espo\Core\Exceptions\Error;
use Espo\Tools\OAuthServer\League\Repositories\AccessTokenRepository;
use Espo\Tools\OAuthServer\League\Repositories\AuthCodeRepository;
use Espo\Tools\OAuthServer\League\Repositories\ClientRepository;
use Espo\Tools\OAuthServer\League\Repositories\RefreshTokenRepository;
use Espo\Tools\OAuthServer\League\Repositories\ScopeRepository;
use Espo\Tools\OAuthServer\Utils\CryptKeyProvider;
use Exception;
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Grant\AuthCodeGrant;
use League\OAuth2\Server\Grant\RefreshTokenGrant;
class AuthorizationServerFactory
{
public function __construct(
private ClientRepository $clientRepository,
private AccessTokenRepository $accessTokenRepository,
private ScopeRepository $scopeRepository,
private AuthCodeRepository $authCodeRepository,
private RefreshTokenRepository $refreshTokenRepository,
private CryptKeyProvider $cryptKeyProvider,
private ConfigDataProvider $configDataProvider,
) {}
/**
* @throws Error
*/
public function create(): AuthorizationServer
{
$server = new AuthorizationServer(
clientRepository: $this->clientRepository,
accessTokenRepository: $this->accessTokenRepository,
scopeRepository: $this->scopeRepository,
privateKey: $this->createPrivateKeyStub(),
encryptionKey: $this->cryptKeyProvider->getCryptKey(),
);
$server->revokeRefreshTokens(true);
$this->enableAuthorizationCodeGrant($server);
$this->enableRefreshTokenGrant($server);
return $server;
}
/**
* @throws Error
*/
private function enableAuthorizationCodeGrant(AuthorizationServer $server): void
{
try {
$grant = new AuthCodeGrant(
authCodeRepository: $this->authCodeRepository,
refreshTokenRepository: $this->refreshTokenRepository,
authCodeTTL: $this->configDataProvider->getAuthorizationCodeTtl(),
);
} catch (Exception $e) {
throw new Error("Error occurred.", previous: $e);
}
$server->enableGrantType(
grantType: $grant,
accessTokenTTL: $this->configDataProvider->getAccessTokenTtl(),
);
}
/**
* @throws Error
*/
private function enableRefreshTokenGrant(AuthorizationServer $server): void
{
try {
$grant = new RefreshTokenGrant($this->refreshTokenRepository);
} catch (Exception $e) {
throw new Error("Error occurred.", previous: $e);
}
$grant->setRefreshTokenTTL($this->configDataProvider->getRefreshTokenTtl());
$server->enableGrantType(
grantType: $grant,
accessTokenTTL: $this->configDataProvider->getAccessTokenTtl(),
);
}
/**
* Access token encryption is not used. Opaque tokens are used.
*/
private function createPrivateKeyStub(): CryptKey
{
return new class() extends CryptKey {
/**
* @noinspection PhpMissingParentConstructorInspection
*/
public function __construct() {}
};
}
}

View File

@@ -0,0 +1,78 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 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\Tools\OAuthServer\League;
use Espo\Core\Utils\Config;
use DateInterval;
use DateMalformedIntervalStringException;
use RuntimeException;
class ConfigDataProvider
{
private const string AUTHORIZATION_CODE_TTL = 'PT10M';
private const string ACCESS_TOKEN_TTL = 'PT1H';
private const string REFRESH_TOKEN_TTL = 'P1M';
public function __construct(
private Config $config,
) {}
public function getAuthorizationCodeTtl(): DateInterval
{
$interval = $this->config->get('oAuthServer.ttl.authorizationCode') ?? self::AUTHORIZATION_CODE_TTL;
return self::convertIntervalToDateInterval($interval);
}
public function getAccessTokenTtl(): DateInterval
{
$interval = $this->config->get('oAuthServer.ttl.accessToken') ?? self::ACCESS_TOKEN_TTL;
return self::convertIntervalToDateInterval($interval);
}
public function getRefreshTokenTtl(): DateInterval
{
$interval = $this->config->get('oAuthServer.ttl.refreshToken') ?? self::REFRESH_TOKEN_TTL;
return self::convertIntervalToDateInterval($interval);
}
private static function convertIntervalToDateInterval(mixed $interval): DateInterval
{
try {
$value = new DateInterval($interval);
} catch (DateMalformedIntervalStringException $e) {
throw new RuntimeException("Bad interval '$interval'.", previous: $e);
}
return $value;
}
}

View File

@@ -0,0 +1,146 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 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\Tools\OAuthServer\League\Entities;
use DateTimeImmutable;
use Espo\Core\Field\DateTime;
use Espo\Core\Field\Link;
use Espo\Tools\OAuthServer\Entities\AccessToken;
use Espo\Tools\OAuthServer\Utils\IdentifierHasher;
use InvalidArgumentException;
use League\OAuth2\Server\CryptKeyInterface;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
class AccessTokenEntity implements AccessTokenEntityInterface
{
public function __construct(
private AccessToken $entity,
private IdentifierHasher $hasher,
) {}
public function toString(): string
{
return $this->entity->getIdentifier();
}
/**
* @inheritDoc
* @return void
*/
public function setPrivateKey(CryptKeyInterface $privateKey): void
{}
public function __toString()
{
return $this->toString();
}
public function getIdentifier(): string
{
return $this->entity->getIdentifier();
}
public function setIdentifier(string $identifier): void
{
$this->entity->setIdentifier($identifier);
$hash = $this->hasher->hash($identifier);
$this->entity->setHash($hash);
}
public function getExpiryDateTime(): DateTimeImmutable
{
return $this->entity->getExpiresAt()->toDateTime();
}
public function setExpiryDateTime(DateTimeImmutable $dateTime): void
{
$this->entity->setExpiresAt(DateTime::fromDateTime($dateTime));
}
public function setUserIdentifier(string $identifier): void
{
$link = Link::create($identifier);
$this->entity->setUser($link);
}
public function getUserIdentifier(): ?string
{
/** @var non-empty-string */
return $this->entity->getUser()->getId();
}
public function getClient(): ClientEntityInterface
{
return ClientEntity::fromEntity($this->entity->getClient());
}
public function setClient(ClientEntityInterface $client): void
{
/** @noinspection PhpConditionAlreadyCheckedInspection */
if (!$client instanceof ClientEntity) {
throw new InvalidArgumentException("Not supported client entity.");
}
$this->entity->setClient(Link::create($client->entityId));
}
public function addScope(ScopeEntityInterface $scope): void
{
$scopes = $this->entity->getScopes();
if (!in_array($scope->getIdentifier(), $scopes)) {
$scopes[] = $scope->getIdentifier();
}
$this->entity->setScopes($scopes);
}
/**
* @return ScopeEntityInterface[]
*/
public function getScopes(): array
{
$scopes = $this->entity->getScopes();
return array_map(fn ($scope) => new ScopeEntity($scope), $scopes);
}
public function getEntity(): AccessToken
{
return $this->entity;
}
}

View File

@@ -0,0 +1,42 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 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\Tools\OAuthServer\League\Entities;
use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
use League\OAuth2\Server\Entities\Traits\AuthCodeTrait;
use League\OAuth2\Server\Entities\Traits\EntityTrait;
use League\OAuth2\Server\Entities\Traits\TokenEntityTrait;
class AuthCodeEntity implements AuthCodeEntityInterface
{
use EntityTrait;
use AuthCodeTrait;
use TokenEntityTrait;
}

View File

@@ -0,0 +1,87 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 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\Tools\OAuthServer\League\Entities;
use Espo\Tools\OAuthServer\ClientType;
use Espo\Tools\OAuthServer\Entities\Client;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use UnexpectedValueException;
class ClientEntity implements ClientEntityInterface
{
/**
* @param non-empty-string $identifier
* @param string[] $redirectUri
*/
public function __construct(
public string $entityId,
private string $identifier,
private string $name,
private array $redirectUri,
private bool $isConfidential,
) {}
public static function fromEntity(Client $entity): self
{
return new self(
entityId: $entity->getId(),
identifier: $entity->getIdentifier() ?? throw new UnexpectedValueException("No client identifier."),
name: $entity->getName() ?? $entity->getId(),
redirectUri: $entity->getRedirectUris(),
isConfidential: $entity->getClientType() === ClientType::Confidential,
);
}
/**
* @return non-empty-string
*/
public function getIdentifier(): string
{
return $this->identifier;
}
public function getName(): string
{
return $this->name;
}
/**
* @return string[]
*/
public function getRedirectUri(): array
{
return $this->redirectUri;
}
public function isConfidential(): bool
{
return $this->isConfidential;
}
}

View File

@@ -0,0 +1,40 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 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\Tools\OAuthServer\League\Entities;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use League\OAuth2\Server\Entities\Traits\EntityTrait;
use League\OAuth2\Server\Entities\Traits\RefreshTokenTrait;
class RefreshTokenEntity implements RefreshTokenEntityInterface
{
use RefreshTokenTrait;
use EntityTrait;
}

View File

@@ -0,0 +1,53 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 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\Tools\OAuthServer\League\Entities;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Entities\Traits\ScopeTrait;
class ScopeEntity implements ScopeEntityInterface
{
use ScopeTrait;
/**
* @param non-empty-string $identifier
*/
public function __construct(
private string $identifier,
) {}
/**
* @return non-empty-string
*/
public function getIdentifier(): string
{
return $this->identifier;
}
}

View File

@@ -0,0 +1,55 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 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\Tools\OAuthServer\League\Entities;
use Espo\Entities\User;
use League\OAuth2\Server\Entities\UserEntityInterface;
use LogicException;
class UserEntity implements UserEntityInterface
{
public function __construct(
private User $user,
) {
if ($user->isSystem()) {
// Extra safety measure.
throw new LogicException();
}
}
/**
* @return non-empty-string
*/
public function getIdentifier(): string
{
/** @var non-empty-string */
return $this->user->getId();
}
}

View File

@@ -0,0 +1,124 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 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\Tools\OAuthServer\League\Repositories;
use Espo\Core\Field\Link;
use Espo\ORM\EntityManager;
use Espo\Tools\OAuthServer\Entities\AccessToken;
use Espo\Tools\OAuthServer\League\Entities\AccessTokenEntity;
use Espo\Tools\OAuthServer\League\Entities\ClientEntity;
use Espo\Tools\OAuthServer\Repository\AccessTokenRepository as Repository;
use Espo\Tools\OAuthServer\Utils\IdentifierHasher;
use InvalidArgumentException;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use SensitiveParameter;
class AccessTokenRepository implements AccessTokenRepositoryInterface
{
public function __construct(
private EntityManager $entityManager,
private Repository $repository,
private IdentifierHasher $hasher,
) {}
/**
* @param ScopeEntityInterface[] $scopes
*/
public function getNewToken(
ClientEntityInterface $clientEntity,
array $scopes,
?string $userIdentifier = null,
): AccessTokenEntityInterface {
$entity = $this->entityManager->getRDBRepositoryByClass(AccessToken::class)->getNew();
/** @noinspection PhpConditionAlreadyCheckedInspection */
if (!$clientEntity instanceof ClientEntity) {
throw new InvalidArgumentException("Bad client.");
}
if (!$userIdentifier) {
throw new InvalidArgumentException('No user ID.');
}
$entity->setClient(Link::create($clientEntity->entityId));
$accessToken = new AccessTokenEntity($entity, $this->hasher);
foreach ($scopes as $scope) {
$accessToken->addScope($scope);
}
$accessToken->setUserIdentifier($userIdentifier);
return $accessToken;
}
public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity): void
{
/** @noinspection PhpConditionAlreadyCheckedInspection */
if (!$accessTokenEntity instanceof AccessTokenEntity) {
throw new InvalidArgumentException("Not supported access token entity.");
}
$this->entityManager->saveEntity($accessTokenEntity->getEntity());
}
public function revokeAccessToken(#[SensitiveParameter] string $tokenId): void
{
$entity = $this->repository->getActiveByIdentifier($tokenId);
if (!$entity || $entity->isRevoked()) {
return;
}
$entity->setRevoked();
$this->entityManager->saveEntity($entity);
}
/**
* Not existent or inactive token is treated as revoked intentionally.
*/
public function isAccessTokenRevoked(#[SensitiveParameter] string $tokenId): bool
{
$entity = $this->repository->getActiveByIdentifier($tokenId);
if (!$entity) {
return true;
}
// Intentionally.
return !$entity->isActive();
}
}

View File

@@ -0,0 +1,119 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 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\Tools\OAuthServer\League\Repositories;
use Espo\Core\Field\DateTime;
use Espo\Core\Field\Link;
use Espo\ORM\EntityManager;
use Espo\Tools\OAuthServer\Entities\AuthorizationCode;
use Espo\Tools\OAuthServer\League\Entities\AuthCodeEntity;
use Espo\Tools\OAuthServer\Repository\AuthorizationCodeRepository;
use Espo\Tools\OAuthServer\Repository\ClientRepository as ClientRepository;
use Espo\Tools\OAuthServer\Utils\IdentifierHasher;
use InvalidArgumentException;
use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
use RuntimeException;
use SensitiveParameter;
class AuthCodeRepository implements AuthCodeRepositoryInterface
{
public function __construct(
private EntityManager $entityManager,
private AuthorizationCodeRepository $repository,
private ClientRepository $clientRepository,
private IdentifierHasher $hasher,
) {}
public function getNewAuthCode(): AuthCodeEntity
{
return new AuthCodeEntity();
}
public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity): void
{
$entity = $this->entityManager->getRDBRepositoryByClass(AuthorizationCode::class)->getNew();
$userId = $authCodeEntity->getUserIdentifier();
if (!is_string($userId)) {
throw new InvalidArgumentException("User ID must be string.");
}
$scopes = array_map(fn ($scope) => $scope->getIdentifier(), $authCodeEntity->getScopes());
$redirectUri = $authCodeEntity->getRedirectUri() ?? throw new InvalidArgumentException("No redirect URI");
$clientId = $authCodeEntity->getClient()->getIdentifier();
$client = $this->clientRepository->getActiveByIdentifier($clientId);
if (!$client) {
throw new RuntimeException("Client not found.");
}
$hash = $this->hasher->hash($authCodeEntity->getIdentifier());
$entity
->setClient($client)
->setHash($hash)
->setUser(Link::create($userId))
->setScopes($scopes)
->setRedirectUri($redirectUri)
->setExpiresAt(DateTime::fromDateTime($authCodeEntity->getExpiryDateTime()));
$this->entityManager->saveEntity($entity);
}
public function revokeAuthCode(#[SensitiveParameter] string $codeId): void
{
$code = $this->repository->getActiveByIdentifier($codeId);
if (!$code || $code->isRevoked()) {
return;
}
$code->setRevoked();
$this->entityManager->saveEntity($code);
}
public function isAuthCodeRevoked(#[SensitiveParameter] string $codeId): bool
{
$entity = $this->repository->getActiveByIdentifier($codeId);
if (!$entity) {
return true;
}
// Intentionally.
return !$entity->isActive();
}
}

View File

@@ -0,0 +1,70 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 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\Tools\OAuthServer\League\Repositories;
use Espo\Tools\OAuthServer\ClientValidator;
use Espo\Tools\OAuthServer\League\Entities\ClientEntity;
use Espo\Tools\OAuthServer\Repository\ClientRepository as Repository;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
use SensitiveParameter;
class ClientRepository implements ClientRepositoryInterface
{
public function __construct(
private Repository $repository,
private ClientValidator $clientValidator,
) {}
public function getClientEntity(string $clientIdentifier): ?ClientEntity
{
$client = $this->repository->getActiveByIdentifier($clientIdentifier);
if (!$client) {
return null;
}
return ClientEntity::fromEntity($client);
}
public function validateClient(
string $clientIdentifier,
#[SensitiveParameter] ?string $clientSecret,
?string $grantType,
): bool {
$client = $this->repository->getActiveByIdentifier($clientIdentifier);
if (!$client) {
return false;
}
return $this->clientValidator->validate($client, $clientSecret);
}
}

View File

@@ -0,0 +1,122 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 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\Tools\OAuthServer\League\Repositories;
use Espo\Core\Field\DateTime;
use Espo\Core\Field\Link;
use Espo\ORM\EntityManager;
use Espo\Tools\OAuthServer\Entities\RefreshToken;
use Espo\Tools\OAuthServer\League\Entities\AccessTokenEntity;
use Espo\Tools\OAuthServer\League\Entities\RefreshTokenEntity;
use Espo\Tools\OAuthServer\Repository\ClientRepository as ClientRepositoryInternal;
use Espo\Tools\OAuthServer\Repository\RefreshTokenRepository as RefreshTokenRepositoryInternal;
use Espo\Tools\OAuthServer\Utils\IdentifierHasher;
use InvalidArgumentException;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use RuntimeException;
use SensitiveParameter;
class RefreshTokenRepository implements RefreshTokenRepositoryInterface
{
public function __construct(
private EntityManager $entityManager,
private RefreshTokenRepositoryInternal $repository,
private ClientRepositoryInternal $clientRepository,
private IdentifierHasher $hasher,
) {}
public function getNewRefreshToken(): RefreshTokenEntity
{
return new RefreshTokenEntity();
}
public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity): void
{
$entity = $this->entityManager->getRDBRepositoryByClass(RefreshToken::class)->getNew();
$userId = $refreshTokenEntity->getAccessToken()->getUserIdentifier();
if (!is_string($userId)) {
throw new InvalidArgumentException("User ID must be string.");
}
$clientId = $refreshTokenEntity->getAccessToken()->getClient()->getIdentifier();
$client = $this->clientRepository->getActiveByIdentifier($clientId) ??
throw new RuntimeException("Client not found.");
$accessToken = $refreshTokenEntity->getAccessToken();
/** @noinspection PhpConditionAlreadyCheckedInspection */
if (!$accessToken instanceof AccessTokenEntity) {
throw new InvalidArgumentException("Not supported access token implementation.");
}
$hash = $this->hasher->hash($refreshTokenEntity->getIdentifier());
$entity
->setClient($client)
->setAccessToken($accessToken->getEntity())
->setHash($hash)
->setUser(Link::create($userId))
->setExpiresAt(DateTime::fromDateTime($refreshTokenEntity->getExpiryDateTime()));
$this->entityManager->saveEntity($entity);
}
/**
* @todo Delay?
*/
public function revokeRefreshToken(#[SensitiveParameter] string $tokenId): void
{
$token = $this->repository->getActiveByIdentifier($tokenId);
if (!$token || $token->isRevoked()) {
return;
}
$token->setRevoked();
$this->entityManager->saveEntity($token);
}
public function isRefreshTokenRevoked(#[SensitiveParameter] string $tokenId): bool
{
$entity = $this->repository->getActiveByIdentifier($tokenId);
if (!$entity) {
return true;
}
// Intentionally.
return !$entity->isActive();
}
}

View File

@@ -0,0 +1,82 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 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\Tools\OAuthServer\League\Repositories;
use Espo\Tools\OAuthServer\League\Entities\ScopeEntity;
use Espo\Tools\OAuthServer\Scope\UserAvailableScopesFilter;
use Espo\Tools\OAuthServer\Scope\ScopeValidator;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
class ScopeRepository implements ScopeRepositoryInterface
{
public function __construct(
private ScopeValidator $scopeValidator,
private UserAvailableScopesFilter $userAvailableScopesProvider,
) {}
/**
* @param non-empty-string $identifier
*/
public function getScopeEntityByIdentifier(string $identifier): ?ScopeEntity
{
if (!$this->scopeValidator->validate($identifier)) {
return null;
}
return new ScopeEntity($identifier);
}
/**
* @inheritDoc
*/
public function finalizeScopes(
array $scopes,
?string $grantType,
ClientEntityInterface $clientEntity,
?string $userIdentifier = null,
?string $authCodeId = null,
): array {
if (!$userIdentifier) {
return $scopes;
}
$ids = array_map(fn ($it) => $it->getIdentifier(), $scopes);
$availableIds = $this->userAvailableScopesProvider->filter($userIdentifier, $ids);
$scopes = array_filter($scopes, function ($scope) use ($availableIds) {
return in_array($scope->getIdentifier(), $availableIds);
});
return array_values($scopes);
}
}