From 941ae9ea341f33f0348caef727f71381412a304a Mon Sep 17 00:00:00 2001 From: Andrej Hudec Date: Tue, 5 Sep 2023 06:16:10 +0200 Subject: [PATCH 1/7] Change cookie domain to accept empty value --- src/DependencyInjection/Configuration.php | 3 +- .../AnzuSystemsAuthExtensionTest.php | 150 ++++++++++++++++++ 2 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 8eea8d2..b0e9b87 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -37,8 +37,9 @@ private function addCookieSection(): NodeDefinition { return (new TreeBuilder('cookie'))->getRootNode() ->isRequired() + ->addDefaultsIfNotSet() ->children() - ->scalarNode('domain')->isRequired()->cannotBeEmpty()->end() + ->scalarNode('domain')->defaultValue(null)->end() ->booleanNode('secure')->isRequired()->end() ->scalarNode('device_id_name')->defaultValue('anz_di')->end() ->arrayNode('jwt') diff --git a/tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php b/tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php new file mode 100644 index 0000000..eb71e15 --- /dev/null +++ b/tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php @@ -0,0 +1,150 @@ +configuration = null; + } + + public function testEmptyConfiguration(): void + { + $this->configuration = new ContainerBuilder(); + $loader = new AnzuSystemsAuthExtension(); + $config = $this->getEmptyConfig(); + $loader->load([$config], $this->configuration); + + $this->assertParameter(null, 'anzu_systems.auth_bundle.cookie.domain'); + $this->assertParameter(true, 'anzu_systems.auth_bundle.cookie.secure'); + $this->assertParameter('anz_di', 'anzu_systems.auth_bundle.cookie.device_id_name'); + $this->assertParameter('anz_jp', 'anzu_systems.auth_bundle.cookie.jwt.payload_part_name'); + $this->assertParameter('anz_js', 'anzu_systems.auth_bundle.cookie.jwt.signature_part_name'); + $this->assertParameter('anz_js', 'anzu_systems.auth_bundle.cookie.jwt.signature_part_name'); + + $this->assertParameter('anz_rt', 'anzu_systems.auth_bundle.cookie.refresh_token.name'); + $this->assertParameter(31536000, 'anzu_systems.auth_bundle.cookie.refresh_token.lifetime'); + $this->assertParameter('anz_rte', 'anzu_systems.auth_bundle.cookie.refresh_token.existence_name'); + + $this->assertParameter('anz', 'anzu_systems.auth_bundle.jwt.audience'); + $this->assertParameter('ES256', 'anzu_systems.auth_bundle.jwt.algorithm'); + $this->assertParameter('foo_public_cert', 'anzu_systems.auth_bundle.jwt.public_cert'); + $this->assertParameter('foo_private_cert', 'anzu_systems.auth_bundle.jwt.private_cert'); + $this->assertParameter(3600, 'anzu_systems.auth_bundle.jwt.lifetime'); + + $this->assertNotHasDefinition(AuthenticationSuccessHandler::class); + $this->assertNotHasDefinition(AuthenticationFailureHandler::class); + $this->assertNotHasDefinition(GrantAccessOnResponseProcess::class); + $this->assertNotHasDefinition(RefreshTokenProcess::class); + $this->assertNotHasDefinition(LogoutListener::class); + } + + public function testFullConfiguration(): void + { + $this->configuration = new ContainerBuilder(); + $loader = new AnzuSystemsAuthExtension(); + $config = $this->getFullConfig(); + $loader->load([$config], $this->configuration); + + $this->assertParameter('.example.com', 'anzu_systems.auth_bundle.cookie.domain'); + $this->assertParameter(true, 'anzu_systems.auth_bundle.cookie.secure'); + $this->assertParameter('anz_di', 'anzu_systems.auth_bundle.cookie.device_id_name'); + $this->assertParameter('anz_jp', 'anzu_systems.auth_bundle.cookie.jwt.payload_part_name'); + $this->assertParameter('anz_js', 'anzu_systems.auth_bundle.cookie.jwt.signature_part_name'); + $this->assertParameter('anz_js', 'anzu_systems.auth_bundle.cookie.jwt.signature_part_name'); + + $this->assertParameter('anz_rt', 'anzu_systems.auth_bundle.cookie.refresh_token.name'); + $this->assertParameter(31536000, 'anzu_systems.auth_bundle.cookie.refresh_token.lifetime'); + $this->assertParameter('anz_rte', 'anzu_systems.auth_bundle.cookie.refresh_token.existence_name'); + + $this->assertParameter('anz', 'anzu_systems.auth_bundle.jwt.audience'); + $this->assertParameter('ES256', 'anzu_systems.auth_bundle.jwt.algorithm'); + $this->assertParameter('foo_public_cert', 'anzu_systems.auth_bundle.jwt.public_cert'); + $this->assertParameter('foo_private_cert', 'anzu_systems.auth_bundle.jwt.private_cert'); + $this->assertParameter(3600, 'anzu_systems.auth_bundle.jwt.lifetime'); + + + $this->assertHasDefinition(AuthenticationSuccessHandler::class); + $this->assertHasDefinition(AuthenticationFailureHandler::class); + $this->assertHasDefinition(GrantAccessOnResponseProcess::class); + $this->assertHasDefinition(RefreshTokenProcess::class); + $this->assertHasDefinition(LogoutListener::class); + } + + private function getEmptyConfig(): ?array + { + $yaml = <<parse($yaml); + } + + private function getFullConfig(): array + { + $yaml = <<parse($yaml); + } + + private function assertParameter($value, string $key): void + { + self::assertSame($value, $this->configuration->getParameter($key), sprintf('%s parameter is correct', $key)); + } + + private function assertHasDefinition(string $id): void + { + self::assertTrue(($this->configuration->hasDefinition($id) || $this->configuration->hasAlias($id))); + } + + private function assertNotHasDefinition(string $id): void + { + self::assertFalse(($this->configuration->hasDefinition($id) || $this->configuration->hasAlias($id))); + } +} From aa83ea017aeaa6d57c9b5e29cebd22496a517199 Mon Sep 17 00:00:00 2001 From: Andrej Hudec Date: Tue, 5 Sep 2023 08:02:30 +0200 Subject: [PATCH 2/7] Allow to have null in cookie domain --- src/Configuration/CookieConfiguration.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Configuration/CookieConfiguration.php b/src/Configuration/CookieConfiguration.php index 2abb89b..4fc906a 100644 --- a/src/Configuration/CookieConfiguration.php +++ b/src/Configuration/CookieConfiguration.php @@ -7,7 +7,7 @@ final class CookieConfiguration { public function __construct( - private readonly string $domain, + private readonly ?string $domain, private readonly bool $secure, private readonly string $jwtPayloadCookieName, private readonly string $jwtSignatureCookieName, @@ -18,7 +18,7 @@ public function __construct( ) { } - public function getDomain(): string + public function getDomain(): ?string { return $this->domain; } From cf6b5053530746cac4107606aa6c985fb00bd11d Mon Sep 17 00:00:00 2001 From: Andrej Hudec Date: Tue, 5 Sep 2023 14:18:18 +0200 Subject: [PATCH 3/7] Configure scope delimiter --- src/Configuration/OAuth2Configuration.php | 3 ++- .../AnzuSystemsAuthExtension.php | 1 + src/DependencyInjection/Configuration.php | 1 + .../AnzuSystemsAuthExtensionTest.php | 26 +++++++++++++++++-- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/Configuration/OAuth2Configuration.php b/src/Configuration/OAuth2Configuration.php index cd45808..d9b9ace 100644 --- a/src/Configuration/OAuth2Configuration.php +++ b/src/Configuration/OAuth2Configuration.php @@ -22,6 +22,7 @@ public function __construct( private readonly string $ssoClientSecret, private readonly string $ssoPublicCert, private readonly array $ssoScopes, + private readonly string $ssoScopeDelimiter, private readonly CacheItemPoolInterface $accessTokenCachePool, ) { } @@ -57,7 +58,7 @@ public function getResolvedSsoAuthorizeUrl(string $state): string 'response_type' => 'code', 'state' => $state, 'redirect_uri' => $this->getSsoRedirectUrl(), - 'scope' => implode(',', $this->getSsoScopes()), + 'scope' => implode($this->ssoScopeDelimiter, $this->getSsoScopes()), ]) ); } diff --git a/src/DependencyInjection/AnzuSystemsAuthExtension.php b/src/DependencyInjection/AnzuSystemsAuthExtension.php index 31c8fc9..5e120be 100644 --- a/src/DependencyInjection/AnzuSystemsAuthExtension.php +++ b/src/DependencyInjection/AnzuSystemsAuthExtension.php @@ -100,6 +100,7 @@ public function load(array $configs, ContainerBuilder $container): void ->setArgument('$ssoClientSecret', $oauth2Section['client_secret']) ->setArgument('$ssoPublicCert', $oauth2Section['public_cert']) ->setArgument('$ssoScopes', $oauth2Section['scopes']) + ->setArgument('$ssoScopeDelimiter', $oauth2Section['scope_delimiter']) ->setArgument('$accessTokenCachePool', new Reference($oauth2Section['access_token_cache'])) ; diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index b0e9b87..2816aa7 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -149,6 +149,7 @@ private function addOAuth2AuthorizationSection(): NodeDefinition ->scalarNode('client_id')->defaultValue('')->end() ->scalarNode('client_secret')->defaultValue('')->end() ->scalarNode('public_cert')->defaultValue('')->end() + ->enumNode('scope_delimiter')->values([' ', ','])->defaultValue(',')->end() ->arrayNode('scopes')->scalarPrototype()->end()->end() ->end() ; diff --git a/tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php b/tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php index eb71e15..0f55448 100644 --- a/tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php +++ b/tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php @@ -4,6 +4,7 @@ namespace AnzuSystems\AuthBundle\Tests\DependencyInjection; +use AnzuSystems\AuthBundle\Configuration\OAuth2Configuration; use AnzuSystems\AuthBundle\DependencyInjection\AnzuSystemsAuthExtension; use AnzuSystems\AuthBundle\Domain\Process\GrantAccessOnResponseProcess; use AnzuSystems\AuthBundle\Domain\Process\RefreshTokenProcess; @@ -52,6 +53,7 @@ public function testEmptyConfiguration(): void $this->assertNotHasDefinition(GrantAccessOnResponseProcess::class); $this->assertNotHasDefinition(RefreshTokenProcess::class); $this->assertNotHasDefinition(LogoutListener::class); + $this->assertNotHasDefinition(OAuth2Configuration::class); } public function testFullConfiguration(): void @@ -84,6 +86,21 @@ public function testFullConfiguration(): void $this->assertHasDefinition(GrantAccessOnResponseProcess::class); $this->assertHasDefinition(RefreshTokenProcess::class); $this->assertHasDefinition(LogoutListener::class); + + $this->assertHasDefinition(OAuth2Configuration::class); + + $oAuth2ConfigurationDefinition = $this->configuration->getDefinition(OAuth2Configuration::class); + $arguments = $oAuth2ConfigurationDefinition->getArguments(); + self::assertSame('https://example.com/access-token-url', $arguments['$ssoAccessTokenUrl']); + self::assertSame('https://example.com/authorize-url', $arguments['$ssoAuthorizeUrl']); + self::assertSame('https://example.com/redirect-url', $arguments['$ssoRedirectUrl']); + self::assertSame('https://example.com/user-info-url', $arguments['$ssoUserInfoUrl']); + self::assertSame('AnzuSystems\AuthBundle\Model\SsoUserDto', $arguments['$ssoUserInfoClass']); + self::assertSame('qux', $arguments['$ssoClientId']); + self::assertSame('bar-secret', $arguments['$ssoClientSecret']); + self::assertSame('qux-public-cert', $arguments['$ssoPublicCert']); + self::assertSame(['email', 'profile'], $arguments['$ssoScopes']); + self::assertSame(' ', $arguments['$ssoScopeDelimiter']); } private function getEmptyConfig(): ?array @@ -120,13 +137,18 @@ private function getFullConfig(): array type: oauth2 oauth2: user_repository_service_id: App\Repository\UserRepository - authorize_url: 'https://example.com/authorize-url%' + authorize_url: 'https://example.com/authorize-url' + user_info_url: 'https://example.com/user-info-url' state_token_salt: 'qux-quux' access_token_url: 'https://example.com/access-token-url' redirect_url: 'https://example.com/redirect-url' - client_id: anzusystems-forum + client_id: qux client_secret: 'bar-secret' public_cert: 'qux-public-cert' + scopes: + - email + - profile + scope_delimiter: ' ' EOF; $parser = new Parser(); From 34b39cacb86745cca6eb5ed40af6ff45ca0c4f71 Mon Sep 17 00:00:00 2001 From: Andrej Hudec Date: Tue, 5 Sep 2023 15:11:09 +0200 Subject: [PATCH 4/7] Allow to authenticate user by SSO email --- src/Configuration/OAuth2Configuration.php | 6 +++- .../OAuth2AuthUserRepositoryInterface.php | 1 + .../AnzuSystemsAuthExtension.php | 1 + src/DependencyInjection/Configuration.php | 6 ++++ .../GrantAccessByOAuth2TokenProcess.php | 28 ++++++++++++++-- src/HttpClient/OAuth2HttpClient.php | 2 +- .../AnzuSystemsAuthExtensionTest.php | 33 +++++++++++-------- 7 files changed, 59 insertions(+), 18 deletions(-) diff --git a/src/Configuration/OAuth2Configuration.php b/src/Configuration/OAuth2Configuration.php index d9b9ace..239eafb 100644 --- a/src/Configuration/OAuth2Configuration.php +++ b/src/Configuration/OAuth2Configuration.php @@ -37,8 +37,12 @@ public function getSsoAuthorizeUrl(): string return $this->ssoAuthorizeUrl; } - public function getSsoUserInfoUrl(string $userId): string + public function getSsoUserInfoUrl(?string $userId): string { + if (!$userId) { + return $this->ssoUserInfoUrl; + } + return str_replace(self::SSO_USER_ID_PLACEHOLDER_URL, $userId, $this->ssoUserInfoUrl); } diff --git a/src/Contracts/OAuth2AuthUserRepositoryInterface.php b/src/Contracts/OAuth2AuthUserRepositoryInterface.php index 9da952f..32e803c 100644 --- a/src/Contracts/OAuth2AuthUserRepositoryInterface.php +++ b/src/Contracts/OAuth2AuthUserRepositoryInterface.php @@ -7,4 +7,5 @@ interface OAuth2AuthUserRepositoryInterface { public function findOneBySsoUserId(string $ssoUserId): ?AnzuAuthUserInterface; + public function findOneByEmail(string $email): ?AnzuAuthUserInterface; } diff --git a/src/DependencyInjection/AnzuSystemsAuthExtension.php b/src/DependencyInjection/AnzuSystemsAuthExtension.php index 5e120be..4c3825e 100644 --- a/src/DependencyInjection/AnzuSystemsAuthExtension.php +++ b/src/DependencyInjection/AnzuSystemsAuthExtension.php @@ -117,6 +117,7 @@ public function load(array $configs, ContainerBuilder $container): void ->register(GrantAccessByOAuth2TokenProcess::class) ->setAutowired(true) ->setAutoconfigured(true) + ->setArgument('$authMethod', $oauth2Section['auth_method']) ; $container diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 2816aa7..afec49b 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -5,6 +5,7 @@ namespace AnzuSystems\AuthBundle\DependencyInjection; use AnzuSystems\AuthBundle\Configuration\OAuth2Configuration; +use AnzuSystems\AuthBundle\Domain\Process\OAuth2\GrantAccessByOAuth2TokenProcess; use AnzuSystems\AuthBundle\Model\Enum\AuthType; use AnzuSystems\AuthBundle\Model\Enum\JwtAlgorithm; use AnzuSystems\AuthBundle\Model\SsoUserDto; @@ -151,6 +152,11 @@ private function addOAuth2AuthorizationSection(): NodeDefinition ->scalarNode('public_cert')->defaultValue('')->end() ->enumNode('scope_delimiter')->values([' ', ','])->defaultValue(',')->end() ->arrayNode('scopes')->scalarPrototype()->end()->end() + ->booleanNode('auth_by_email')->defaultFalse()->end() + ->enumNode('auth_method') + ->values([GrantAccessByOAuth2TokenProcess::AUTH_METHOD_SSO_ID, GrantAccessByOAuth2TokenProcess::AUTH_METHOD_SSO_EMAIL]) + ->defaultValue(GrantAccessByOAuth2TokenProcess::AUTH_METHOD_SSO_ID) + ->end() ->end() ; } diff --git a/src/Domain/Process/OAuth2/GrantAccessByOAuth2TokenProcess.php b/src/Domain/Process/OAuth2/GrantAccessByOAuth2TokenProcess.php index 4d043c5..e95d8b2 100644 --- a/src/Domain/Process/OAuth2/GrantAccessByOAuth2TokenProcess.php +++ b/src/Domain/Process/OAuth2/GrantAccessByOAuth2TokenProcess.php @@ -8,11 +8,13 @@ use AnzuSystems\AuthBundle\Domain\Process\GrantAccessOnResponseProcess; use AnzuSystems\AuthBundle\Exception\InvalidJwtException; use AnzuSystems\AuthBundle\Exception\UnsuccessfulAccessTokenRequestException; +use AnzuSystems\AuthBundle\Exception\UnsuccessfulUserInfoRequestException; use AnzuSystems\AuthBundle\HttpClient\OAuth2HttpClient; use AnzuSystems\AuthBundle\Model\Enum\UserOAuthLoginState; use AnzuSystems\AuthBundle\Util\HttpUtil; use AnzuSystems\CommonBundle\Log\Factory\LogContextFactory; use AnzuSystems\CommonBundle\Traits\SerializerAwareTrait; +use AnzuSystems\Contracts\Exception\AnzuException; use AnzuSystems\SerializerBundle\Exception\SerializerException; use Exception; use Lcobucci\JWT\Token\RegisteredClaims; @@ -26,19 +28,24 @@ final class GrantAccessByOAuth2TokenProcess { use SerializerAwareTrait; + public const AUTH_METHOD_SSO_ID = 'sso_id'; + public const AUTH_METHOD_SSO_EMAIL = 'sso_email'; + public function __construct( private readonly OAuth2HttpClient $OAuth2HttpClient, private readonly GrantAccessOnResponseProcess $grantAccessOnResponseProcess, private readonly ValidateOAuth2AccessTokenProcess $validateOAuth2AccessTokenProcess, - private readonly OAuth2AuthUserRepositoryInterface $OAuth2AuthUserRepository, + private readonly OAuth2AuthUserRepositoryInterface $oAuth2AuthUserRepository, private readonly HttpUtil $httpUtil, private readonly LoggerInterface $appLogger, private readonly LogContextFactory $contextFactory, + private readonly string $authMethod, ) { } /** * @throws SerializerException + * @throws AnzuException */ public function execute(Request $request): Response { @@ -54,13 +61,28 @@ public function execute(Request $request): Response try { $this->validateOAuth2AccessTokenProcess->execute($ssoJwt->getAccessToken()); - $ssoUserId = (string) $ssoJwt->getAccessToken()->claims()->get(RegisteredClaims::SUBJECT); } catch (InvalidJwtException $exception) { $this->logException($request, $exception); return $this->createRedirectResponseForRequest($request, UserOAuthLoginState::FailureUnauthorized); } - $authUser = $this->OAuth2AuthUserRepository->findOneBySsoUserId($ssoUserId); + + if (self::AUTH_METHOD_SSO_EMAIL === $this->authMethod) { + try { + $ssoUser = $this->OAuth2HttpClient->getSsoUserInfo(); + } catch (UnsuccessfulUserInfoRequestException | UnsuccessfulAccessTokenRequestException $exception) { + $this->logException($request, $exception); + + return $this->createRedirectResponseForRequest($request, UserOAuthLoginState::FailureSsoCommunicationFailed); + } + $authUser = $this->oAuth2AuthUserRepository->findOneByEmail($ssoUser->getEmail()); + } else if (self::AUTH_METHOD_SSO_ID === $this->authMethod) { + $ssoUserId = (string)$ssoJwt->getAccessToken()->claims()->get(RegisteredClaims::SUBJECT); + $authUser = $this->oAuth2AuthUserRepository->findOneBySsoUserId($ssoUserId); + } else { + throw new AnzuException(sprintf('Unknown auth method "%s".', $this->authMethod)); + } + if (null === $authUser || false === $authUser->isEnabled()) { return $this->createRedirectResponseForRequest($request, UserOAuthLoginState::FailureUnauthorized); } diff --git a/src/HttpClient/OAuth2HttpClient.php b/src/HttpClient/OAuth2HttpClient.php index 95c4cc5..5682fb9 100644 --- a/src/HttpClient/OAuth2HttpClient.php +++ b/src/HttpClient/OAuth2HttpClient.php @@ -46,7 +46,7 @@ public function requestAccessTokenByAuthCode(string $code): AccessTokenResponseD * @throws UnsuccessfulAccessTokenRequestException * @throws UnsuccessfulUserInfoRequestException */ - public function getSsoUserInfo(string $id): SsoUserDto + public function getSsoUserInfo(?string $id = null): SsoUserDto { try { $response = $this->client->request( diff --git a/tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php b/tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php index 0f55448..7fc7dbf 100644 --- a/tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php +++ b/tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php @@ -7,6 +7,7 @@ use AnzuSystems\AuthBundle\Configuration\OAuth2Configuration; use AnzuSystems\AuthBundle\DependencyInjection\AnzuSystemsAuthExtension; use AnzuSystems\AuthBundle\Domain\Process\GrantAccessOnResponseProcess; +use AnzuSystems\AuthBundle\Domain\Process\OAuth2\GrantAccessByOAuth2TokenProcess; use AnzuSystems\AuthBundle\Domain\Process\RefreshTokenProcess; use AnzuSystems\AuthBundle\Event\Listener\LogoutListener; use AnzuSystems\AuthBundle\Security\AuthenticationFailureHandler; @@ -54,6 +55,7 @@ public function testEmptyConfiguration(): void $this->assertNotHasDefinition(RefreshTokenProcess::class); $this->assertNotHasDefinition(LogoutListener::class); $this->assertNotHasDefinition(OAuth2Configuration::class); + $this->assertNotHasDefinition(GrantAccessByOAuth2TokenProcess::class); } public function testFullConfiguration(): void @@ -88,19 +90,23 @@ public function testFullConfiguration(): void $this->assertHasDefinition(LogoutListener::class); $this->assertHasDefinition(OAuth2Configuration::class); - $oAuth2ConfigurationDefinition = $this->configuration->getDefinition(OAuth2Configuration::class); - $arguments = $oAuth2ConfigurationDefinition->getArguments(); - self::assertSame('https://example.com/access-token-url', $arguments['$ssoAccessTokenUrl']); - self::assertSame('https://example.com/authorize-url', $arguments['$ssoAuthorizeUrl']); - self::assertSame('https://example.com/redirect-url', $arguments['$ssoRedirectUrl']); - self::assertSame('https://example.com/user-info-url', $arguments['$ssoUserInfoUrl']); - self::assertSame('AnzuSystems\AuthBundle\Model\SsoUserDto', $arguments['$ssoUserInfoClass']); - self::assertSame('qux', $arguments['$ssoClientId']); - self::assertSame('bar-secret', $arguments['$ssoClientSecret']); - self::assertSame('qux-public-cert', $arguments['$ssoPublicCert']); - self::assertSame(['email', 'profile'], $arguments['$ssoScopes']); - self::assertSame(' ', $arguments['$ssoScopeDelimiter']); + $oAuth2ConfArguments = $oAuth2ConfigurationDefinition->getArguments(); + self::assertSame('https://example.com/access-token-url', $oAuth2ConfArguments['$ssoAccessTokenUrl']); + self::assertSame('https://example.com/authorize-url', $oAuth2ConfArguments['$ssoAuthorizeUrl']); + self::assertSame('https://example.com/redirect-url', $oAuth2ConfArguments['$ssoRedirectUrl']); + self::assertSame('https://example.com/user-info-url', $oAuth2ConfArguments['$ssoUserInfoUrl']); + self::assertSame('AnzuSystems\AuthBundle\Model\SsoUserDto', $oAuth2ConfArguments['$ssoUserInfoClass']); + self::assertSame('qux', $oAuth2ConfArguments['$ssoClientId']); + self::assertSame('bar-secret', $oAuth2ConfArguments['$ssoClientSecret']); + self::assertSame('qux-public-cert', $oAuth2ConfArguments['$ssoPublicCert']); + self::assertSame(['email', 'profile'], $oAuth2ConfArguments['$ssoScopes']); + self::assertSame(' ', $oAuth2ConfArguments['$ssoScopeDelimiter']); + + $this->assertHasDefinition(GrantAccessByOAuth2TokenProcess::class); + $grantProcessDefinition = $this->configuration->getDefinition(GrantAccessByOAuth2TokenProcess::class); + $grantProcessArguments = $grantProcessDefinition->getArguments(); + self::assertSame('sso_email', $grantProcessArguments['$authMethod']); } private function getEmptyConfig(): ?array @@ -148,7 +154,8 @@ private function getFullConfig(): array scopes: - email - profile - scope_delimiter: ' ' + scope_delimiter: ' ' + auth_method: sso_email EOF; $parser = new Parser(); From 21bc6b0600e1c53f5a7b5712faba64f10a5e652e Mon Sep 17 00:00:00 2001 From: Andrej Hudec Date: Tue, 5 Sep 2023 15:22:19 +0200 Subject: [PATCH 5/7] Rename method `findOneByEmail` to `findOneBySsoEmail` --- src/Contracts/OAuth2AuthUserRepositoryInterface.php | 2 +- src/Domain/Process/OAuth2/GrantAccessByOAuth2TokenProcess.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Contracts/OAuth2AuthUserRepositoryInterface.php b/src/Contracts/OAuth2AuthUserRepositoryInterface.php index 32e803c..1195da0 100644 --- a/src/Contracts/OAuth2AuthUserRepositoryInterface.php +++ b/src/Contracts/OAuth2AuthUserRepositoryInterface.php @@ -7,5 +7,5 @@ interface OAuth2AuthUserRepositoryInterface { public function findOneBySsoUserId(string $ssoUserId): ?AnzuAuthUserInterface; - public function findOneByEmail(string $email): ?AnzuAuthUserInterface; + public function findOneBySsoEmail(string $email): ?AnzuAuthUserInterface; } diff --git a/src/Domain/Process/OAuth2/GrantAccessByOAuth2TokenProcess.php b/src/Domain/Process/OAuth2/GrantAccessByOAuth2TokenProcess.php index e95d8b2..f5a4205 100644 --- a/src/Domain/Process/OAuth2/GrantAccessByOAuth2TokenProcess.php +++ b/src/Domain/Process/OAuth2/GrantAccessByOAuth2TokenProcess.php @@ -75,7 +75,7 @@ public function execute(Request $request): Response return $this->createRedirectResponseForRequest($request, UserOAuthLoginState::FailureSsoCommunicationFailed); } - $authUser = $this->oAuth2AuthUserRepository->findOneByEmail($ssoUser->getEmail()); + $authUser = $this->oAuth2AuthUserRepository->findOneBySsoEmail($ssoUser->getEmail()); } else if (self::AUTH_METHOD_SSO_ID === $this->authMethod) { $ssoUserId = (string)$ssoJwt->getAccessToken()->claims()->get(RegisteredClaims::SUBJECT); $authUser = $this->oAuth2AuthUserRepository->findOneBySsoUserId($ssoUserId); From fcc4ab5187c345acbcc527dc25d01e0417429b56 Mon Sep 17 00:00:00 2001 From: Andrej Hudec Date: Tue, 5 Sep 2023 19:58:08 +0200 Subject: [PATCH 6/7] Accept opaque oAuth2 access tokens --- src/Configuration/OAuth2Configuration.php | 6 ++ .../AnzuSystemsAuthExtension.php | 1 + src/DependencyInjection/Configuration.php | 2 +- .../GrantAccessByOAuth2TokenProcess.php | 69 ++++++++++++++----- src/HttpClient/OAuth2HttpClient.php | 27 +++++--- src/Model/AccessTokenDto.php | 56 +++++++++++++++ src/Model/Enum/JwtAlgorithm.php | 2 +- src/Model/OpaqueAccessTokenResponseDto.php | 40 +++++++++++ .../AnzuSystemsAuthExtensionTest.php | 2 + 9 files changed, 174 insertions(+), 31 deletions(-) create mode 100644 src/Model/AccessTokenDto.php create mode 100644 src/Model/OpaqueAccessTokenResponseDto.php diff --git a/src/Configuration/OAuth2Configuration.php b/src/Configuration/OAuth2Configuration.php index 239eafb..784871d 100644 --- a/src/Configuration/OAuth2Configuration.php +++ b/src/Configuration/OAuth2Configuration.php @@ -23,6 +23,7 @@ public function __construct( private readonly string $ssoPublicCert, private readonly array $ssoScopes, private readonly string $ssoScopeDelimiter, + private readonly bool $considerAccessTokenAsJwt, private readonly CacheItemPoolInterface $accessTokenCachePool, ) { } @@ -99,4 +100,9 @@ public function getAccessTokenCachePool(): CacheItemPoolInterface { return $this->accessTokenCachePool; } + + public function isAccessTokenConsideredJwt(): bool + { + return $this->considerAccessTokenAsJwt; + } } diff --git a/src/DependencyInjection/AnzuSystemsAuthExtension.php b/src/DependencyInjection/AnzuSystemsAuthExtension.php index 4c3825e..55d147c 100644 --- a/src/DependencyInjection/AnzuSystemsAuthExtension.php +++ b/src/DependencyInjection/AnzuSystemsAuthExtension.php @@ -101,6 +101,7 @@ public function load(array $configs, ContainerBuilder $container): void ->setArgument('$ssoPublicCert', $oauth2Section['public_cert']) ->setArgument('$ssoScopes', $oauth2Section['scopes']) ->setArgument('$ssoScopeDelimiter', $oauth2Section['scope_delimiter']) + ->setArgument('$considerAccessTokenAsJwt', $oauth2Section['consider_access_token_as_jwt']) ->setArgument('$accessTokenCachePool', new Reference($oauth2Section['access_token_cache'])) ; diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index afec49b..0dd4757 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -152,7 +152,7 @@ private function addOAuth2AuthorizationSection(): NodeDefinition ->scalarNode('public_cert')->defaultValue('')->end() ->enumNode('scope_delimiter')->values([' ', ','])->defaultValue(',')->end() ->arrayNode('scopes')->scalarPrototype()->end()->end() - ->booleanNode('auth_by_email')->defaultFalse()->end() + ->booleanNode('consider_access_token_as_jwt')->defaultTrue()->end() ->enumNode('auth_method') ->values([GrantAccessByOAuth2TokenProcess::AUTH_METHOD_SSO_ID, GrantAccessByOAuth2TokenProcess::AUTH_METHOD_SSO_EMAIL]) ->defaultValue(GrantAccessByOAuth2TokenProcess::AUTH_METHOD_SSO_ID) diff --git a/src/Domain/Process/OAuth2/GrantAccessByOAuth2TokenProcess.php b/src/Domain/Process/OAuth2/GrantAccessByOAuth2TokenProcess.php index f5a4205..8f827c9 100644 --- a/src/Domain/Process/OAuth2/GrantAccessByOAuth2TokenProcess.php +++ b/src/Domain/Process/OAuth2/GrantAccessByOAuth2TokenProcess.php @@ -4,12 +4,14 @@ namespace AnzuSystems\AuthBundle\Domain\Process\OAuth2; +use AnzuSystems\AuthBundle\Contracts\AnzuAuthUserInterface; use AnzuSystems\AuthBundle\Contracts\OAuth2AuthUserRepositoryInterface; use AnzuSystems\AuthBundle\Domain\Process\GrantAccessOnResponseProcess; use AnzuSystems\AuthBundle\Exception\InvalidJwtException; use AnzuSystems\AuthBundle\Exception\UnsuccessfulAccessTokenRequestException; use AnzuSystems\AuthBundle\Exception\UnsuccessfulUserInfoRequestException; use AnzuSystems\AuthBundle\HttpClient\OAuth2HttpClient; +use AnzuSystems\AuthBundle\Model\AccessTokenDto; use AnzuSystems\AuthBundle\Model\Enum\UserOAuthLoginState; use AnzuSystems\AuthBundle\Util\HttpUtil; use AnzuSystems\CommonBundle\Log\Factory\LogContextFactory; @@ -52,35 +54,33 @@ public function execute(Request $request): Response $code = (string) $request->query->get('code'); try { - $ssoJwt = $this->OAuth2HttpClient->requestAccessTokenByAuthCode($code); + $accessTokenDto = $this->OAuth2HttpClient->requestAccessTokenByAuthCode($code); } catch (UnsuccessfulAccessTokenRequestException $exception) { $this->logException($request, $exception); return $this->createRedirectResponseForRequest($request, UserOAuthLoginState::FailureSsoCommunicationFailed); } - try { - $this->validateOAuth2AccessTokenProcess->execute($ssoJwt->getAccessToken()); - } catch (InvalidJwtException $exception) { - $this->logException($request, $exception); - - return $this->createRedirectResponseForRequest($request, UserOAuthLoginState::FailureUnauthorized); - } - - if (self::AUTH_METHOD_SSO_EMAIL === $this->authMethod) { + if ($accessTokenDto->getJwt()) { + // validate jwt try { - $ssoUser = $this->OAuth2HttpClient->getSsoUserInfo(); - } catch (UnsuccessfulUserInfoRequestException | UnsuccessfulAccessTokenRequestException $exception) { + $this->validateOAuth2AccessTokenProcess->execute($accessTokenDto->getJwt()); + } catch (InvalidJwtException $exception) { $this->logException($request, $exception); - return $this->createRedirectResponseForRequest($request, UserOAuthLoginState::FailureSsoCommunicationFailed); + return $this->createRedirectResponseForRequest($request, UserOAuthLoginState::FailureUnauthorized); } - $authUser = $this->oAuth2AuthUserRepository->findOneBySsoEmail($ssoUser->getEmail()); - } else if (self::AUTH_METHOD_SSO_ID === $this->authMethod) { - $ssoUserId = (string)$ssoJwt->getAccessToken()->claims()->get(RegisteredClaims::SUBJECT); - $authUser = $this->oAuth2AuthUserRepository->findOneBySsoUserId($ssoUserId); - } else { - throw new AnzuException(sprintf('Unknown auth method "%s".', $this->authMethod)); + } + + try { + $authUser = $this->getAuthUser($accessTokenDto); + } catch (UnsuccessfulUserInfoRequestException | UnsuccessfulAccessTokenRequestException $exception) { + $this->logException($request, $exception); + + return $this->createRedirectResponseForRequest( + $request, + UserOAuthLoginState::FailureSsoCommunicationFailed + ); } if (null === $authUser || false === $authUser->isEnabled()) { @@ -114,4 +114,35 @@ private function createRedirectResponseForRequest(Request $request, UserOAuthLog return new RedirectResponse($redirectUrl); } + + /** + * @throws AnzuException + * @throws UnsuccessfulAccessTokenRequestException + * @throws UnsuccessfulUserInfoRequestException + */ + private function getAuthUser(AccessTokenDto $accessTokenDto): ?AnzuAuthUserInterface + { + if (self::AUTH_METHOD_SSO_EMAIL === $this->authMethod) { + // fetch user info + $ssoUser = $this->OAuth2HttpClient->getSsoUserInfo(); + + return $this->oAuth2AuthUserRepository->findOneBySsoEmail($ssoUser->getEmail()); + } + + if (self::AUTH_METHOD_SSO_ID === $this->authMethod) { + // prefer to use the jwt + if ($accessTokenDto->getJwt()) { + $ssoUserId = (string) $accessTokenDto->getJwt()->claims()->get(RegisteredClaims::SUBJECT); + + return $this->oAuth2AuthUserRepository->findOneBySsoUserId($ssoUserId); + } + + // otherwise fetch user info + $ssoUser = $this->OAuth2HttpClient->getSsoUserInfo(); + + return $this->oAuth2AuthUserRepository->findOneBySsoUserId($ssoUser->getId()); + } + + throw new AnzuException(sprintf('Unknown auth method "%s".', $this->authMethod)); + } } diff --git a/src/HttpClient/OAuth2HttpClient.php b/src/HttpClient/OAuth2HttpClient.php index 5682fb9..e8bf0ff 100644 --- a/src/HttpClient/OAuth2HttpClient.php +++ b/src/HttpClient/OAuth2HttpClient.php @@ -7,12 +7,12 @@ use AnzuSystems\AuthBundle\Configuration\OAuth2Configuration; use AnzuSystems\AuthBundle\Exception\UnsuccessfulAccessTokenRequestException; use AnzuSystems\AuthBundle\Exception\UnsuccessfulUserInfoRequestException; +use AnzuSystems\AuthBundle\Model\AccessTokenDto; use AnzuSystems\AuthBundle\Model\AccessTokenResponseDto; +use AnzuSystems\AuthBundle\Model\OpaqueAccessTokenResponseDto; use AnzuSystems\AuthBundle\Model\SsoUserDto; -use AnzuSystems\CommonBundle\Log\Factory\LogContextFactory; use AnzuSystems\SerializerBundle\Exception\SerializerException; use AnzuSystems\SerializerBundle\Serializer; -use DateTimeInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -31,7 +31,7 @@ public function __construct( /** * @throws UnsuccessfulAccessTokenRequestException */ - public function requestAccessTokenByAuthCode(string $code): AccessTokenResponseDto + public function requestAccessTokenByAuthCode(string $code): AccessTokenDto { return $this->sendTokenRequest($this->configuration->getSsoAccessTokenUrl(), [ 'grant_type' => 'authorization_code', @@ -53,7 +53,7 @@ public function getSsoUserInfo(?string $id = null): SsoUserDto method: Request::METHOD_GET, url: $this->configuration->getSsoUserInfoUrl($id), options: [ - 'auth_bearer' => $this->requestAccessTokenForClientService()->getAccessToken()->toString(), + 'auth_bearer' => $this->requestAccessTokenForClientService()->getAccessToken(), ] ); @@ -70,7 +70,7 @@ public function getSsoUserInfo(?string $id = null): SsoUserDto * * @noinspection PhpDocMissingThrowsInspection */ - private function requestAccessTokenForClientService(): AccessTokenResponseDto + private function requestAccessTokenForClientService(): AccessTokenDto { $cachePool = $this->configuration->getAccessTokenCachePool(); /** @noinspection PhpUnhandledExceptionInspection */ @@ -84,10 +84,9 @@ private function requestAccessTokenForClientService(): AccessTokenResponseDto 'client_id' => $this->configuration->getSsoClientId(), 'client_secret' => $this->configuration->getSsoClientSecret(), ]); - /** @var DateTimeInterface $expiresAfter */ - $expiresAfter = $accessToken->getAccessToken()->claims()->get('exp'); + $accessTokenCacheItem->set($accessToken); - $accessTokenCacheItem->expiresAt($expiresAfter); + $accessTokenCacheItem->expiresAt($accessToken->getExpiresAt()); $cachePool->save($accessTokenCacheItem); return $accessToken; @@ -96,12 +95,20 @@ private function requestAccessTokenForClientService(): AccessTokenResponseDto /** * @throws UnsuccessfulAccessTokenRequestException */ - private function sendTokenRequest(string $url, array $bodyParameters): AccessTokenResponseDto + private function sendTokenRequest(string $url, array $bodyParameters): AccessTokenDto { try { $response = $this->client->request(Request::METHOD_POST, $url, ['body' => $bodyParameters]); - return $this->serializer->deserialize($response->getContent(), AccessTokenResponseDto::class); + if ($this->configuration->isAccessTokenConsideredJwt()) { + return AccessTokenDto::createFromJwtAccessTokenResponse( + $this->serializer->deserialize($response->getContent(), AccessTokenResponseDto::class) + ); + } + + return AccessTokenDto::createFromOpaqueAccessTokenResponse( + $this->serializer->deserialize($response->getContent(), OpaqueAccessTokenResponseDto::class) + ); } catch (ExceptionInterface $exception) { throw UnsuccessfulAccessTokenRequestException::create('Token request failed!', $exception); } catch (SerializerException $exception) { diff --git a/src/Model/AccessTokenDto.php b/src/Model/AccessTokenDto.php new file mode 100644 index 0000000..b2f171d --- /dev/null +++ b/src/Model/AccessTokenDto.php @@ -0,0 +1,56 @@ +accessToken = $accessToken; + $this->expiresAt = $expiresAt; + $this->jwt = $accessTokenJwt; + } + + + public function getJwt(): ?Plain + { + return $this->jwt; + } + + public function getAccessToken(): string + { + return $this->accessToken; + } + + public function getExpiresAt(): DateTimeInterface + { + return $this->expiresAt; + } + + public static function createFromJwtAccessTokenResponse(AccessTokenResponseDto $accessTokenResponseDto): self + { + $jwt = $accessTokenResponseDto->getAccessToken(); + /** @var DateTimeInterface $expiresAt */ + $expiresAt = $jwt->claims()->get('exp'); + + return new self($jwt->toString(), $expiresAt, $jwt); + } + + public static function createFromOpaqueAccessTokenResponse(OpaqueAccessTokenResponseDto $accessTokenResponseDto): self + { + $date = (new DateTimeImmutable())->add(new DateInterval('PT' . $accessTokenResponseDto->getExpiresIn() . 'S')); + + return new self($accessTokenResponseDto->getAccessToken(), $date); + } +} diff --git a/src/Model/Enum/JwtAlgorithm.php b/src/Model/Enum/JwtAlgorithm.php index bcde451..b52c173 100644 --- a/src/Model/Enum/JwtAlgorithm.php +++ b/src/Model/Enum/JwtAlgorithm.php @@ -20,7 +20,7 @@ enum JwtAlgorithm: string implements EnumInterface public function signer(): Signer\Ecdsa | Signer\Rsa\Sha256 { return match ($this) { - self::ES256 => Signer\Ecdsa\Sha256::create(), + self::ES256 => new Signer\Ecdsa\Sha256(), self::RS256 => new Signer\Rsa\Sha256(), }; } diff --git a/src/Model/OpaqueAccessTokenResponseDto.php b/src/Model/OpaqueAccessTokenResponseDto.php new file mode 100644 index 0000000..34d88e2 --- /dev/null +++ b/src/Model/OpaqueAccessTokenResponseDto.php @@ -0,0 +1,40 @@ +accessToken; + } + + public function setAccessToken(string $accessToken): self + { + $this->accessToken = $accessToken; + + return $this; + } + + public function getExpiresIn(): int + { + return $this->expiresIn; + } + + public function setExpiresIn(int $expiresIn): self + { + $this->expiresIn = $expiresIn; + + return $this; + } +} diff --git a/tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php b/tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php index 7fc7dbf..f6fc1ea 100644 --- a/tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php +++ b/tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php @@ -102,6 +102,7 @@ public function testFullConfiguration(): void self::assertSame('qux-public-cert', $oAuth2ConfArguments['$ssoPublicCert']); self::assertSame(['email', 'profile'], $oAuth2ConfArguments['$ssoScopes']); self::assertSame(' ', $oAuth2ConfArguments['$ssoScopeDelimiter']); + self::assertFalse($oAuth2ConfArguments['$considerAccessTokenAsJwt']); $this->assertHasDefinition(GrantAccessByOAuth2TokenProcess::class); $grantProcessDefinition = $this->configuration->getDefinition(GrantAccessByOAuth2TokenProcess::class); @@ -156,6 +157,7 @@ private function getFullConfig(): array - profile scope_delimiter: ' ' auth_method: sso_email + consider_access_token_as_jwt: false EOF; $parser = new Parser(); From d4fae3e3b78698206353823b5c980ca494f6fdfd Mon Sep 17 00:00:00 2001 From: Andrej Hudec Date: Tue, 5 Sep 2023 20:30:45 +0200 Subject: [PATCH 7/7] Store access token to cache on first request --- src/HttpClient/OAuth2HttpClient.php | 34 +++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/HttpClient/OAuth2HttpClient.php b/src/HttpClient/OAuth2HttpClient.php index e8bf0ff..6cc3604 100644 --- a/src/HttpClient/OAuth2HttpClient.php +++ b/src/HttpClient/OAuth2HttpClient.php @@ -13,6 +13,7 @@ use AnzuSystems\AuthBundle\Model\SsoUserDto; use AnzuSystems\SerializerBundle\Exception\SerializerException; use AnzuSystems\SerializerBundle\Serializer; +use Psr\Cache\CacheItemInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -33,13 +34,17 @@ public function __construct( */ public function requestAccessTokenByAuthCode(string $code): AccessTokenDto { - return $this->sendTokenRequest($this->configuration->getSsoAccessTokenUrl(), [ + $accessToken = $this->sendTokenRequest($this->configuration->getSsoAccessTokenUrl(), [ 'grant_type' => 'authorization_code', 'code' => $code, 'client_id' => $this->configuration->getSsoClientId(), 'client_secret' => $this->configuration->getSsoClientSecret(), 'redirect_uri' => $this->configuration->getSsoRedirectUrl(), ]); + + $this->storeAccessTokenToCache($this->getAccessTokenCacheItem(), $accessToken); + + return $accessToken; } /** @@ -72,9 +77,7 @@ public function getSsoUserInfo(?string $id = null): SsoUserDto */ private function requestAccessTokenForClientService(): AccessTokenDto { - $cachePool = $this->configuration->getAccessTokenCachePool(); - /** @noinspection PhpUnhandledExceptionInspection */ - $accessTokenCacheItem = $cachePool->getItem(self::CLIENT_SERVICE_ACCESS_TOKEN_CACHE_KEY); + $accessTokenCacheItem = $this->getAccessTokenCacheItem(); if ($accessTokenCacheItem->isHit()) { return $accessTokenCacheItem->get(); } @@ -85,9 +88,7 @@ private function requestAccessTokenForClientService(): AccessTokenDto 'client_secret' => $this->configuration->getSsoClientSecret(), ]); - $accessTokenCacheItem->set($accessToken); - $accessTokenCacheItem->expiresAt($accessToken->getExpiresAt()); - $cachePool->save($accessTokenCacheItem); + $this->storeAccessTokenToCache($accessTokenCacheItem, $accessToken); return $accessToken; } @@ -115,4 +116,23 @@ private function sendTokenRequest(string $url, array $bodyParameters): AccessTok throw UnsuccessfulAccessTokenRequestException::create('Invalid jwt token response!', $exception); } } + + private function getAccessTokenCacheItem(): CacheItemInterface + { + /** @noinspection PhpUnhandledExceptionInspection */ + return $this->configuration->getAccessTokenCachePool()->getItem( + self::CLIENT_SERVICE_ACCESS_TOKEN_CACHE_KEY + ); + } + + private function storeAccessTokenToCache( + CacheItemInterface $accessTokenCacheItem, + AccessTokenDto $accessToken + ): void { + $cachePool = $this->configuration->getAccessTokenCachePool(); + + $accessTokenCacheItem->set($accessToken); + $accessTokenCacheItem->expiresAt($accessToken->getExpiresAt()); + $cachePool->save($accessTokenCacheItem); + } }