From 18cad4271b622c51404dfd440703683f188fe6b7 Mon Sep 17 00:00:00 2001 From: Oleksii Bulba Date: Sat, 6 Jul 2024 16:41:30 +0400 Subject: [PATCH] Update v2.x-automation-test for micro/plugin-oauth2-client-keycloak --- .../Provider/ProviderConfiguration.php | 55 ++++++ Client/LICENSE | 21 +++ Client/OAuth2KeycloakProviderPlugin.php | 69 ++++++++ ...th2KeycloakProviderPluginConfiguration.php | 34 ++++ ...KeycloakProviderConfigurationInterface.php | 42 +++++ Client/Provider/OAuth2Provider.php | 161 ++++++++++++++++++ Client/Provider/ResourceOwner.php | 81 +++++++++ Client/composer.json | 25 +++ 8 files changed, 488 insertions(+) create mode 100644 Client/Configuration/Provider/ProviderConfiguration.php create mode 100755 Client/LICENSE create mode 100644 Client/OAuth2KeycloakProviderPlugin.php create mode 100644 Client/OAuth2KeycloakProviderPluginConfiguration.php create mode 100644 Client/Provider/KeycloakProviderConfigurationInterface.php create mode 100644 Client/Provider/OAuth2Provider.php create mode 100644 Client/Provider/ResourceOwner.php create mode 100644 Client/composer.json diff --git a/Client/Configuration/Provider/ProviderConfiguration.php b/Client/Configuration/Provider/ProviderConfiguration.php new file mode 100644 index 0000000..34e744b --- /dev/null +++ b/Client/Configuration/Provider/ProviderConfiguration.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Micro\Plugin\OAuth2\Keycloak\Client\Configuration\Provider; + +use Micro\Plugin\OAuth2\Client\Configuration\Provider\OAuth2ClientProviderConfiguration; +use Micro\Plugin\OAuth2\Keycloak\Client\Provider\KeycloakProviderConfigurationInterface; + +/** + * @author Stanislau Komar + */ +class ProviderConfiguration extends OAuth2ClientProviderConfiguration implements KeycloakProviderConfigurationInterface +{ + const CFG_REALM = 'MICRO_OAUTH2_%s_REALM'; + const CFG_SCOPES = 'MICRO_OAUTH2_%s_SCOPES'; + + /** + * {@inheritDoc} + */ + public function getRealm(): string + { + return $this->get(self::CFG_REALM, 'micro'); + } + + /** + * {@inheritDoc} + */ + public function getScopesDefault(): array + { + return $this->explodeStringToArray($this->get(self::CFG_SCOPES, 'email,profile')); + } + + /** + * {@inheritDoc} + */ + public function getScopesSeparator(): string + { + return ' '; + } + + public function getSecurityProvider(): string|null + { + return null; + } +} \ No newline at end of file diff --git a/Client/LICENSE b/Client/LICENSE new file mode 100755 index 0000000..e6c0e69 --- /dev/null +++ b/Client/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Komar Stanislau + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Client/OAuth2KeycloakProviderPlugin.php b/Client/OAuth2KeycloakProviderPlugin.php new file mode 100644 index 0000000..6d4828a --- /dev/null +++ b/Client/OAuth2KeycloakProviderPlugin.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Micro\Plugin\OAuth2\Keycloak\Client; + +use League\OAuth2\Client\Provider\AbstractProvider; +use Micro\Framework\DependencyInjection\Container; +use Micro\Framework\BootConfiguration\Plugin\ConfigurableInterface; +use Micro\Framework\BootDependency\Plugin\DependencyProviderInterface; +use Micro\Framework\BootConfiguration\Plugin\PluginConfigurationTrait; +use Micro\Plugin\OAuth2\Client\Configuration\OAuth2ClientPluginConfigurationInterface; +use Micro\Plugin\OAuth2\Keycloak\Client\Provider\OAuth2Provider; +use Micro\Plugin\OAuth2\Client\Provider\OAuth2ClientProviderPluginInterface; +use Micro\Plugin\Security\Facade\SecurityFacadeInterface; + +/** + * @author Stanislau Komar + * + * @method OAuth2ClientPluginConfigurationInterface configuration() + */ +class OAuth2KeycloakProviderPlugin implements OAuth2ClientProviderPluginInterface, DependencyProviderInterface, ConfigurableInterface +{ + + use PluginConfigurationTrait; + + const PROVIDER_TYPE = 'keycloak'; + + /** + * @var Container + */ + private readonly Container $container; + + /** + * {@inheritDoc} + */ + public function createProvider(string $providerName): AbstractProvider + { + return new OAuth2Provider( + $this->configuration()->getProviderConfiguration($providerName), + $this->container->get(SecurityFacadeInterface::class), + ); + } + + /** + * {@inheritDoc} + */ + public function getType(): string + { + return self::PROVIDER_TYPE; + } + + /** + * {@inheritDoc} + */ + public function provideDependencies(Container $container): void + { + $this->container = $container; + } +} \ No newline at end of file diff --git a/Client/OAuth2KeycloakProviderPluginConfiguration.php b/Client/OAuth2KeycloakProviderPluginConfiguration.php new file mode 100644 index 0000000..78fd96c --- /dev/null +++ b/Client/OAuth2KeycloakProviderPluginConfiguration.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Micro\Plugin\OAuth2\Keycloak\Client; + +use Micro\Plugin\OAuth2\Client\Configuration\Provider\OAuth2ClientProviderConfigurationInterface; +use Micro\Plugin\OAuth2\Client\OAuth2ClientPluginConfiguration; +use Micro\Plugin\OAuth2\Keycloak\Client\Configuration\Provider\ProviderConfiguration; + +/** + * @author Stanislau Komar + */ +class OAuth2KeycloakProviderPluginConfiguration extends OAuth2ClientPluginConfiguration +{ + /** + * @param string $providerName + * + * @return OAuth2ClientProviderConfigurationInterface + */ + public function getProviderConfiguration(string $providerName): OAuth2ClientProviderConfigurationInterface + { + return new ProviderConfiguration($this->configuration, $providerName); + } +} \ No newline at end of file diff --git a/Client/Provider/KeycloakProviderConfigurationInterface.php b/Client/Provider/KeycloakProviderConfigurationInterface.php new file mode 100644 index 0000000..94ee64b --- /dev/null +++ b/Client/Provider/KeycloakProviderConfigurationInterface.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Micro\Plugin\OAuth2\Keycloak\Client\Provider; + +use Micro\Plugin\OAuth2\Client\Configuration\Provider\OAuth2ClientProviderConfigurationInterface; + +/** + * @author Stanislau Komar + */ +interface KeycloakProviderConfigurationInterface extends OAuth2ClientProviderConfigurationInterface +{ + /** + * @return string + */ + public function getRealm(): string; + + /** + * @return array + */ + public function getScopesDefault(): array; + + /** + * @return string + */ + public function getScopesSeparator(): string; + + /** + * @return string|null + */ + public function getSecurityProvider(): string|null; +} \ No newline at end of file diff --git a/Client/Provider/OAuth2Provider.php b/Client/Provider/OAuth2Provider.php new file mode 100644 index 0000000..8feeaff --- /dev/null +++ b/Client/Provider/OAuth2Provider.php @@ -0,0 +1,161 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Micro\Plugin\OAuth2\Keycloak\Client\Provider; + +use League\OAuth2\Client\Provider\AbstractProvider; +use League\OAuth2\Client\Provider\Exception\IdentityProviderException; +use League\OAuth2\Client\Provider\ResourceOwnerInterface; +use League\OAuth2\Client\Token\AccessToken; +use League\OAuth2\Client\Tool\BearerAuthorizationTrait; +use Micro\Plugin\OAuth2\Client\Configuration\Provider\OAuth2ClientProviderConfigurationInterface; +use Micro\Plugin\Security\Facade\SecurityFacadeInterface; +use Psr\Http\Message\ResponseInterface; + +/** + * @author Stanislau Komar + */ +class OAuth2Provider extends AbstractProvider +{ + use BearerAuthorizationTrait; + + /** + * @param OAuth2ClientProviderConfigurationInterface $providerConfiguration + * @param SecurityFacadeInterface $securityFacade + */ + public function __construct( + private readonly OAuth2ClientProviderConfigurationInterface $providerConfiguration, + private readonly SecurityFacadeInterface $securityFacade + ) { + parent::__construct([ + 'authServerUrl' => $this->providerConfiguration->getUrlAuthorization(), + 'clientId' => $providerConfiguration->getClientId(), + 'clientSecret' => $this->providerConfiguration->getClientSecret(), + 'redirectUri' => $this->providerConfiguration->getUrlRedirect(), + ],[]); + } + + /** + * {@inheritDoc} + */ + public function getBaseAuthorizationUrl(): string + { + return $this->getBaseUrlWithRealm() . '/protocol/openid-connect/auth'; + } + + /** + * {@inheritDoc} + */ + public function getBaseAccessTokenUrl(array $params): string + { + return $this->getBaseUrlWithRealm() . '/protocol/openid-connect/token'; + } + + /** + * {@inheritDoc} + */ + public function getResourceOwnerDetailsUrl(AccessToken $token): string + { + return $this->getBaseUrlWithRealm() . '/protocol/openid-connect/userinfo'; + } + + /** + * @return string + */ + protected function getBaseUrlWithRealm(): string + { + return + $this->providerConfiguration->getUrlAuthorization() . + '/realms/' . + $this->providerConfiguration->getRealm(); + } + + /** + * {@inheritDoc} + */ + protected function getDefaultScopes(): array + { + return $this->providerConfiguration->getScopesDefault(); + } + + /** + * {@inheritDoc} + */ + protected function checkResponse(ResponseInterface $response, $data): void + { + if(is_string($data)) { + throw new IdentityProviderException('Invalid response data', 0, $data); + } + + if (empty($data['error'])) { + return; + } + + $error = $data['error']; + + if(isset($data['error_description'])){ + $error .= ': ' . $data['error_description']; + } + + throw new IdentityProviderException($error, 0, $data); + } + + /** + * {@inheritDoc} + */ + public function getResourceOwner(AccessToken $token): ResourceOwnerInterface + { + $response = $this->fetchResourceOwnerDetails($token); + if (array_key_exists('jwt', $response)) { + $response = $response['jwt']; + } + + $response = $this->decryptResponse($response); + + return $this->createResourceOwner($response, $token); + } + + /** + * Attempts to decrypt the given response. + * + * @param string|array $response + * + * @return array|null + */ + public function decryptResponse(string|array $response): array|null + { + if (!is_string($response)) { + return $response; + } + + return $this->securityFacade + ->decodeToken($response) + ->getParameters(); + } + + /** + * {@inheritDoc} + */ + protected function getScopeSeparator(): string + { + return $this->providerConfiguration->getScopesSeparator(); + } + + /** + * {@inheritDoc} + */ + protected function createResourceOwner(array $response, AccessToken $token): ResourceOwnerInterface + { + return new ResourceOwner($response); + } +} \ No newline at end of file diff --git a/Client/Provider/ResourceOwner.php b/Client/Provider/ResourceOwner.php new file mode 100644 index 0000000..3ac09a3 --- /dev/null +++ b/Client/Provider/ResourceOwner.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Micro\Plugin\OAuth2\Keycloak\Client\Provider; + +use League\OAuth2\Client\Provider\ResourceOwnerInterface; + +/** + * @author Stanislau Komar + */ +readonly class ResourceOwner implements ResourceOwnerInterface +{ + /** + * Creates new resource owner. + * + * @param array $response Raw response + */ + public function __construct(protected array $response = []) + { + } + + /** + * Get resource owner id + * + * @return string|null + */ + public function getId(): string|null + { + return $this->getTokenParameter('sub'); + } + + /** + * Get resource owner email + * + * @return string|null + */ + public function getEmail(): string|null + { + return $this->getTokenParameter('email'); + } + + /** + * Get resource owner name + * + * @return string|null + */ + public function getName(): string|null + { + return $this->getTokenParameter('name'); + } + + /** + * @param string $parameter + * + * @return string|null + */ + protected function getTokenParameter(string $parameter): string|null + { + return \array_key_exists($parameter, $this->response) ? $this->response[$parameter] : null; + } + + /** + * Return all the owner details available as an array. + * + * @return array + */ + public function toArray(): array + { + return $this->response; + } +} \ No newline at end of file diff --git a/Client/composer.json b/Client/composer.json new file mode 100644 index 0000000..65052d8 --- /dev/null +++ b/Client/composer.json @@ -0,0 +1,25 @@ +{ + "name": "micro/plugin-oauth2-client-keycloak", + "description": "Micro Framework: Adapter for using Keycloak as an authorization server based on the OAuth2 protocol.", + "license": "MIT", + "type": "micro-library", + "authors": [ + { + "name": "Stanislau Komar", + "email": "stanislau_komar@epam.com" + } + ], + "require": { + "micro/plugin-oauth2-client": "^2.0", + "micro/dependency-injection": "^2.0", + "micro/kernel-boot-dependency": "^2.0", + "micro/kernel-boot-configuration": "^2.0", + "micro/plugin-security": "^2.0" + }, + "minimum-stability": "dev", + "autoload": { + "psr-4": { + "Micro\\Plugin\\OAuth2\\Keycloak\\Client\\": "/" + } + } +}