mirror of
https://github.com/elyby/accounts.git
synced 2025-05-31 14:11:46 +05:30
Move OAuth module from API to common and solve PHPStan's errors
This commit is contained in:
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2;
|
||||
|
||||
use Carbon\CarbonInterval;
|
||||
use DateInterval;
|
||||
use League\OAuth2\Server\AuthorizationServer;
|
||||
use yii\base\Component as BaseComponent;
|
||||
|
||||
final class Component extends BaseComponent {
|
||||
|
||||
private ?AuthorizationServer $_authServer = null;
|
||||
|
||||
public function getAuthServer(): AuthorizationServer {
|
||||
if ($this->_authServer === null) {
|
||||
$this->_authServer = $this->createAuthServer();
|
||||
}
|
||||
|
||||
return $this->_authServer;
|
||||
}
|
||||
|
||||
private function createAuthServer(): AuthorizationServer {
|
||||
$clientsRepo = new Repositories\ClientRepository();
|
||||
$accessTokensRepo = new Repositories\AccessTokenRepository();
|
||||
$publicScopesRepo = new Repositories\PublicScopeRepository();
|
||||
$internalScopesRepo = new Repositories\InternalScopeRepository();
|
||||
$authCodesRepo = new Repositories\AuthCodeRepository();
|
||||
$refreshTokensRepo = new Repositories\RefreshTokenRepository();
|
||||
|
||||
$accessTokenTTL = CarbonInterval::create(-1); // Set negative value to make tokens non expiring
|
||||
|
||||
$authServer = new AuthorizationServer(
|
||||
$clientsRepo,
|
||||
$accessTokensRepo,
|
||||
new Repositories\EmptyScopeRepository(),
|
||||
new Keys\EmptyKey(),
|
||||
'', // Omit the key because we use our own encryption mechanism
|
||||
new ResponseTypes\BearerTokenResponse(),
|
||||
);
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$authCodeGrant = new Grants\AuthCodeGrant($authCodesRepo, $refreshTokensRepo, new DateInterval('PT10M'));
|
||||
$authCodeGrant->disableRequireCodeChallengeForPublicClients();
|
||||
$authServer->enableGrantType($authCodeGrant, $accessTokenTTL);
|
||||
$authCodeGrant->setScopeRepository($publicScopesRepo); // Change repository after enabling
|
||||
|
||||
$refreshTokenGrant = new Grants\RefreshTokenGrant($refreshTokensRepo);
|
||||
$authServer->enableGrantType($refreshTokenGrant, $accessTokenTTL);
|
||||
$refreshTokenGrant->setScopeRepository($publicScopesRepo); // Change repository after enabling
|
||||
|
||||
$clientCredentialsGrant = new Grants\ClientCredentialsGrant();
|
||||
$authServer->enableGrantType($clientCredentialsGrant, $accessTokenTTL);
|
||||
$clientCredentialsGrant->setScopeRepository($internalScopesRepo); // Change repository after enabling
|
||||
|
||||
return $authServer;
|
||||
}
|
||||
|
||||
}
|
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2;
|
||||
|
||||
use LogicException;
|
||||
use RangeException;
|
||||
use SodiumException;
|
||||
use Yii;
|
||||
|
||||
/**
|
||||
* This trait is intended to override the standard data encryption behavior
|
||||
* with the help of \Defuse\Crypto\Crypto class, because the resultant string
|
||||
* is much larger than the original one.
|
||||
*
|
||||
* The implementation under the hood relies on using libsodium library
|
||||
* that provides more compact result values.
|
||||
*/
|
||||
trait CryptTrait {
|
||||
|
||||
protected function encrypt($unencryptedData): string {
|
||||
return Yii::$app->tokens->encryptValue($unencryptedData);
|
||||
}
|
||||
|
||||
protected function decrypt($encryptedData): string {
|
||||
try {
|
||||
return Yii::$app->tokens->decryptValue($encryptedData);
|
||||
} catch (SodiumException|RangeException $e) {
|
||||
throw new LogicException($e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Entities;
|
||||
|
||||
use League\OAuth2\Server\CryptKeyInterface;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\Traits\EntityTrait;
|
||||
use League\OAuth2\Server\Entities\Traits\TokenEntityTrait;
|
||||
use Yii;
|
||||
|
||||
final class AccessTokenEntity implements AccessTokenEntityInterface {
|
||||
use EntityTrait;
|
||||
use TokenEntityTrait;
|
||||
|
||||
public function toString(): string {
|
||||
return Yii::$app->tokensFactory->createForOAuthClient($this)->toString();
|
||||
}
|
||||
|
||||
public function setPrivateKey(CryptKeyInterface $privateKey): void {
|
||||
// We use a general-purpose component to build JWT tokens, so there is no need to keep the key
|
||||
}
|
||||
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\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;
|
||||
|
||||
}
|
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Entities;
|
||||
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\Traits\ClientTrait;
|
||||
use League\OAuth2\Server\Entities\Traits\EntityTrait;
|
||||
|
||||
class ClientEntity implements ClientEntityInterface {
|
||||
use EntityTrait;
|
||||
use ClientTrait;
|
||||
|
||||
/**
|
||||
* @param non-empty-string $id
|
||||
* @param string|string[] $redirectUri
|
||||
*/
|
||||
public function __construct(
|
||||
string $id,
|
||||
string $name,
|
||||
string|array $redirectUri,
|
||||
private readonly bool $isTrusted,
|
||||
) {
|
||||
$this->identifier = $id;
|
||||
$this->name = $name;
|
||||
$this->redirectUri = $redirectUri;
|
||||
}
|
||||
|
||||
public function isConfidential(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isTrusted(): bool {
|
||||
return $this->isTrusted;
|
||||
}
|
||||
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Entities;
|
||||
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Entities\Traits\EntityTrait;
|
||||
use League\OAuth2\Server\Entities\Traits\ScopeTrait;
|
||||
|
||||
class ScopeEntity implements ScopeEntityInterface {
|
||||
use EntityTrait;
|
||||
use ScopeTrait;
|
||||
|
||||
public function __construct(string $id) {
|
||||
$this->identifier = $id;
|
||||
}
|
||||
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Entities;
|
||||
|
||||
use League\OAuth2\Server\Entities\Traits\EntityTrait;
|
||||
use League\OAuth2\Server\Entities\UserEntityInterface;
|
||||
|
||||
class UserEntity implements UserEntityInterface {
|
||||
use EntityTrait;
|
||||
|
||||
public function __construct(int $id) {
|
||||
$this->identifier = (string)$id;
|
||||
}
|
||||
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Events;
|
||||
|
||||
use League\OAuth2\Server\EventEmitting\AbstractEvent;
|
||||
|
||||
class RequestedRefreshToken extends AbstractEvent {
|
||||
|
||||
}
|
@@ -1,65 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Grants;
|
||||
|
||||
use api\components\OAuth2\CryptTrait;
|
||||
use api\components\OAuth2\Events\RequestedRefreshToken;
|
||||
use api\components\OAuth2\Repositories\PublicScopeRepository;
|
||||
use DateInterval;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
|
||||
use League\OAuth2\Server\Grant\AuthCodeGrant as BaseAuthCodeGrant;
|
||||
use League\OAuth2\Server\RequestEvent;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use yii\helpers\StringHelper;
|
||||
|
||||
class AuthCodeGrant extends BaseAuthCodeGrant {
|
||||
use CryptTrait;
|
||||
|
||||
/**
|
||||
* @param DateInterval $accessTokenTTL
|
||||
* @param ClientEntityInterface $client
|
||||
* @param string|null $userIdentifier
|
||||
* @param ScopeEntityInterface[] $scopes
|
||||
*
|
||||
* @return AccessTokenEntityInterface
|
||||
* @throws OAuthServerException
|
||||
* @throws UniqueTokenIdentifierConstraintViolationException
|
||||
*/
|
||||
protected function issueAccessToken(
|
||||
DateInterval $accessTokenTTL,
|
||||
ClientEntityInterface $client,
|
||||
?string $userIdentifier,
|
||||
array $scopes = [],
|
||||
): AccessTokenEntityInterface {
|
||||
foreach ($scopes as $i => $scope) {
|
||||
if ($scope->getIdentifier() === PublicScopeRepository::OFFLINE_ACCESS) {
|
||||
unset($scopes[$i]);
|
||||
$this->getEmitter()->emit(new RequestedRefreshToken('refresh_token_requested'));
|
||||
}
|
||||
}
|
||||
|
||||
return parent::issueAccessToken($accessTokenTTL, $client, $userIdentifier, $scopes);
|
||||
}
|
||||
|
||||
protected function validateRedirectUri(
|
||||
string $redirectUri,
|
||||
ClientEntityInterface $client,
|
||||
ServerRequestInterface $request,
|
||||
): void {
|
||||
$allowedRedirectUris = (array)$client->getRedirectUri();
|
||||
foreach ($allowedRedirectUris as $allowedRedirectUri) {
|
||||
if (StringHelper::startsWith($redirectUri, $allowedRedirectUri)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
|
||||
throw OAuthServerException::invalidClient($request);
|
||||
}
|
||||
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Grants;
|
||||
|
||||
use api\components\OAuth2\CryptTrait;
|
||||
use League\OAuth2\Server\Grant\ClientCredentialsGrant as BaseClientCredentialsGrant;
|
||||
|
||||
class ClientCredentialsGrant extends BaseClientCredentialsGrant {
|
||||
use CryptTrait;
|
||||
|
||||
}
|
@@ -1,131 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Grants;
|
||||
|
||||
use api\components\OAuth2\CryptTrait;
|
||||
use api\components\Tokens\TokenReader;
|
||||
use Carbon\FactoryImmutable;
|
||||
use common\models\OauthSession;
|
||||
use InvalidArgumentException;
|
||||
use Lcobucci\JWT\Validation\Constraint\LooseValidAt;
|
||||
use Lcobucci\JWT\Validation\Validator;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
|
||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||
use League\OAuth2\Server\Grant\RefreshTokenGrant as BaseRefreshTokenGrant;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Throwable;
|
||||
use Yii;
|
||||
|
||||
class RefreshTokenGrant extends BaseRefreshTokenGrant {
|
||||
use CryptTrait;
|
||||
|
||||
/**
|
||||
* Previously, refresh tokens were stored in Redis.
|
||||
* If received refresh token is matches the legacy token template,
|
||||
* restore the information from the legacy storage.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param string $clientId
|
||||
*
|
||||
* @return array
|
||||
* @throws OAuthServerException
|
||||
*/
|
||||
protected function validateOldRefreshToken(ServerRequestInterface $request, string $clientId): array {
|
||||
$refreshToken = $this->getRequestParameter('refresh_token', $request);
|
||||
if ($refreshToken !== null && mb_strlen($refreshToken) === 40) {
|
||||
return $this->validateLegacyRefreshToken($refreshToken);
|
||||
}
|
||||
|
||||
return $this->validateAccessToken($refreshToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently we're not rotating refresh tokens.
|
||||
* So we're overriding this method to always return null, which means,
|
||||
* that refresh_token will not be issued.
|
||||
*
|
||||
* @param AccessTokenEntityInterface $accessToken
|
||||
*
|
||||
* @return RefreshTokenEntityInterface|null
|
||||
*/
|
||||
protected function issueRefreshToken(AccessTokenEntityInterface $accessToken): ?RefreshTokenEntityInterface {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $refreshToken
|
||||
* @return array
|
||||
* @throws OAuthServerException
|
||||
*/
|
||||
private function validateLegacyRefreshToken(string $refreshToken): array {
|
||||
$result = Yii::$app->redis->get("oauth:refresh:tokens:{$refreshToken}");
|
||||
if ($result === null) {
|
||||
throw OAuthServerException::invalidRefreshToken('Token has been revoked');
|
||||
}
|
||||
|
||||
try {
|
||||
[
|
||||
'access_token_id' => $accessTokenId,
|
||||
'session_id' => $sessionId,
|
||||
] = json_decode((string)$result, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (Throwable $e) {
|
||||
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e);
|
||||
}
|
||||
|
||||
/** @var OauthSession|null $relatedSession */
|
||||
$relatedSession = OauthSession::findOne(['legacy_id' => $sessionId]);
|
||||
if ($relatedSession === null) {
|
||||
throw OAuthServerException::invalidRefreshToken('Token has been revoked');
|
||||
}
|
||||
|
||||
return [
|
||||
'client_id' => $relatedSession->client_id,
|
||||
'refresh_token_id' => $refreshToken,
|
||||
'access_token_id' => $accessTokenId,
|
||||
'scopes' => $relatedSession->getScopes(),
|
||||
'user_id' => $relatedSession->account_id,
|
||||
'expire_time' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* client_id: string,
|
||||
* refresh_token_id?: string,
|
||||
* access_token_id?: string,
|
||||
* scopes: list<string>|null,
|
||||
* user_id: string|null,
|
||||
* expire_time: int|null,
|
||||
* }
|
||||
* @throws OAuthServerException
|
||||
*/
|
||||
private function validateAccessToken(string $jwt): array {
|
||||
try {
|
||||
$token = Yii::$app->tokens->parse($jwt);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e);
|
||||
}
|
||||
|
||||
if (!Yii::$app->tokens->verify($token)) {
|
||||
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token');
|
||||
}
|
||||
|
||||
if (!(new Validator())->validate($token, new LooseValidAt(FactoryImmutable::getDefaultInstance()))) {
|
||||
throw OAuthServerException::invalidRefreshToken('Token has expired');
|
||||
}
|
||||
|
||||
$reader = new TokenReader($token);
|
||||
|
||||
return [
|
||||
'client_id' => $reader->getClientId(),
|
||||
'refresh_token_id' => '', // This value used only to invalidate old token
|
||||
'access_token_id' => '', // This value used only to invalidate old token
|
||||
'scopes' => $reader->getScopes(),
|
||||
'user_id' => $reader->getAccountId(),
|
||||
'expire_time' => null,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Keys;
|
||||
|
||||
use League\OAuth2\Server\CryptKeyInterface;
|
||||
|
||||
class EmptyKey implements CryptKeyInterface {
|
||||
|
||||
public function getKeyPath(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getPassPhrase(): ?string {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getKeyContents(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
}
|
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Repositories;
|
||||
|
||||
use api\components\OAuth2\Entities\AccessTokenEntity;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
|
||||
|
||||
class AccessTokenRepository implements AccessTokenRepositoryInterface {
|
||||
|
||||
/**
|
||||
* Create a new access token
|
||||
*
|
||||
* @param ClientEntityInterface $clientEntity
|
||||
* @param \League\OAuth2\Server\Entities\ScopeEntityInterface[] $scopes
|
||||
* @param mixed $userIdentifier
|
||||
*
|
||||
* @return AccessTokenEntityInterface
|
||||
*/
|
||||
public function getNewToken(
|
||||
ClientEntityInterface $clientEntity,
|
||||
array $scopes,
|
||||
$userIdentifier = null,
|
||||
): AccessTokenEntityInterface {
|
||||
$accessToken = new AccessTokenEntity();
|
||||
$accessToken->setClient($clientEntity);
|
||||
array_map($accessToken->addScope(...), $scopes);
|
||||
if ($userIdentifier !== null) {
|
||||
$accessToken->setUserIdentifier($userIdentifier);
|
||||
}
|
||||
|
||||
return $accessToken;
|
||||
}
|
||||
|
||||
public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity): void {
|
||||
// We don't store access tokens, so there's no need to do anything here
|
||||
}
|
||||
|
||||
public function revokeAccessToken($tokenId): void {
|
||||
// We don't store access tokens, so there's no need to do anything here
|
||||
}
|
||||
|
||||
public function isAccessTokenRevoked($tokenId): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Repositories;
|
||||
|
||||
use api\components\OAuth2\Entities\AuthCodeEntity;
|
||||
use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
|
||||
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
|
||||
|
||||
class AuthCodeRepository implements AuthCodeRepositoryInterface {
|
||||
|
||||
public function getNewAuthCode(): AuthCodeEntityInterface {
|
||||
return new AuthCodeEntity();
|
||||
}
|
||||
|
||||
public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity): void {
|
||||
}
|
||||
|
||||
public function revokeAuthCode($codeId): void {
|
||||
}
|
||||
|
||||
public function isAuthCodeRevoked($codeId): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Repositories;
|
||||
|
||||
use api\components\OAuth2\Entities\ClientEntity;
|
||||
use common\models\OauthClient;
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
|
||||
|
||||
class ClientRepository implements ClientRepositoryInterface {
|
||||
|
||||
public function getClientEntity($clientId): ?ClientEntityInterface {
|
||||
$client = $this->findModel($clientId);
|
||||
if ($client === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ClientEntity($client->id, $client->name, $client->redirect_uri ?? '', (bool)$client->is_trusted);
|
||||
}
|
||||
|
||||
public function validateClient($clientId, $clientSecret, $grantType): bool {
|
||||
$client = $this->findModel($clientId);
|
||||
if ($client === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($client->type !== OauthClient::TYPE_APPLICATION) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($clientSecret !== null && $clientSecret !== $client->secret) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function findModel(string $id): ?OauthClient {
|
||||
$client = OauthClient::findOne(['id' => $id]);
|
||||
if ($client === null || $client->type !== OauthClient::TYPE_APPLICATION) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $client;
|
||||
}
|
||||
|
||||
}
|
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Repositories;
|
||||
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
|
||||
|
||||
/**
|
||||
* In our application we use separate scopes repositories for different grants.
|
||||
* To create an instance of the authorization server, you need to pass the scopes
|
||||
* repository. This class acts as a dummy to meet this requirement.
|
||||
*/
|
||||
class EmptyScopeRepository implements ScopeRepositoryInterface {
|
||||
|
||||
public function getScopeEntityByIdentifier($identifier): ?ScopeEntityInterface {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function finalizeScopes(
|
||||
array $scopes,
|
||||
$grantType,
|
||||
ClientEntityInterface $clientEntity,
|
||||
$userIdentifier = null,
|
||||
?string $authCodeId = null,
|
||||
): array {
|
||||
return $scopes;
|
||||
}
|
||||
|
||||
}
|
@@ -1,64 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Repositories;
|
||||
|
||||
use api\components\OAuth2\Entities\ClientEntity;
|
||||
use api\components\OAuth2\Entities\ScopeEntity;
|
||||
use api\rbac\Permissions as P;
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
|
||||
|
||||
class InternalScopeRepository implements ScopeRepositoryInterface {
|
||||
|
||||
private const array ALLOWED_SCOPES = [
|
||||
P::CHANGE_ACCOUNT_USERNAME,
|
||||
P::CHANGE_ACCOUNT_PASSWORD,
|
||||
P::BLOCK_ACCOUNT,
|
||||
P::OBTAIN_EXTENDED_ACCOUNT_INFO,
|
||||
P::ESCAPE_IDENTITY_VERIFICATION,
|
||||
];
|
||||
|
||||
private const array PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS = [
|
||||
'internal_account_info' => P::OBTAIN_EXTENDED_ACCOUNT_INFO,
|
||||
];
|
||||
|
||||
public function getScopeEntityByIdentifier($identifier): ?ScopeEntityInterface {
|
||||
$identifier = $this->convertToInternalPermission($identifier);
|
||||
if (!in_array($identifier, self::ALLOWED_SCOPES, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ScopeEntity($identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OAuthServerException
|
||||
*/
|
||||
public function finalizeScopes(
|
||||
array $scopes,
|
||||
$grantType,
|
||||
ClientEntityInterface $clientEntity,
|
||||
$userIdentifier = null,
|
||||
?string $authCodeId = null,
|
||||
): array {
|
||||
if (empty($scopes)) {
|
||||
return $scopes;
|
||||
}
|
||||
|
||||
/** @var ClientEntity $clientEntity */
|
||||
// Right now we have no available scopes for the client_credentials grant
|
||||
if (!$clientEntity->isTrusted()) {
|
||||
throw OAuthServerException::invalidScope($scopes[0]->getIdentifier());
|
||||
}
|
||||
|
||||
return $scopes;
|
||||
}
|
||||
|
||||
private function convertToInternalPermission(string $publicScope): string {
|
||||
return self::PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS[$publicScope] ?? $publicScope;
|
||||
}
|
||||
|
||||
}
|
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Repositories;
|
||||
|
||||
use api\components\OAuth2\Entities\ScopeEntity;
|
||||
use api\rbac\Permissions as P;
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
|
||||
|
||||
class PublicScopeRepository implements ScopeRepositoryInterface {
|
||||
|
||||
public const string OFFLINE_ACCESS = 'offline_access';
|
||||
public const string CHANGE_SKIN = 'change_skin';
|
||||
|
||||
private const string ACCOUNT_INFO = 'account_info';
|
||||
private const string ACCOUNT_EMAIL = 'account_email';
|
||||
|
||||
private const array PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS = [
|
||||
self::ACCOUNT_INFO => P::OBTAIN_OWN_ACCOUNT_INFO,
|
||||
self::ACCOUNT_EMAIL => P::OBTAIN_ACCOUNT_EMAIL,
|
||||
];
|
||||
|
||||
private const array ALLOWED_SCOPES = [
|
||||
P::OBTAIN_OWN_ACCOUNT_INFO,
|
||||
P::OBTAIN_ACCOUNT_EMAIL,
|
||||
P::MINECRAFT_SERVER_SESSION,
|
||||
self::OFFLINE_ACCESS,
|
||||
self::CHANGE_SKIN,
|
||||
];
|
||||
|
||||
public function getScopeEntityByIdentifier($identifier): ?ScopeEntityInterface {
|
||||
$identifier = $this->convertToInternalPermission($identifier);
|
||||
if (!in_array($identifier, self::ALLOWED_SCOPES, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ScopeEntity($identifier);
|
||||
}
|
||||
|
||||
public function finalizeScopes(
|
||||
array $scopes,
|
||||
$grantType,
|
||||
ClientEntityInterface $clientEntity,
|
||||
$userIdentifier = null,
|
||||
?string $authCodeId = null,
|
||||
): array {
|
||||
return $scopes;
|
||||
}
|
||||
|
||||
private function convertToInternalPermission(string $publicScope): string {
|
||||
return self::PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS[$publicScope] ?? $publicScope;
|
||||
}
|
||||
|
||||
}
|
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Repositories;
|
||||
|
||||
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
|
||||
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
|
||||
|
||||
class RefreshTokenRepository implements RefreshTokenRepositoryInterface {
|
||||
|
||||
public function getNewRefreshToken(): ?RefreshTokenEntityInterface {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity): void {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
public function revokeRefreshToken($tokenId): void {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
public function isRefreshTokenRevoked($tokenId): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\ResponseTypes;
|
||||
|
||||
use api\components\OAuth2\CryptTrait;
|
||||
use League\OAuth2\Server\ResponseTypes\BearerTokenResponse as BaseBearerTokenResponse;
|
||||
|
||||
class BearerTokenResponse extends BaseBearerTokenResponse {
|
||||
use CryptTrait;
|
||||
|
||||
}
|
@@ -30,6 +30,9 @@ final readonly class TokenReader {
|
||||
return $this->token->claims()->get('client_id', false) ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>|null
|
||||
*/
|
||||
public function getScopes(): ?array {
|
||||
$scopes = $this->token->claims()->get('scope', false);
|
||||
if ($scopes !== false) {
|
||||
|
@@ -26,9 +26,6 @@ return [
|
||||
'user' => [
|
||||
'class' => api\components\User\Component::class,
|
||||
],
|
||||
'oauth' => [
|
||||
'class' => api\components\OAuth2\Component::class,
|
||||
],
|
||||
'tokens' => [
|
||||
'class' => api\components\Tokens\Component::class,
|
||||
'privateKeyPath' => getenv('JWT_PRIVATE_KEY_PATH') ?: __DIR__ . '/../../data/certs/private.pem',
|
||||
|
@@ -9,10 +9,20 @@ use api\rbac\Permissions as P;
|
||||
use GuzzleHttp\Psr7\ServerRequest;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Yii;
|
||||
use yii\base\Module;
|
||||
use yii\filters\AccessControl;
|
||||
use yii\helpers\ArrayHelper;
|
||||
|
||||
class AuthorizationController extends Controller {
|
||||
final class AuthorizationController extends Controller {
|
||||
|
||||
public function __construct(
|
||||
string $id,
|
||||
Module $module,
|
||||
private readonly OauthProcess $oauthProcess,
|
||||
array $config = [],
|
||||
) {
|
||||
parent::__construct($id, $module, $config);
|
||||
}
|
||||
|
||||
public function behaviors(): array {
|
||||
return ArrayHelper::merge(Controller::behaviors(), [
|
||||
@@ -45,19 +55,15 @@ class AuthorizationController extends Controller {
|
||||
}
|
||||
|
||||
public function actionValidate(): array {
|
||||
return $this->createOauthProcess()->validate($this->getServerRequest());
|
||||
return $this->oauthProcess->validate($this->getServerRequest());
|
||||
}
|
||||
|
||||
public function actionComplete(): array {
|
||||
return $this->createOauthProcess()->complete($this->getServerRequest());
|
||||
return $this->oauthProcess->complete($this->getServerRequest());
|
||||
}
|
||||
|
||||
public function actionToken(): array {
|
||||
return $this->createOauthProcess()->getToken($this->getServerRequest());
|
||||
}
|
||||
|
||||
private function createOauthProcess(): OauthProcess {
|
||||
return new OauthProcess(Yii::$app->oauth->getAuthServer());
|
||||
return $this->oauthProcess->getToken($this->getServerRequest());
|
||||
}
|
||||
|
||||
private function getServerRequest(): ServerRequestInterface {
|
||||
|
@@ -3,9 +3,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\modules\oauth\models;
|
||||
|
||||
use api\components\OAuth2\Entities\UserEntity;
|
||||
use api\components\OAuth2\Events\RequestedRefreshToken;
|
||||
use api\rbac\Permissions as P;
|
||||
use common\components\OAuth2\Entities\UserEntity;
|
||||
use common\components\OAuth2\Events\RequestedRefreshToken;
|
||||
use common\models\Account;
|
||||
use common\models\OauthClient;
|
||||
use common\models\OauthSession;
|
||||
@@ -18,14 +18,16 @@ use Psr\Http\Message\ServerRequestInterface;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Yii;
|
||||
|
||||
class OauthProcess {
|
||||
final readonly class OauthProcess {
|
||||
|
||||
private const array INTERNAL_PERMISSIONS_TO_PUBLIC_SCOPES = [
|
||||
P::OBTAIN_OWN_ACCOUNT_INFO => 'account_info',
|
||||
P::OBTAIN_ACCOUNT_EMAIL => 'account_email',
|
||||
];
|
||||
|
||||
public function __construct(private readonly AuthorizationServer $server) {
|
||||
public function __construct(
|
||||
private AuthorizationServer $server,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,8 +45,7 @@ class OauthProcess {
|
||||
*
|
||||
* In addition, you can pass the description value to override the application's description.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return array
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function validate(ServerRequestInterface $request): array {
|
||||
try {
|
||||
@@ -77,8 +78,7 @@ class OauthProcess {
|
||||
* If the field is present, it will be interpreted as any value resulting in false positives.
|
||||
* Otherwise, the value will be interpreted as "true".
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return array
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function complete(ServerRequestInterface $request): array {
|
||||
try {
|
||||
@@ -144,8 +144,7 @@ class OauthProcess {
|
||||
* grant_type,
|
||||
* ]
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return array
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function getToken(ServerRequestInterface $request): array {
|
||||
$params = (array)$request->getParsedBody();
|
||||
@@ -232,11 +231,9 @@ class OauthProcess {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @param OauthClient $client
|
||||
* @param ScopeEntityInterface[] $scopes
|
||||
*
|
||||
* @return array
|
||||
* @return array<mixed>
|
||||
*/
|
||||
private function buildSuccessResponse(ServerRequestInterface $request, OauthClient $client, array $scopes): array {
|
||||
return [
|
||||
@@ -262,7 +259,7 @@ class OauthProcess {
|
||||
|
||||
/**
|
||||
* @param ScopeEntityInterface[] $scopes
|
||||
* @return array
|
||||
* @return string[]
|
||||
*/
|
||||
private function buildScopesArray(array $scopes): array {
|
||||
$result = [];
|
||||
@@ -273,6 +270,15 @@ class OauthProcess {
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* success: false,
|
||||
* error: string,
|
||||
* parameter: string|null,
|
||||
* statusCode: int,
|
||||
* redirectUri?: string,
|
||||
* }
|
||||
*/
|
||||
private function buildCompleteErrorResponse(OAuthServerException $e): array {
|
||||
$hint = $e->getPayload()['hint'] ?? '';
|
||||
if (preg_match('/the `(\w+)` scope/', $hint, $matches)) {
|
||||
@@ -304,8 +310,10 @@ class OauthProcess {
|
||||
*
|
||||
* Part of the existing texts are the legacy from the previous implementation.
|
||||
*
|
||||
* @param OAuthServerException $e
|
||||
* @return array
|
||||
* @return array{
|
||||
* error: string,
|
||||
* message: string,
|
||||
* }
|
||||
*/
|
||||
private function buildIssueErrorResponse(OAuthServerException $e): array {
|
||||
$errorType = $e->getErrorType();
|
||||
@@ -331,6 +339,9 @@ class OauthProcess {
|
||||
return new OAuthServerException('Client must accept authentication request.', 0, 'accept_required', 401);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function getScopesList(AuthorizationRequestInterface $request): array {
|
||||
return array_values(array_map(fn(ScopeEntityInterface $scope): string => $scope->getIdentifier(), $request->getScopes()));
|
||||
}
|
||||
|
@@ -3,8 +3,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\_steps;
|
||||
|
||||
use api\components\OAuth2\Repositories\PublicScopeRepository;
|
||||
use api\tests\FunctionalTester;
|
||||
use common\components\OAuth2\Repositories\PublicScopeRepository;
|
||||
|
||||
class OauthSteps extends FunctionalTester {
|
||||
|
||||
|
@@ -3,8 +3,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\tests\unit\components\OAuth2\Entities;
|
||||
|
||||
use api\components\OAuth2\Entities\AccessTokenEntity;
|
||||
use api\tests\unit\TestCase;
|
||||
use common\components\OAuth2\Entities\AccessTokenEntity;
|
||||
use DateTimeImmutable;
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
|
Reference in New Issue
Block a user