mirror of
https://github.com/elyby/accounts.git
synced 2025-05-31 14:11:46 +05:30
Restore full functionality of OAuth2 server [skip ci]
This commit is contained in:
@ -3,6 +3,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2;
|
||||
|
||||
use api\components\OAuth2\Grants\AuthCodeGrant;
|
||||
use api\components\OAuth2\Grants\RefreshTokenGrant;
|
||||
use api\components\OAuth2\Keys\EmptyKey;
|
||||
use DateInterval;
|
||||
use League\OAuth2\Server\AuthorizationServer;
|
||||
@ -23,8 +25,8 @@ class Component extends BaseComponent {
|
||||
if ($this->_authServer === null) {
|
||||
$clientsRepo = new Repositories\ClientRepository();
|
||||
$accessTokensRepo = new Repositories\AccessTokenRepository();
|
||||
$scopesRepo = new Repositories\ScopeRepository();
|
||||
$publicScopesRepo = new Repositories\PublicScopeRepository();
|
||||
$internalScopesRepo = new Repositories\InternalScopeRepository();
|
||||
$authCodesRepo = new Repositories\AuthCodeRepository();
|
||||
$refreshTokensRepo = new Repositories\RefreshTokenRepository();
|
||||
|
||||
@ -33,17 +35,24 @@ class Component extends BaseComponent {
|
||||
$authServer = new AuthorizationServer(
|
||||
$clientsRepo,
|
||||
$accessTokensRepo,
|
||||
$scopesRepo,
|
||||
new Repositories\EmptyScopeRepository(),
|
||||
new EmptyKey(),
|
||||
'123' // TODO: extract to the variable
|
||||
);
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$authCodeGrant = new Grant\AuthCodeGrant($authCodesRepo, $refreshTokensRepo, new DateInterval('PT10M'));
|
||||
$authCodeGrant = new AuthCodeGrant($authCodesRepo, $refreshTokensRepo, new DateInterval('PT10M'));
|
||||
$authCodeGrant->disableRequireCodeChallengeForPublicClients();
|
||||
$authServer->enableGrantType($authCodeGrant, $accessTokenTTL);
|
||||
$authCodeGrant->setScopeRepository($publicScopesRepo); // Change repository after enabling
|
||||
$authServer->enableGrantType(new Grant\RefreshTokenGrant($refreshTokensRepo), $accessTokenTTL);
|
||||
$authServer->enableGrantType(new Grant\ClientCredentialsGrant(), $accessTokenTTL);
|
||||
|
||||
// TODO: extends refresh token life time to forever
|
||||
$refreshTokenGrant = new RefreshTokenGrant($refreshTokensRepo);
|
||||
$authServer->enableGrantType($refreshTokenGrant, $accessTokenTTL);
|
||||
$refreshTokenGrant->setScopeRepository($publicScopesRepo); // Change repository after enabling
|
||||
|
||||
$clientCredentialsGrant = new Grant\ClientCredentialsGrant();
|
||||
$authServer->enableGrantType($clientCredentialsGrant, $accessTokenTTL);
|
||||
$clientCredentialsGrant->setScopeRepository($internalScopesRepo); // Change repository after enabling
|
||||
|
||||
$this->_authServer = $authServer;
|
||||
}
|
||||
|
@ -1,44 +1,32 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Entities;
|
||||
|
||||
use api\components\OAuth2\Repositories\SessionStorage;
|
||||
use ErrorException;
|
||||
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
|
||||
use api\components\Tokens\TokensFactory;
|
||||
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;
|
||||
|
||||
class AccessTokenEntity extends \League\OAuth2\Server\Entity\AccessTokenEntity {
|
||||
|
||||
protected $sessionId;
|
||||
|
||||
public function getSessionId() {
|
||||
return $this->sessionId;
|
||||
class AccessTokenEntity implements AccessTokenEntityInterface {
|
||||
use EntityTrait;
|
||||
use TokenEntityTrait {
|
||||
getExpiryDateTime as parentGetExpiryDateTime;
|
||||
}
|
||||
|
||||
public function setSessionId($sessionId) {
|
||||
$this->sessionId = $sessionId;
|
||||
public function __toString(): string {
|
||||
// TODO: strip "offline_access" scope from the scopes list
|
||||
return (string)TokensFactory::createForOAuthClient($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @return static
|
||||
*/
|
||||
public function setSession(OriginalSessionEntity $session) {
|
||||
parent::setSession($session);
|
||||
$this->sessionId = $session->getId();
|
||||
|
||||
return $this;
|
||||
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
|
||||
}
|
||||
|
||||
public function getSession() {
|
||||
if ($this->session instanceof OriginalSessionEntity) {
|
||||
return $this->session;
|
||||
}
|
||||
|
||||
$sessionStorage = $this->server->getSessionStorage();
|
||||
if (!$sessionStorage instanceof SessionStorage) {
|
||||
throw new ErrorException('SessionStorage must be instance of ' . SessionStorage::class);
|
||||
}
|
||||
|
||||
return $sessionStorage->getById($this->sessionId);
|
||||
public function getExpiryDateTime() {
|
||||
// TODO: extend token life depending on scopes list
|
||||
return $this->parentGetExpiryDateTime();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -13,6 +13,4 @@ class AuthCodeEntity implements AuthCodeEntityInterface {
|
||||
use AuthCodeTrait;
|
||||
use TokenEntityTrait;
|
||||
|
||||
// TODO: constructor
|
||||
|
||||
}
|
||||
|
@ -11,11 +11,24 @@ class ClientEntity implements ClientEntityInterface {
|
||||
use EntityTrait;
|
||||
use ClientTrait;
|
||||
|
||||
public function __construct(string $id, string $name, $redirectUri, bool $isTrusted = false) {
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $isTrusted;
|
||||
|
||||
public function __construct(string $id, string $name, $redirectUri, bool $isTrusted) {
|
||||
$this->identifier = $id;
|
||||
$this->name = $name;
|
||||
$this->redirectUri = $redirectUri;
|
||||
$this->isConfidential = $isTrusted;
|
||||
$this->isTrusted = $isTrusted;
|
||||
}
|
||||
|
||||
public function isConfidential(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isTrusted(): bool {
|
||||
return $this->isTrusted;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,27 +0,0 @@
|
||||
<?php
|
||||
namespace api\components\OAuth2\Entities;
|
||||
|
||||
use League\OAuth2\Server\Entity\ClientEntity as OriginalClientEntity;
|
||||
use League\OAuth2\Server\Entity\EntityTrait;
|
||||
|
||||
class SessionEntity extends \League\OAuth2\Server\Entity\SessionEntity {
|
||||
use EntityTrait;
|
||||
|
||||
protected $clientId;
|
||||
|
||||
public function getClientId() {
|
||||
return $this->clientId;
|
||||
}
|
||||
|
||||
public function associateClient(OriginalClientEntity $client) {
|
||||
parent::associateClient($client);
|
||||
$this->clientId = $client->getId();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setClientId(string $clientId) {
|
||||
$this->clientId = $clientId;
|
||||
}
|
||||
|
||||
}
|
@ -9,7 +9,7 @@ use League\OAuth2\Server\Entities\UserEntityInterface;
|
||||
class UserEntity implements UserEntityInterface {
|
||||
use EntityTrait;
|
||||
|
||||
public function __construct($id) {
|
||||
public function __construct(int $id) {
|
||||
$this->identifier = $id;
|
||||
}
|
||||
|
||||
|
@ -1,239 +1,23 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Grants;
|
||||
|
||||
use api\components\OAuth2\Entities\AccessTokenEntity;
|
||||
use api\components\OAuth2\Entities\AuthCodeEntity;
|
||||
use api\components\OAuth2\Entities\ClientEntity;
|
||||
use api\components\OAuth2\Entities\RefreshTokenEntity;
|
||||
use api\components\OAuth2\Entities\SessionEntity;
|
||||
use api\components\OAuth2\Repositories\ScopeStorage;
|
||||
use api\components\OAuth2\Utils\Scopes;
|
||||
use League\OAuth2\Server\Entity\AuthCodeEntity as BaseAuthCodeEntity;
|
||||
use League\OAuth2\Server\Entity\ClientEntity as BaseClientEntity;
|
||||
use League\OAuth2\Server\Event\ClientAuthenticationFailedEvent;
|
||||
use League\OAuth2\Server\Exception;
|
||||
use League\OAuth2\Server\Grant\AbstractGrant;
|
||||
use League\OAuth2\Server\Util\SecureKey;
|
||||
use api\components\OAuth2\Repositories\PublicScopeRepository;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
|
||||
use League\OAuth2\Server\Grant\AuthCodeGrant as BaseAuthCodeGrant;
|
||||
|
||||
class AuthCodeGrant extends AbstractGrant {
|
||||
class AuthCodeGrant extends BaseAuthCodeGrant {
|
||||
|
||||
protected $identifier = 'authorization_code';
|
||||
|
||||
protected $responseType = 'code';
|
||||
|
||||
protected $authTokenTTL = 600;
|
||||
|
||||
protected $requireClientSecret = true;
|
||||
|
||||
public function setAuthTokenTTL(int $authTokenTTL): void {
|
||||
$this->authTokenTTL = $authTokenTTL;
|
||||
}
|
||||
|
||||
public function setRequireClientSecret(bool $required): void {
|
||||
$this->requireClientSecret = $required;
|
||||
}
|
||||
|
||||
public function shouldRequireClientSecret(): bool {
|
||||
return $this->requireClientSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check authorize parameters
|
||||
*
|
||||
* @return AuthorizeParams Authorize request parameters
|
||||
* @throws Exception\OAuthException
|
||||
*
|
||||
* @throws
|
||||
*/
|
||||
public function checkAuthorizeParams(): AuthorizeParams {
|
||||
// Get required params
|
||||
$clientId = $this->server->getRequest()->query->get('client_id');
|
||||
if ($clientId === null) {
|
||||
throw new Exception\InvalidRequestException('client_id');
|
||||
protected function issueRefreshToken(AccessTokenEntityInterface $accessToken): ?RefreshTokenEntityInterface {
|
||||
foreach ($accessToken->getScopes() as $scope) {
|
||||
if ($scope->getIdentifier() === PublicScopeRepository::OFFLINE_ACCESS) {
|
||||
return parent::issueRefreshToken($accessToken);
|
||||
}
|
||||
}
|
||||
|
||||
$redirectUri = $this->server->getRequest()->query->get('redirect_uri');
|
||||
if ($redirectUri === null) {
|
||||
throw new Exception\InvalidRequestException('redirect_uri');
|
||||
}
|
||||
|
||||
// Validate client ID and redirect URI
|
||||
$client = $this->server->getClientStorage()->get($clientId, null, $redirectUri, $this->getIdentifier());
|
||||
if (!$client instanceof ClientEntity) {
|
||||
$this->server->getEventEmitter()->emit(new ClientAuthenticationFailedEvent($this->server->getRequest()));
|
||||
throw new Exception\InvalidClientException();
|
||||
}
|
||||
|
||||
$state = $this->server->getRequest()->query->get('state');
|
||||
if ($state === null && $this->server->stateParamRequired()) {
|
||||
throw new Exception\InvalidRequestException('state', $redirectUri);
|
||||
}
|
||||
|
||||
$responseType = $this->server->getRequest()->query->get('response_type');
|
||||
if ($responseType === null) {
|
||||
throw new Exception\InvalidRequestException('response_type', $redirectUri);
|
||||
}
|
||||
|
||||
// Ensure response type is one that is recognised
|
||||
if (!in_array($responseType, $this->server->getResponseTypes(), true)) {
|
||||
throw new Exception\UnsupportedResponseTypeException($responseType, $redirectUri);
|
||||
}
|
||||
|
||||
// Validate any scopes that are in the request
|
||||
$scopeParam = $this->server->getRequest()->query->get('scope', '');
|
||||
$scopes = $this->validateScopes($scopeParam, $client, $redirectUri);
|
||||
|
||||
return new AuthorizeParams($client, $redirectUri, $state, $responseType, $scopes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a new authorize request
|
||||
*
|
||||
* @param string $type The session owner's type
|
||||
* @param string $typeId The session owner's ID
|
||||
* @param AuthorizeParams $authParams The authorize request $_GET parameters
|
||||
*
|
||||
* @return string An authorisation code
|
||||
*/
|
||||
public function newAuthorizeRequest(string $type, string $typeId, AuthorizeParams $authParams): string {
|
||||
// Create a new session
|
||||
$session = new SessionEntity($this->server);
|
||||
$session->setOwner($type, $typeId);
|
||||
$session->associateClient($authParams->getClient());
|
||||
|
||||
// Create a new auth code
|
||||
$authCode = new AuthCodeEntity($this->server);
|
||||
$authCode->setId(SecureKey::generate());
|
||||
$authCode->setRedirectUri($authParams->getRedirectUri());
|
||||
$authCode->setExpireTime(time() + $this->authTokenTTL);
|
||||
|
||||
foreach ($authParams->getScopes() as $scope) {
|
||||
$authCode->associateScope($scope);
|
||||
$session->associateScope($scope);
|
||||
}
|
||||
|
||||
$session->save();
|
||||
$authCode->setSession($session);
|
||||
$authCode->save();
|
||||
|
||||
return $authCode->generateRedirectUri($authParams->getState());
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete the auth code grant
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws Exception\OAuthException
|
||||
*/
|
||||
public function completeFlow(): array {
|
||||
// Get the required params
|
||||
$clientId = $this->server->getRequest()->request->get('client_id', $this->server->getRequest()->getUser());
|
||||
if ($clientId === null) {
|
||||
throw new Exception\InvalidRequestException('client_id');
|
||||
}
|
||||
|
||||
$clientSecret = $this->server->getRequest()->request->get(
|
||||
'client_secret',
|
||||
$this->server->getRequest()->getPassword()
|
||||
);
|
||||
if ($clientSecret === null && $this->shouldRequireClientSecret()) {
|
||||
throw new Exception\InvalidRequestException('client_secret');
|
||||
}
|
||||
|
||||
$redirectUri = $this->server->getRequest()->request->get('redirect_uri');
|
||||
if ($redirectUri === null) {
|
||||
throw new Exception\InvalidRequestException('redirect_uri');
|
||||
}
|
||||
|
||||
// Validate client ID and client secret
|
||||
$client = $this->server->getClientStorage()->get($clientId, $clientSecret, $redirectUri, $this->getIdentifier());
|
||||
if (!$client instanceof BaseClientEntity) {
|
||||
$this->server->getEventEmitter()->emit(new ClientAuthenticationFailedEvent($this->server->getRequest()));
|
||||
throw new Exception\InvalidClientException();
|
||||
}
|
||||
|
||||
// Validate the auth code
|
||||
$authCode = $this->server->getRequest()->request->get('code');
|
||||
if ($authCode === null) {
|
||||
throw new Exception\InvalidRequestException('code');
|
||||
}
|
||||
|
||||
$code = $this->server->getAuthCodeStorage()->get($authCode);
|
||||
if (($code instanceof BaseAuthCodeEntity) === false) {
|
||||
throw new Exception\InvalidRequestException('code');
|
||||
}
|
||||
|
||||
// Ensure the auth code hasn't expired
|
||||
if ($code->isExpired()) {
|
||||
throw new Exception\InvalidRequestException('code');
|
||||
}
|
||||
|
||||
// Check redirect URI presented matches redirect URI originally used in authorize request
|
||||
if ($code->getRedirectUri() !== $redirectUri) {
|
||||
throw new Exception\InvalidRequestException('redirect_uri');
|
||||
}
|
||||
|
||||
$session = $code->getSession();
|
||||
$session->associateClient($client);
|
||||
|
||||
$authCodeScopes = $code->getScopes();
|
||||
|
||||
// Generate the access token
|
||||
$accessToken = new AccessTokenEntity($this->server);
|
||||
$accessToken->setId(SecureKey::generate());
|
||||
$accessToken->setExpireTime($this->getAccessTokenTTL() + time());
|
||||
|
||||
foreach ($authCodeScopes as $authCodeScope) {
|
||||
$session->associateScope($authCodeScope);
|
||||
}
|
||||
|
||||
foreach ($session->getScopes() as $scope) {
|
||||
$accessToken->associateScope($scope);
|
||||
}
|
||||
|
||||
$this->server->getTokenType()->setSession($session);
|
||||
$this->server->getTokenType()->setParam('access_token', $accessToken->getId());
|
||||
$this->server->getTokenType()->setParam('expires_in', $this->getAccessTokenTTL());
|
||||
|
||||
// Set refresh_token param only in case when offline_access requested
|
||||
if (isset($accessToken->getScopes()[ScopeStorage::OFFLINE_ACCESS])) {
|
||||
/** @var RefreshTokenGrant $refreshTokenGrant */
|
||||
$refreshTokenGrant = $this->server->getGrantType('refresh_token');
|
||||
$refreshToken = new RefreshTokenEntity($this->server);
|
||||
$refreshToken->setId(SecureKey::generate());
|
||||
$refreshToken->setExpireTime($refreshTokenGrant->getRefreshTokenTTL() + time());
|
||||
$this->server->getTokenType()->setParam('refresh_token', $refreshToken->getId());
|
||||
}
|
||||
|
||||
// Expire the auth code
|
||||
$code->expire();
|
||||
|
||||
// Save all the things
|
||||
$accessToken->setSession($session);
|
||||
$accessToken->save();
|
||||
|
||||
if (isset($refreshToken)) {
|
||||
$refreshToken->setAccessToken($accessToken);
|
||||
$refreshToken->save();
|
||||
}
|
||||
|
||||
return $this->server->getTokenType()->generateResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* In the earlier versions of Accounts Ely.by backend we had a comma-separated scopes
|
||||
* list, while by OAuth2 standard it they should be separated by a space. Shit happens :)
|
||||
* So override scopes validation function to reformat passed value.
|
||||
*
|
||||
* @param string $scopeParam
|
||||
* @param BaseClientEntity $client
|
||||
* @param string $redirectUri
|
||||
*
|
||||
* @return \League\OAuth2\Server\Entity\ScopeEntity[]
|
||||
*/
|
||||
public function validateScopes($scopeParam = '', BaseClientEntity $client, $redirectUri = null) {
|
||||
return parent::validateScopes(Scopes::format($scopeParam), $client, $redirectUri);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,58 +0,0 @@
|
||||
<?php
|
||||
namespace api\components\OAuth2\Grants;
|
||||
|
||||
use api\components\OAuth2\Entities\ClientEntity;
|
||||
|
||||
class AuthorizeParams {
|
||||
|
||||
private $client;
|
||||
|
||||
private $redirectUri;
|
||||
|
||||
private $state;
|
||||
|
||||
private $responseType;
|
||||
|
||||
/**
|
||||
* @var \api\components\OAuth2\Entities\ScopeEntity[]
|
||||
*/
|
||||
private $scopes;
|
||||
|
||||
public function __construct(
|
||||
ClientEntity $client,
|
||||
string $redirectUri,
|
||||
?string $state,
|
||||
string $responseType,
|
||||
array $scopes
|
||||
) {
|
||||
$this->client = $client;
|
||||
$this->redirectUri = $redirectUri;
|
||||
$this->state = $state;
|
||||
$this->responseType = $responseType;
|
||||
$this->scopes = $scopes;
|
||||
}
|
||||
|
||||
public function getClient(): ClientEntity {
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
public function getRedirectUri(): string {
|
||||
return $this->redirectUri;
|
||||
}
|
||||
|
||||
public function getState(): ?string {
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
public function getResponseType(): string {
|
||||
return $this->responseType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \api\components\OAuth2\Entities\ScopeEntity[]
|
||||
*/
|
||||
public function getScopes(): array {
|
||||
return $this->scopes ?? [];
|
||||
}
|
||||
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
<?php
|
||||
namespace api\components\OAuth2\Grants;
|
||||
|
||||
use api\components\OAuth2\Entities\AccessTokenEntity;
|
||||
use api\components\OAuth2\Entities\SessionEntity;
|
||||
use api\components\OAuth2\Utils\Scopes;
|
||||
use League\OAuth2\Server\Entity\ClientEntity as BaseClientEntity;
|
||||
use League\OAuth2\Server\Event;
|
||||
use League\OAuth2\Server\Exception;
|
||||
use League\OAuth2\Server\Grant\AbstractGrant;
|
||||
use League\OAuth2\Server\Util\SecureKey;
|
||||
|
||||
class ClientCredentialsGrant extends AbstractGrant {
|
||||
|
||||
protected $identifier = 'client_credentials';
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @throws \League\OAuth2\Server\Exception\OAuthException
|
||||
*/
|
||||
public function completeFlow(): array {
|
||||
$clientId = $this->server->getRequest()->request->get('client_id', $this->server->getRequest()->getUser());
|
||||
if ($clientId === null) {
|
||||
throw new Exception\InvalidRequestException('client_id');
|
||||
}
|
||||
|
||||
$clientSecret = $this->server->getRequest()->request->get('client_secret');
|
||||
if ($clientSecret === null) {
|
||||
throw new Exception\InvalidRequestException('client_secret');
|
||||
}
|
||||
|
||||
// Validate client ID and client secret
|
||||
$client = $this->server->getClientStorage()->get($clientId, $clientSecret, null, $this->getIdentifier());
|
||||
if (!$client instanceof BaseClientEntity) {
|
||||
$this->server->getEventEmitter()->emit(new Event\ClientAuthenticationFailedEvent($this->server->getRequest()));
|
||||
throw new Exception\InvalidClientException();
|
||||
}
|
||||
|
||||
// Validate any scopes that are in the request
|
||||
$scopeParam = $this->server->getRequest()->request->get('scope', '');
|
||||
$scopes = $this->validateScopes($scopeParam, $client);
|
||||
|
||||
// Create a new session
|
||||
$session = new SessionEntity($this->server);
|
||||
$session->setOwner('client', $client->getId());
|
||||
$session->associateClient($client);
|
||||
|
||||
// Generate an access token
|
||||
$accessToken = new AccessTokenEntity($this->server);
|
||||
$accessToken->setId(SecureKey::generate());
|
||||
$accessToken->setExpireTime($this->getAccessTokenTTL() + time());
|
||||
|
||||
// Associate scopes with the session and access token
|
||||
foreach ($scopes as $scope) {
|
||||
$session->associateScope($scope);
|
||||
$accessToken->associateScope($scope);
|
||||
}
|
||||
|
||||
// Save everything
|
||||
$session->save();
|
||||
$accessToken->setSession($session);
|
||||
$accessToken->save();
|
||||
|
||||
$this->server->getTokenType()->setSession($session);
|
||||
$this->server->getTokenType()->setParam('access_token', $accessToken->getId());
|
||||
$this->server->getTokenType()->setParam('expires_in', $this->getAccessTokenTTL());
|
||||
|
||||
return $this->server->getTokenType()->generateResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* In the earlier versions of Accounts Ely.by backend we had a comma-separated scopes
|
||||
* list, while by OAuth2 standard it they should be separated by a space. Shit happens :)
|
||||
* So override scopes validation function to reformat passed value.
|
||||
*
|
||||
* @param string $scopeParam
|
||||
* @param BaseClientEntity $client
|
||||
* @param string $redirectUri
|
||||
*
|
||||
* @return \League\OAuth2\Server\Entity\ScopeEntity[]
|
||||
*/
|
||||
public function validateScopes($scopeParam = '', BaseClientEntity $client, $redirectUri = null) {
|
||||
return parent::validateScopes(Scopes::format($scopeParam), $client, $redirectUri);
|
||||
}
|
||||
|
||||
}
|
@ -1,183 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Grants;
|
||||
|
||||
use api\components\OAuth2\Entities\AccessTokenEntity;
|
||||
use api\components\OAuth2\Entities\RefreshTokenEntity;
|
||||
use api\components\OAuth2\Utils\Scopes;
|
||||
use ErrorException;
|
||||
use League\OAuth2\Server\Entity\AccessTokenEntity as BaseAccessTokenEntity;
|
||||
use League\OAuth2\Server\Entity\ClientEntity as BaseClientEntity;
|
||||
use League\OAuth2\Server\Entity\RefreshTokenEntity as BaseRefreshTokenEntity;
|
||||
use League\OAuth2\Server\Event\ClientAuthenticationFailedEvent;
|
||||
use League\OAuth2\Server\Exception;
|
||||
use League\OAuth2\Server\Grant\AbstractGrant;
|
||||
use League\OAuth2\Server\Util\SecureKey;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
|
||||
use League\OAuth2\Server\Grant\RefreshTokenGrant as BaseRefreshTokenGrant;
|
||||
|
||||
class RefreshTokenGrant extends AbstractGrant {
|
||||
|
||||
protected $identifier = 'refresh_token';
|
||||
|
||||
protected $refreshTokenTTL = 604800;
|
||||
|
||||
protected $refreshTokenRotate = false;
|
||||
|
||||
protected $requireClientSecret = true;
|
||||
|
||||
public function setRefreshTokenTTL($refreshTokenTTL): void {
|
||||
$this->refreshTokenTTL = $refreshTokenTTL;
|
||||
}
|
||||
|
||||
public function getRefreshTokenTTL(): int {
|
||||
return $this->refreshTokenTTL;
|
||||
}
|
||||
|
||||
public function setRefreshTokenRotation(bool $refreshTokenRotate = true): void {
|
||||
$this->refreshTokenRotate = $refreshTokenRotate;
|
||||
}
|
||||
|
||||
public function shouldRotateRefreshTokens(): bool {
|
||||
return $this->refreshTokenRotate;
|
||||
}
|
||||
|
||||
public function setRequireClientSecret(string $required): void {
|
||||
$this->requireClientSecret = $required;
|
||||
}
|
||||
|
||||
public function shouldRequireClientSecret(): bool {
|
||||
return $this->requireClientSecret;
|
||||
}
|
||||
class RefreshTokenGrant extends BaseRefreshTokenGrant {
|
||||
|
||||
/**
|
||||
* In the earlier versions of Accounts Ely.by backend we had a comma-separated scopes
|
||||
* list, while by OAuth2 standard it they should be separated by a space. Shit happens :)
|
||||
* So override scopes validation function to reformat passed value.
|
||||
* Currently we're not rotating refresh tokens.
|
||||
* So we overriding this method to always return null, which means,
|
||||
* that refresh_token will not be issued.
|
||||
*
|
||||
* @param string $scopeParam
|
||||
* @param BaseClientEntity $client
|
||||
* @param string $redirectUri
|
||||
* @param AccessTokenEntityInterface $accessToken
|
||||
*
|
||||
* @return \League\OAuth2\Server\Entity\ScopeEntity[]
|
||||
* @return RefreshTokenEntityInterface|null
|
||||
*/
|
||||
public function validateScopes($scopeParam = '', BaseClientEntity $client, $redirectUri = null) {
|
||||
return parent::validateScopes(Scopes::format($scopeParam), $client, $redirectUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* The method has been overridden because we stores access_tokens in Redis with expire value,
|
||||
* so they might not exists at the moment, when it will be requested via refresh_token.
|
||||
* That's why we extends RefreshTokenEntity to give it knowledge about related session.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @throws \League\OAuth2\Server\Exception\OAuthException
|
||||
*/
|
||||
public function completeFlow(): array {
|
||||
$clientId = $this->server->getRequest()->request->get('client_id', $this->server->getRequest()->getUser());
|
||||
if ($clientId === null) {
|
||||
throw new Exception\InvalidRequestException('client_id');
|
||||
}
|
||||
|
||||
$clientSecret = $this->server->getRequest()->request->get(
|
||||
'client_secret',
|
||||
$this->server->getRequest()->getPassword()
|
||||
);
|
||||
if ($clientSecret === null && $this->shouldRequireClientSecret()) {
|
||||
throw new Exception\InvalidRequestException('client_secret');
|
||||
}
|
||||
|
||||
// Validate client ID and client secret
|
||||
$client = $this->server->getClientStorage()->get($clientId, $clientSecret, null, $this->getIdentifier());
|
||||
if (($client instanceof BaseClientEntity) === false) {
|
||||
$this->server->getEventEmitter()->emit(new ClientAuthenticationFailedEvent($this->server->getRequest()));
|
||||
throw new Exception\InvalidClientException();
|
||||
}
|
||||
|
||||
$oldRefreshTokenParam = $this->server->getRequest()->request->get('refresh_token');
|
||||
if ($oldRefreshTokenParam === null) {
|
||||
throw new Exception\InvalidRequestException('refresh_token');
|
||||
}
|
||||
|
||||
// Validate refresh token
|
||||
$oldRefreshToken = $this->server->getRefreshTokenStorage()->get($oldRefreshTokenParam);
|
||||
if (($oldRefreshToken instanceof BaseRefreshTokenEntity) === false) {
|
||||
throw new Exception\InvalidRefreshException();
|
||||
}
|
||||
|
||||
// Ensure the old refresh token hasn't expired
|
||||
if ($oldRefreshToken->isExpired()) {
|
||||
throw new Exception\InvalidRefreshException();
|
||||
}
|
||||
|
||||
/** @var AccessTokenEntity|null $oldAccessToken */
|
||||
$oldAccessToken = $oldRefreshToken->getAccessToken();
|
||||
if ($oldAccessToken instanceof AccessTokenEntity) {
|
||||
// Get the scopes for the original session
|
||||
$session = $oldAccessToken->getSession();
|
||||
} else {
|
||||
if (!$oldRefreshToken instanceof RefreshTokenEntity) {
|
||||
/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
|
||||
throw new ErrorException('oldRefreshToken must be instance of ' . RefreshTokenEntity::class);
|
||||
}
|
||||
|
||||
$session = $oldRefreshToken->getSession();
|
||||
}
|
||||
|
||||
$scopes = $this->formatScopes($session->getScopes());
|
||||
|
||||
// Get and validate any requested scopes
|
||||
$requestedScopesString = $this->server->getRequest()->request->get('scope', '');
|
||||
$requestedScopes = $this->validateScopes($requestedScopesString, $client);
|
||||
|
||||
// If no new scopes are requested then give the access token the original session scopes
|
||||
if (count($requestedScopes) === 0) {
|
||||
$newScopes = $scopes;
|
||||
} else {
|
||||
// The OAuth spec says that a refreshed access token can have the original scopes or fewer so ensure
|
||||
// the request doesn't include any new scopes
|
||||
foreach ($requestedScopes as $requestedScope) {
|
||||
if (!isset($scopes[$requestedScope->getId()])) {
|
||||
throw new Exception\InvalidScopeException($requestedScope->getId());
|
||||
}
|
||||
}
|
||||
|
||||
$newScopes = $requestedScopes;
|
||||
}
|
||||
|
||||
// Generate a new access token and assign it the correct sessions
|
||||
$newAccessToken = new AccessTokenEntity($this->server);
|
||||
$newAccessToken->setId(SecureKey::generate());
|
||||
$newAccessToken->setExpireTime($this->getAccessTokenTTL() + time());
|
||||
$newAccessToken->setSession($session);
|
||||
|
||||
foreach ($newScopes as $newScope) {
|
||||
$newAccessToken->associateScope($newScope);
|
||||
}
|
||||
|
||||
// Expire the old token and save the new one
|
||||
$oldAccessToken instanceof BaseAccessTokenEntity && $oldAccessToken->expire();
|
||||
$newAccessToken->save();
|
||||
|
||||
$this->server->getTokenType()->setSession($session);
|
||||
$this->server->getTokenType()->setParam('access_token', $newAccessToken->getId());
|
||||
$this->server->getTokenType()->setParam('expires_in', $this->getAccessTokenTTL());
|
||||
|
||||
if ($this->shouldRotateRefreshTokens()) {
|
||||
// Expire the old refresh token
|
||||
$oldRefreshToken->expire();
|
||||
|
||||
// Generate a new refresh token
|
||||
$newRefreshToken = new RefreshTokenEntity($this->server);
|
||||
$newRefreshToken->setId(SecureKey::generate());
|
||||
$newRefreshToken->setExpireTime($this->getRefreshTokenTTL() + time());
|
||||
$newRefreshToken->setAccessToken($newAccessToken);
|
||||
$newRefreshToken->save();
|
||||
|
||||
$this->server->getTokenType()->setParam('refresh_token', $newRefreshToken->getId());
|
||||
} else {
|
||||
$oldRefreshToken->setAccessToken($newAccessToken);
|
||||
$oldRefreshToken->save();
|
||||
}
|
||||
|
||||
return $this->server->getTokenType()->generateResponse();
|
||||
protected function issueRefreshToken(AccessTokenEntityInterface $accessToken): ?RefreshTokenEntityInterface {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ class EmptyKey implements CryptKeyInterface {
|
||||
}
|
||||
|
||||
public function getPassPhrase(): ?string {
|
||||
return '';
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ 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;
|
||||
@ -13,44 +14,36 @@ class AccessTokenRepository implements AccessTokenRepositoryInterface {
|
||||
* Create a new access token
|
||||
*
|
||||
* @param ClientEntityInterface $clientEntity
|
||||
* @param \League\OAuth2\Server\Entities\ScopeEntityInterface $scopes
|
||||
* @param \League\OAuth2\Server\Entities\ScopeEntityInterface[] $scopes
|
||||
* @param mixed $userIdentifier
|
||||
*
|
||||
* @return AccessTokenEntityInterface
|
||||
*/
|
||||
public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null) {
|
||||
// TODO: Implement getNewToken() method.
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists a new access token to permanent storage.
|
||||
*
|
||||
* @param AccessTokenEntityInterface $accessTokenEntity
|
||||
*
|
||||
* @throws \League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException
|
||||
*/
|
||||
public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity) {
|
||||
// TODO: Implement persistNewAccessToken() method.
|
||||
public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity): void {
|
||||
// We don't store access tokens, so there's no need to do anything here
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke an access token.
|
||||
*
|
||||
* @param string $tokenId
|
||||
*/
|
||||
public function revokeAccessToken($tokenId) {
|
||||
// TODO: Implement revokeAccessToken() method.
|
||||
public function revokeAccessToken($tokenId): void {
|
||||
// We don't store access tokens, so there's no need to do anything here
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the access token has been revoked.
|
||||
*
|
||||
* @param string $tokenId
|
||||
*
|
||||
* @return bool Return true if this token has been revoked
|
||||
*/
|
||||
public function isAccessTokenRevoked($tokenId) {
|
||||
// TODO: Implement isAccessTokenRevoked() method.
|
||||
public function isAccessTokenRevoked($tokenId): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -25,6 +25,10 @@ class ClientRepository implements ClientRepositoryInterface {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($client->type !== OauthClient::TYPE_APPLICATION) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($clientSecret !== null && $clientSecret !== $client->secret) {
|
||||
return false;
|
||||
}
|
||||
|
@ -1,80 +0,0 @@
|
||||
<?php
|
||||
namespace api\components\OAuth2\Repositories;
|
||||
|
||||
use api\components\OAuth2\Entities\ClientEntity;
|
||||
use api\components\OAuth2\Entities\SessionEntity;
|
||||
use common\models\OauthClient;
|
||||
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
|
||||
use League\OAuth2\Server\Storage\AbstractStorage;
|
||||
use League\OAuth2\Server\Storage\ClientInterface;
|
||||
use yii\helpers\StringHelper;
|
||||
|
||||
class ClientStorage extends AbstractStorage implements ClientInterface {
|
||||
|
||||
private const REDIRECT_STATIC_PAGE = 'static_page';
|
||||
private const REDIRECT_STATIC_PAGE_WITH_CODE = 'static_page_with_code';
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function get($clientId, $clientSecret = null, $redirectUri = null, $grantType = null) {
|
||||
$model = $this->findClient($clientId);
|
||||
if ($model === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($clientSecret !== null && $clientSecret !== $model->secret) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: should check application type
|
||||
// For "desktop" app type redirect_uri is not required and should be by default set
|
||||
// to the static redirect, but for "site" it's required always.
|
||||
if ($redirectUri !== null) {
|
||||
if (in_array($redirectUri, [self::REDIRECT_STATIC_PAGE, self::REDIRECT_STATIC_PAGE_WITH_CODE], true)) {
|
||||
// I think we should check the type of application here
|
||||
} else {
|
||||
if (!StringHelper::startsWith($redirectUri, $model->redirect_uri, false)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$entity = $this->hydrate($model);
|
||||
$entity->setRedirectUri($redirectUri);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getBySession(OriginalSessionEntity $session) {
|
||||
if (!$session instanceof SessionEntity) {
|
||||
throw new \ErrorException('This module assumes that $session typeof ' . SessionEntity::class);
|
||||
}
|
||||
|
||||
$model = $this->findClient($session->getClientId());
|
||||
if ($model === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->hydrate($model);
|
||||
}
|
||||
|
||||
private function hydrate(OauthClient $model): ClientEntity {
|
||||
$entity = new ClientEntity($this->server);
|
||||
$entity->setId($model->id);
|
||||
$entity->setName($model->name);
|
||||
$entity->setSecret($model->secret);
|
||||
$entity->setIsTrusted($model->is_trusted);
|
||||
$entity->setRedirectUri($model->redirect_uri);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
private function findClient(string $clientId): ?OauthClient {
|
||||
return OauthClient::findOne($clientId);
|
||||
}
|
||||
|
||||
}
|
30
api/components/OAuth2/Repositories/EmptyScopeRepository.php
Normal file
30
api/components/OAuth2/Repositories/EmptyScopeRepository.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?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 $client,
|
||||
$userIdentifier = null
|
||||
): array {
|
||||
return $scopes;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
<?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;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
class InternalScopeRepository implements ScopeRepositoryInterface {
|
||||
|
||||
private const ALLOWED_SCOPES = [
|
||||
P::CHANGE_ACCOUNT_USERNAME,
|
||||
P::CHANGE_ACCOUNT_PASSWORD,
|
||||
P::BLOCK_ACCOUNT,
|
||||
P::OBTAIN_EXTENDED_ACCOUNT_INFO,
|
||||
P::ESCAPE_IDENTITY_VERIFICATION,
|
||||
];
|
||||
|
||||
public function getScopeEntityByIdentifier($identifier): ?ScopeEntityInterface {
|
||||
if (!in_array($identifier, self::ALLOWED_SCOPES, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ScopeEntity($identifier);
|
||||
}
|
||||
|
||||
public function finalizeScopes(
|
||||
array $scopes,
|
||||
$grantType,
|
||||
ClientEntityInterface $client,
|
||||
$userIdentifier = null
|
||||
): array {
|
||||
/** @var ClientEntity $client */
|
||||
Assert::isInstanceOf($client, ClientEntity::class);
|
||||
|
||||
if (empty($scopes)) {
|
||||
return $scopes;
|
||||
}
|
||||
|
||||
// Right now we have no available scopes for the client_credentials grant
|
||||
if (!$client->isTrusted()) {
|
||||
throw OAuthServerException::invalidScope($scopes[0]->getIdentifier());
|
||||
}
|
||||
|
||||
return $scopes;
|
||||
}
|
||||
|
||||
}
|
@ -11,7 +11,8 @@ use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
|
||||
|
||||
class PublicScopeRepository implements ScopeRepositoryInterface {
|
||||
|
||||
private const OFFLINE_ACCESS = 'offline_access';
|
||||
public const OFFLINE_ACCESS = 'offline_access';
|
||||
|
||||
private const CHANGE_SKIN = 'change_skin';
|
||||
private const ACCOUNT_INFO = 'account_info';
|
||||
private const ACCOUNT_EMAIL = 'account_email';
|
||||
|
@ -3,49 +3,35 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Repositories;
|
||||
|
||||
use api\components\OAuth2\Entities\RefreshTokenEntity;
|
||||
use common\models\OauthRefreshToken;
|
||||
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
|
||||
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
class RefreshTokenRepository implements RefreshTokenRepositoryInterface {
|
||||
|
||||
/**
|
||||
* Creates a new refresh token
|
||||
*
|
||||
* @return RefreshTokenEntityInterface|null
|
||||
*/
|
||||
public function getNewRefreshToken(): RefreshTokenEntityInterface {
|
||||
// TODO: Implement getNewRefreshToken() method.
|
||||
public function getNewRefreshToken(): ?RefreshTokenEntityInterface {
|
||||
return new RefreshTokenEntity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new refresh token_name.
|
||||
*
|
||||
* @param RefreshTokenEntityInterface $refreshTokenEntity
|
||||
*
|
||||
* @throws \League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException
|
||||
*/
|
||||
public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity) {
|
||||
// TODO: Implement persistNewRefreshToken() method.
|
||||
public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity): void {
|
||||
$model = new OauthRefreshToken();
|
||||
$model->id = $refreshTokenEntity->getIdentifier();
|
||||
$model->account_id = $refreshTokenEntity->getAccessToken()->getUserIdentifier();
|
||||
$model->client_id = $refreshTokenEntity->getAccessToken()->getClient()->getIdentifier();
|
||||
|
||||
Assert::true($model->save());
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke the refresh token.
|
||||
*
|
||||
* @param string $tokenId
|
||||
*/
|
||||
public function revokeRefreshToken($tokenId) {
|
||||
// TODO: Implement revokeRefreshToken() method.
|
||||
public function revokeRefreshToken($tokenId): void {
|
||||
// Currently we're not rotating refresh tokens so do not revoke
|
||||
// token during any OAuth2 grant
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the refresh token has been revoked.
|
||||
*
|
||||
* @param string $tokenId
|
||||
*
|
||||
* @return bool Return true if this token has been revoked
|
||||
*/
|
||||
public function isRefreshTokenRevoked($tokenId) {
|
||||
// TODO: Implement isRefreshTokenRevoked() method.
|
||||
public function isRefreshTokenRevoked($tokenId): bool {
|
||||
// TODO: validate old refresh tokens
|
||||
return !OauthRefreshToken::find()->andWhere(['id' => $tokenId])->exists();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,37 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Repositories;
|
||||
|
||||
use api\components\OAuth2\Entities\ScopeEntity;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
|
||||
|
||||
class ScopeRepository implements ScopeRepositoryInterface {
|
||||
|
||||
public function getScopeEntityByIdentifier($identifier): ?ScopeEntityInterface {
|
||||
// TODO: validate not exists scopes
|
||||
return new ScopeEntity($identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a client, grant type and optional user identifier validate the set of scopes requested are valid and optionally
|
||||
* append additional scopes or remove requested scopes.
|
||||
*
|
||||
* @param ScopeEntityInterface $scopes
|
||||
* @param string $grantType
|
||||
* @param \League\OAuth2\Server\Entities\ClientEntityInterface $clientEntity
|
||||
* @param null|string $userIdentifier
|
||||
*
|
||||
* @return ScopeEntityInterface
|
||||
*/
|
||||
public function finalizeScopes(
|
||||
array $scopes,
|
||||
$grantType,
|
||||
\League\OAuth2\Server\Entities\ClientEntityInterface $clientEntity,
|
||||
$userIdentifier = null
|
||||
): array {
|
||||
// TODO: Implement finalizeScopes() method.
|
||||
}
|
||||
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
<?php
|
||||
namespace api\components\OAuth2\Repositories;
|
||||
|
||||
use api\components\OAuth2\Entities\ClientEntity;
|
||||
use api\components\OAuth2\Entities\ScopeEntity;
|
||||
use api\rbac\Permissions as P;
|
||||
use Assert\Assert;
|
||||
use League\OAuth2\Server\Storage\AbstractStorage;
|
||||
use League\OAuth2\Server\Storage\ScopeInterface;
|
||||
|
||||
class ScopeStorage extends AbstractStorage implements ScopeInterface {
|
||||
|
||||
public const OFFLINE_ACCESS = 'offline_access';
|
||||
|
||||
public const CHANGE_SKIN = 'change_skin';
|
||||
|
||||
private const PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS = [
|
||||
'account_info' => P::OBTAIN_OWN_ACCOUNT_INFO,
|
||||
'account_email' => P::OBTAIN_ACCOUNT_EMAIL,
|
||||
'account_block' => P::BLOCK_ACCOUNT,
|
||||
'internal_account_info' => P::OBTAIN_EXTENDED_ACCOUNT_INFO,
|
||||
];
|
||||
|
||||
private const AUTHORIZATION_CODE_PERMISSIONS = [
|
||||
P::OBTAIN_OWN_ACCOUNT_INFO,
|
||||
P::OBTAIN_ACCOUNT_EMAIL,
|
||||
P::MINECRAFT_SERVER_SESSION,
|
||||
self::OFFLINE_ACCESS,
|
||||
self::CHANGE_SKIN,
|
||||
];
|
||||
|
||||
private const CLIENT_CREDENTIALS_PERMISSIONS = [
|
||||
];
|
||||
|
||||
private const CLIENT_CREDENTIALS_PERMISSIONS_INTERNAL = [
|
||||
P::CHANGE_ACCOUNT_USERNAME,
|
||||
P::CHANGE_ACCOUNT_PASSWORD,
|
||||
P::BLOCK_ACCOUNT,
|
||||
P::OBTAIN_EXTENDED_ACCOUNT_INFO,
|
||||
P::ESCAPE_IDENTITY_VERIFICATION,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $scope
|
||||
* @param string $grantType is passed on if called from the grant.
|
||||
* In this case, you only need to filter out the rights that you can get on this grant.
|
||||
* @param string $clientId
|
||||
*
|
||||
* @return ScopeEntity|null
|
||||
*/
|
||||
public function get($scope, $grantType = null, $clientId = null): ?ScopeEntity {
|
||||
$permission = $this->convertToInternalPermission($scope);
|
||||
|
||||
if ($grantType === 'authorization_code') {
|
||||
$permissions = self::AUTHORIZATION_CODE_PERMISSIONS;
|
||||
} elseif ($grantType === 'client_credentials') {
|
||||
$permissions = self::CLIENT_CREDENTIALS_PERMISSIONS;
|
||||
$isTrusted = false;
|
||||
if ($clientId !== null) {
|
||||
/** @var ClientEntity $client */
|
||||
$client = $this->server->getClientStorage()->get($clientId);
|
||||
Assert::that($client)->isInstanceOf(ClientEntity::class);
|
||||
|
||||
/** @noinspection NullPointerExceptionInspection */
|
||||
$isTrusted = $client->isTrusted();
|
||||
}
|
||||
|
||||
if ($isTrusted) {
|
||||
$permissions = array_merge($permissions, self::CLIENT_CREDENTIALS_PERMISSIONS_INTERNAL);
|
||||
}
|
||||
} else {
|
||||
$permissions = array_merge(
|
||||
self::AUTHORIZATION_CODE_PERMISSIONS,
|
||||
self::CLIENT_CREDENTIALS_PERMISSIONS,
|
||||
self::CLIENT_CREDENTIALS_PERMISSIONS_INTERNAL
|
||||
);
|
||||
}
|
||||
|
||||
if (!in_array($permission, $permissions, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entity = new ScopeEntity($this->server);
|
||||
$entity->setId($permission);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
private function convertToInternalPermission(string $publicScope): string {
|
||||
return self::PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS[$publicScope] ?? $publicScope;
|
||||
}
|
||||
|
||||
}
|
12
api/components/OAuth2/Traits/ValidateScopesTrait.php
Normal file
12
api/components/OAuth2/Traits/ValidateScopesTrait.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Traits;
|
||||
|
||||
trait ValidateScopesTrait {
|
||||
|
||||
public function validateScopes($scopes, $redirectUri = null): array {
|
||||
return parent::validateScopes($scopes, $redirectUri = null);
|
||||
}
|
||||
|
||||
}
|
@ -7,16 +7,19 @@ use Carbon\Carbon;
|
||||
use common\models\Account;
|
||||
use common\models\AccountSession;
|
||||
use Lcobucci\JWT\Token;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use Yii;
|
||||
|
||||
class TokensFactory {
|
||||
|
||||
public const SUB_ACCOUNT_PREFIX = 'ely|';
|
||||
public const AUD_CLIENT_PREFIX = 'client|';
|
||||
|
||||
public static function createForAccount(Account $account, AccountSession $session = null): Token {
|
||||
$payloads = [
|
||||
'ely-scopes' => 'accounts_web_user',
|
||||
'sub' => self::SUB_ACCOUNT_PREFIX . $account->id,
|
||||
'sub' => self::buildSub($account->id),
|
||||
];
|
||||
if ($session === null) {
|
||||
// If we don't remember a session, the token should live longer
|
||||
@ -29,4 +32,27 @@ class TokensFactory {
|
||||
return Yii::$app->tokens->create($payloads);
|
||||
}
|
||||
|
||||
public static function createForOAuthClient(AccessTokenEntityInterface $accessToken): Token {
|
||||
$payloads = [
|
||||
'aud' => self::buildAud($accessToken->getClient()->getIdentifier()),
|
||||
'ely-scopes' => array_map(static function(ScopeEntityInterface $scope): string {
|
||||
return $scope->getIdentifier();
|
||||
}, $accessToken->getScopes()),
|
||||
'exp' => $accessToken->getExpiryDateTime()->getTimestamp(),
|
||||
];
|
||||
if ($accessToken->getUserIdentifier() !== null) {
|
||||
$payloads['sub'] = self::buildSub($accessToken->getUserIdentifier());
|
||||
}
|
||||
|
||||
return Yii::$app->tokens->create($payloads);
|
||||
}
|
||||
|
||||
private static function buildSub(int $accountId): string {
|
||||
return self::SUB_ACCOUNT_PREFIX . $accountId;
|
||||
}
|
||||
|
||||
private static function buildAud(string $clientId): string {
|
||||
return self::AUD_CLIENT_PREFIX . $clientId;
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user