diff --git a/com_oauthserver/administrator/src/Event/Scope/ScopeResolveEvent.php b/com_oauthserver/administrator/src/Event/Scope/ScopeResolveEvent.php new file mode 100644 index 0000000..50ae660 --- /dev/null +++ b/com_oauthserver/administrator/src/Event/Scope/ScopeResolveEvent.php @@ -0,0 +1,87 @@ + $scopes, + 'grant' => $grant, + 'client' => $client, + 'user_id' => $user_id + ]; + + $this->is_constructed = false; + + parent::__construct('onOauthServerScopeResolve', $arguments); + + $this->is_constructed = true; + } + + protected function onSetScopes(array $scopes): array + { + foreach ($scopes as &$scope) { + if (!($scope instanceof Scope)) { + throw new \InvalidArgumentException(sprintf('Argument "scopes" must be array of "%s" in class "%s". "%s" given.', + Scope::class, + get_class($this), + get_debug_type($scope) + )); + } + } + + return $scopes; + } + + protected function onSetGrant(string $grant) + { + if ($this->is_constructed) { + $grant = $this->getArgument('grant'); + } + return $grant; + } + + protected function onSetClient(object $client) + { + if ($this->is_constructed) { + $client = $this->getArgument('client'); + } + return $client; + } + + protected function onSetUser_id(?int $user_id) + { + if ($this->is_constructed) { + $user_id = $this->getArgument('user_id'); + } + return $user_id; + } + + public function removeArgument($name) + { + throw new \BadMethodCallException( + sprintf( + 'Cannot remove the argument %s of the event %s.', + $name, + $this->name + ) + ); + } + + public function clearArguments() + { + throw new \BadMethodCallException( + sprintf( + 'Cannot clear arguments of the event %s.', + $this->name + ) + ); + } +} \ No newline at end of file diff --git a/com_oauthserver/administrator/src/Model/AuthCodeModel.php b/com_oauthserver/administrator/src/Model/AuthCodeModel.php new file mode 100644 index 0000000..950e3a2 --- /dev/null +++ b/com_oauthserver/administrator/src/Model/AuthCodeModel.php @@ -0,0 +1,64 @@ +name = 'AuthCode'; + parent::__construct($config, $factory, $formFactory); + } + + public function getTable($name = 'AuthCode', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } + + public function getForm($data = [], $loadData = true): Form|bool + { + $form = $this->loadForm('com_oauthserver.auth_code', 'auth_code', ['control' => 'jform', 'load_data' => $loadData]); + + if (empty($form)) { + return false; + } + + return $form; + } + + protected function loadFormData(): mixed + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_oauthserver.edit.auth_code.data', []); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_oauthserver.auth_code', $data); + + return $data; + } + + /** + * @param \Webmasterskaya\Component\OauthServer\Administrator\Table\AuthCodeTable $table + * @return void + * @since version + */ + protected function prepareTable($table) + { + if ($table->expiry instanceof \DateTime || $table->expiry instanceof \DateTimeImmutable) { + $table->expiry = $table->expiry->format($table->getDbo()->getDateFormat()); + } + } +} \ No newline at end of file diff --git a/com_oauthserver/administrator/src/Model/GetItemByIdentifierTrait.php b/com_oauthserver/administrator/src/Model/GetItemByIdentifierTrait.php index 1576af8..fcddff1 100644 --- a/com_oauthserver/administrator/src/Model/GetItemByIdentifierTrait.php +++ b/com_oauthserver/administrator/src/Model/GetItemByIdentifierTrait.php @@ -4,7 +4,6 @@ namespace Webmasterskaya\Component\OauthServer\Administrator\Model; use Joomla\CMS\Language\Text; use Joomla\CMS\Object\CMSObject; -use Joomla\Registry\Registry; use Joomla\Utilities\ArrayHelper; trait GetItemByIdentifierTrait @@ -37,7 +36,7 @@ trait GetItemByIdentifierTrait $all_properties = $table->getProperties(false); if (!empty($all_properties['_jsonEncode'])) { - foreach ($all_properties['$_jsonEncode'] as $prop) { + foreach ($all_properties['_jsonEncode'] as $prop) { if (array_key_exists($prop, $properties) && is_string($properties[$prop])) { $properties[$prop] = json_decode($properties[$prop]); } diff --git a/com_oauthserver/administrator/src/Model/RefreshTokenModel.php b/com_oauthserver/administrator/src/Model/RefreshTokenModel.php new file mode 100644 index 0000000..ae66ae7 --- /dev/null +++ b/com_oauthserver/administrator/src/Model/RefreshTokenModel.php @@ -0,0 +1,38 @@ +loadForm('com_oauthserver.refresh_token', 'refresh_token', ['control' => 'jform', 'load_data' => $loadData]); + + if (empty($form)) { + return false; + } + + return $form; + } + + protected function loadFormData(): mixed + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_oauthserver.edit.refresh_token.data', []); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_oauthserver.refresh_token', $data); + + return $data; + } +} \ No newline at end of file diff --git a/com_oauthserver/administrator/src/Model/ScopeModel.php b/com_oauthserver/administrator/src/Model/ScopeModel.php new file mode 100644 index 0000000..d5bac94 --- /dev/null +++ b/com_oauthserver/administrator/src/Model/ScopeModel.php @@ -0,0 +1,26 @@ +app; $user = $app->getIdentity(); + $uri = Uri::getInstance(); if (!$user->id) { - $return = http_build_query(['return' => base64_encode(Uri::getInstance()->toString(['scheme', 'user', 'pass', 'host', 'port', 'path']))]); - $this->app->setUserState('oauthserver.login.authorize.request', Uri::getInstance()->getQuery(true)); + $return = http_build_query(['return' => base64_encode($uri->toString(['scheme', 'user', 'pass', 'host', 'port', 'path']))]); + $this->app->setUserState('oauthserver.login.authorize.request', $uri->getQuery(true)); $this->app->enqueueMessage('Необходимо авторизоваться!'); $this->app->redirect(Route::_('index.php?option=com_users&view=login&' . $return)); } - $clientRepository = new ClientRepository($this->factory); - var_dump($this->app->getUserState('oauthserver.login.authorize.request')); + $state_request = $this->app->getUserState('oauthserver.login.authorize.request'); + if (!empty($state_request) && empty($uri->getQuery(true))) { + foreach ($state_request as $k => $v) { + $uri->setVar($k, $v); + } + } + $this->app->setUserState('oauthserver.login.authorize.request', []); + + /** @var \Webmasterskaya\Component\OauthServer\Administrator\Model\ClientModel $clientModel */ + $clientModel = $this->factory->createModel('Client', 'Administrator', ['request_ignore' => true]); + $clientRepository = new ClientRepository($clientModel); + + /** @var \Webmasterskaya\Component\OauthServer\Administrator\Model\AccessTokenModel $accessTokenModel */ + $accessTokenModel = $this->factory->createModel('AccessToken', 'Administrator', ['request_ignore' => true]); + $accessTokenRepository = new AccessTokenRepository($accessTokenModel, $clientModel); + + $scopeRepository = new ScopeRepository($clientModel); + $scopeRepository->setDispatcher($this->getDispatcher()); + + /** @var \Webmasterskaya\Component\OauthServer\Administrator\Model\AuthCodeModel $authCodeModel */ + $authCodeModel = $this->factory->createModel('AuthCode', 'Administrator', ['request_ignore' => true]); + $authCodeRepository = new AuthCodeRepository($authCodeModel, $clientModel); + + /** @var \Webmasterskaya\Component\OauthServer\Administrator\Model\RefreshTokenModel $refreshTokenModel */ + $refreshTokenModel = $this->factory->createModel('RefreshToken', 'Administrator', ['request_ignore' => true]); + $refreshTokenRepository = new RefreshTokenRepository($refreshTokenModel, $accessTokenModel); + + $key = openssl_pkey_new([ + "digest_alg" => "sha512", + "private_key_bits" => 4096, + "private_key_type" => OPENSSL_KEYTYPE_RSA, + ]); + + $ppk = ''; + openssl_pkey_export($key, $ppk); + + // Extract the public key from $res to $pubKey +// $pub = openssl_pkey_get_details($key); +// $pub = $pub["key"]; + +// var_dump($this->app->getUserState('oauthserver.login.authorize.request')); + + $server = new AuthorizationServer( + $clientRepository, + $accessTokenRepository, + $scopeRepository, + $ppk, + $this->app->get('secret') + ); + + $grant = new AuthCodeGrant( + $authCodeRepository, + $refreshTokenRepository, + new \DateInterval('PT10M') // authorization codes will expire after 10 minutes + ); + + $grant->setRefreshTokenTTL(new \DateInterval('P1M')); // refresh tokens will expire after 1 month + + $server->enableGrantType( + $grant, + new \DateInterval('PT1H') // access tokens will expire after 1 hour + ); + + $serverRequest = ServerRequestFactory::fromGlobals(); + $serverResponse = $this->app->getResponse(); + +// var_dump($serverRequest->getQueryParams()); die(); + + $authRequest = $server->validateAuthorizationRequest($serverRequest); + $authRequest->setUser(new UserEntity($user)); + $authRequest->setAuthorizationApproved(true); + + $this->app->setResponse($server->completeAuthorizationRequest($authRequest, $serverResponse)); + + return; + + echo "
"; + + var_dump(); + die(); } } \ No newline at end of file diff --git a/com_oauthserver/site/src/Entity/AuthCode.php b/com_oauthserver/site/src/Entity/AuthCode.php new file mode 100644 index 0000000..2aafdb4 --- /dev/null +++ b/com_oauthserver/site/src/Entity/AuthCode.php @@ -0,0 +1,26 @@ + $this->getIdentifier(), + 'expiry' => $this->getExpiryDateTime(), + 'user_id' => $this->getUserIdentifier(), + 'scopes' => $this->getScopes(), + 'client_identifier' => $this->getClient()->getIdentifier() + ]; + } +} \ No newline at end of file diff --git a/com_oauthserver/site/src/Entity/RefreshToken.php b/com_oauthserver/site/src/Entity/RefreshToken.php new file mode 100644 index 0000000..1d23dc3 --- /dev/null +++ b/com_oauthserver/site/src/Entity/RefreshToken.php @@ -0,0 +1,22 @@ + $this->getIdentifier(), + 'expiry' => $this->getExpiryDateTime(), + 'access_token_identifier' => $this->getAccessToken()->getIdentifier() + ]; + } +} \ No newline at end of file diff --git a/com_oauthserver/site/src/Entity/Scope.php b/com_oauthserver/site/src/Entity/Scope.php new file mode 100644 index 0000000..88d5c97 --- /dev/null +++ b/com_oauthserver/site/src/Entity/Scope.php @@ -0,0 +1,48 @@ +getIdentifier(); + } + + /** + * @return string|null + * @since version + */ + public function getDescription(): ?string + { + return $this->description ?? null; + } + + /** + * @param string $description + * @since version + */ + public function setDescription(string $description): void + { + $this->description = $description; + } + + public function __toString(): string + { + return $this->identifier; + } + + +} \ No newline at end of file diff --git a/com_oauthserver/site/src/Entity/User.php b/com_oauthserver/site/src/Entity/User.php new file mode 100644 index 0000000..b185fa4 --- /dev/null +++ b/com_oauthserver/site/src/Entity/User.php @@ -0,0 +1,16 @@ +setIdentifier($user->id); + } +} \ No newline at end of file diff --git a/com_oauthserver/site/src/Repository/AuthCodeRepository.php b/com_oauthserver/site/src/Repository/AuthCodeRepository.php index 92aeba0..f92fe88 100644 --- a/com_oauthserver/site/src/Repository/AuthCodeRepository.php +++ b/com_oauthserver/site/src/Repository/AuthCodeRepository.php @@ -3,28 +3,75 @@ namespace Webmasterskaya\Component\OauthServer\Site\Repository; use League\OAuth2\Server\Entities\AuthCodeEntityInterface; +use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; +use Wamania\Snowball\NotFoundException; +use Webmasterskaya\Component\OauthServer\Administrator\Model\AuthCodeModel; +use Webmasterskaya\Component\OauthServer\Administrator\Model\ClientModel; +use Webmasterskaya\Component\OauthServer\Site\Entity\AuthCode; class AuthCodeRepository implements AuthCodeRepositoryInterface { + private AuthCodeModel $authCodeModel; - public function getNewAuthCode() + private ClientModel $clientModel; + + /** + * @param \Webmasterskaya\Component\OauthServer\Administrator\Model\AuthCodeModel $authCodeModel + * @param \Webmasterskaya\Component\OauthServer\Administrator\Model\ClientModel $clientModel + * @since version + */ + public function __construct(AuthCodeModel $authCodeModel, ClientModel $clientModel) { - // TODO: Implement getNewAuthCode() method. + $this->authCodeModel = $authCodeModel; + $this->clientModel = $clientModel; + } + + public function getNewAuthCode(): AuthCode + { + return new AuthCode(); } public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity) { - // TODO: Implement persistNewAuthCode() method. + $found = false; + try { + $authCode = $this->authCodeModel->getItemByIdentifier($authCodeEntity->getIdentifier()); + + if ($authCode->id > 0) { + $found = true; + + } + } catch (\Throwable $e) { + } + + if ($found) { + throw UniqueTokenIdentifierConstraintViolationException::create(); + } + + $data = $authCodeEntity->getData(); + + $client = $this->clientModel->getItemByIdentifier($authCodeEntity->getClient()->getIdentifier()); + + $data['client_id'] = $client->id; + unset($data['client_identifier']); + + $this->authCodeModel->save($data); } public function revokeAuthCode($codeId) { - // TODO: Implement revokeAuthCode() method. + $this->authCodeModel->revoke($codeId); } public function isAuthCodeRevoked($codeId) { - // TODO: Implement isAuthCodeRevoked() method. + $authCode = $this->authCodeModel->getItemByIdentifier($codeId); + + if (empty($authCode->id)) { + return true; + } + + return !!$authCode->revoked; } } \ No newline at end of file diff --git a/com_oauthserver/site/src/Repository/RefreshTokenRepository.php b/com_oauthserver/site/src/Repository/RefreshTokenRepository.php index 31ec9e3..704f612 100644 --- a/com_oauthserver/site/src/Repository/RefreshTokenRepository.php +++ b/com_oauthserver/site/src/Repository/RefreshTokenRepository.php @@ -3,28 +3,67 @@ namespace Webmasterskaya\Component\OauthServer\Site\Repository; use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; +use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; +use Webmasterskaya\Component\OauthServer\Administrator\Model\AccessTokenModel; +use Webmasterskaya\Component\OauthServer\Administrator\Model\RefreshTokenModel; +use Webmasterskaya\Component\OauthServer\Site\Entity\RefreshToken; class RefreshTokenRepository implements RefreshTokenRepositoryInterface { - public function getNewRefreshToken() + private RefreshTokenModel $refreshTokenModel; + + private AccessTokenModel $accessTokenModel; + + /** + * @param \Webmasterskaya\Component\OauthServer\Administrator\Model\RefreshTokenModel $refreshTokenModel + * @param \Webmasterskaya\Component\OauthServer\Administrator\Model\AccessTokenModel $accessTokenModel + * @since version + */ + public function __construct(RefreshTokenModel $refreshTokenModel, AccessTokenModel $accessTokenModel) { - // TODO: Implement getNewRefreshToken() method. + $this->refreshTokenModel = $refreshTokenModel; + $this->accessTokenModel = $accessTokenModel; + } + + + public function getNewRefreshToken(): RefreshToken + { + return new RefreshToken(); } public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity) { - // TODO: Implement persistNewRefreshToken() method. + $refreshToken = $this->refreshTokenModel->getItemByIdentifier($refreshTokenEntity->getIdentifier()); + + if ($refreshToken->id > 0) { + throw UniqueTokenIdentifierConstraintViolationException::create(); + } + + $data = $refreshTokenEntity->getData(); + + $accessToken = $this->accessTokenModel->getItemByIdentifier($refreshTokenEntity->getAccessToken()); + + unset($data['access_token_identifier']); + $data['access_token_id'] = $accessToken->id; + + $this->refreshTokenModel->save($data); } - public function revokeRefreshToken($tokenId) + public function revokeRefreshToken($tokenId): void { - // TODO: Implement revokeRefreshToken() method. + $this->refreshTokenModel->revoke($tokenId); } - public function isRefreshTokenRevoked($tokenId) + public function isRefreshTokenRevoked($tokenId): bool { - // TODO: Implement isRefreshTokenRevoked() method. + $refreshToken = $this->refreshTokenModel->getItemByIdentifier($tokenId); + + if (empty($refreshToken->id)) { + return true; + } + + return !!$refreshToken->revoked; } } \ No newline at end of file diff --git a/com_oauthserver/site/src/Repository/ScopeRepository.php b/com_oauthserver/site/src/Repository/ScopeRepository.php index a606365..2bb7df0 100644 --- a/com_oauthserver/site/src/Repository/ScopeRepository.php +++ b/com_oauthserver/site/src/Repository/ScopeRepository.php @@ -2,19 +2,107 @@ namespace Webmasterskaya\Component\OauthServer\Site\Repository; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\Event\DispatcherAwareInterface; +use Joomla\Event\DispatcherAwareTrait; use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; +use Webmasterskaya\Component\OauthServer\Administrator\Event\Scope\ScopeResolveEvent; +use Webmasterskaya\Component\OauthServer\Administrator\Model\ClientModel; +use Webmasterskaya\Component\OauthServer\Site\Entity\Scope; -class ScopeRepository implements ScopeRepositoryInterface +class ScopeRepository implements ScopeRepositoryInterface, DispatcherAwareInterface { + use DispatcherAwareTrait; - public function getScopeEntityByIdentifier($identifier) + private ClientModel $clientModel; + + public function __construct(ClientModel $clientModel) { - // TODO: Implement getScopeEntityByIdentifier() method. + $this->clientModel = $clientModel; } + public function getScopeEntityByIdentifier($identifier): ?Scope + { + $defined = ['userinfo', 'email']; + + if (!in_array($identifier, $defined)) { + return null; + } + + $scope = new Scope(); + $scope->setIdentifier($identifier); + + return $scope; + } + + /** + * @param Scope[] $scopes + * @param $grantType + * @param \League\OAuth2\Server\Entities\ClientEntityInterface $clientEntity + * @param null $userIdentifier + * @return mixed + * @throws \League\OAuth2\Server\Exception\OAuthServerException + * @since version + */ public function finalizeScopes(array $scopes, $grantType, ClientEntityInterface $clientEntity, $userIdentifier = null) { - // TODO: Implement finalizeScopes() method. + $client = $this->clientModel->getItemByIdentifier($clientEntity->getIdentifier()); + + $scopes = $this->setupScopes($client, array_values($scopes)); + + PluginHelper::importPlugin('oauthserver'); + + $event = new ScopeResolveEvent( + $scopes, + $grantType, + $client, + $userIdentifier + ); + + return $this->getDispatcher() + ->dispatch($event->getName(), $event) + ->getArgument('scopes', []); + } + + /** + * @param object $client + * @param array $requestedScopes + * @return array + * @throws \League\OAuth2\Server\Exception\OAuthServerException + * @since version + */ + private function setupScopes(object $client, array $requestedScopes): array + { + $clientScopes = $client->scopes; + + if (empty($clientScopes)) { + return $requestedScopes; + } + + $clientScopes = array_map(function ($item) { + $scope = new Scope(); + $scope->setIdentifier((string)$item); + return $scope; + }, $clientScopes); + + if (empty($requestedScopes)) { + return $clientScopes; + } + + $finalizedScopes = []; + $clientScopesAsStrings = array_map('strval', $clientScopes); + + foreach ($requestedScopes as $requestedScope) { + $requestedScopeAsString = (string)$requestedScope; + if (!\in_array($requestedScopeAsString, $clientScopesAsStrings, true)) { + throw OAuthServerException::invalidScope($requestedScopeAsString); + } + + $finalizedScopes[] = $requestedScope; + } + + return $finalizedScopes; } } \ No newline at end of file diff --git a/lib_oauthserver/composer.json b/lib_oauthserver/composer.json index 134816f..465ef23 100644 --- a/lib_oauthserver/composer.json +++ b/lib_oauthserver/composer.json @@ -16,6 +16,7 @@ "minimum-stability": "stable", "require": { "php": "^8.0", - "league/oauth2-server": "^8.5" + "league/oauth2-server": "^8.5", + "ext-openssl": "*" } }