diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 808c625..8b5938f 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -5,31 +5,27 @@ backupGlobals="false" colors="true" bootstrap="tests/bootstrap.php" - convertDeprecationsToExceptions="false" -> - - - ./tests - - - - - - - - - - - - - . - - - ./vendor - ./tests - - - - - + cacheDirectory=".phpunit.cache"> + + + ./tests + + + + + + + + + + + + + . + + + ./vendor + ./tests + + diff --git a/src/Configuration/CookieConfiguration.php b/src/Configuration/CookieConfiguration.php index 4fc906a..fd27b08 100644 --- a/src/Configuration/CookieConfiguration.php +++ b/src/Configuration/CookieConfiguration.php @@ -4,11 +4,17 @@ namespace AnzuSystems\AuthBundle\Configuration; +use Symfony\Component\HttpFoundation\Cookie; + final class CookieConfiguration { public function __construct( private readonly ?string $domain, private readonly bool $secure, + /** + * @var Cookie::SAMESITE_*|''|null + */ + private readonly ?string $sameSite, private readonly string $jwtPayloadCookieName, private readonly string $jwtSignatureCookieName, private readonly string $deviceIdCookieName, @@ -23,6 +29,14 @@ public function getDomain(): ?string return $this->domain; } + /** + * @return Cookie::SAMESITE_*|''|null + */ + public function getSameSite(): ?string + { + return $this->sameSite; + } + public function isSecure(): bool { return $this->secure; diff --git a/src/DependencyInjection/AnzuSystemsAuthExtension.php b/src/DependencyInjection/AnzuSystemsAuthExtension.php index 5bd8bca..cb3dde9 100644 --- a/src/DependencyInjection/AnzuSystemsAuthExtension.php +++ b/src/DependencyInjection/AnzuSystemsAuthExtension.php @@ -38,6 +38,7 @@ public function load(array $configs, ContainerBuilder $container): void $cookieSection = $processedConfig['cookie']; $container->setParameter('anzu_systems.auth_bundle.cookie.domain', $cookieSection['domain']); + $container->setParameter('anzu_systems.auth_bundle.cookie.same_site', $cookieSection['same_site']); $container->setParameter('anzu_systems.auth_bundle.cookie.secure', $cookieSection['secure']); $container->setParameter('anzu_systems.auth_bundle.cookie.device_id_name', $cookieSection['device_id_name']); $container->setParameter('anzu_systems.auth_bundle.cookie.jwt.payload_part_name', $cookieSection['jwt']['payload_part_name']); diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 509d535..f231240 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -13,6 +13,7 @@ use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\HttpFoundation\Cookie; final class Configuration implements ConfigurationInterface { @@ -41,6 +42,10 @@ private function addCookieSection(): NodeDefinition ->addDefaultsIfNotSet() ->children() ->scalarNode('domain')->defaultValue(null)->end() + ->scalarNode('same_site') + ->defaultValue(Cookie::SAMESITE_STRICT) + ->info('SameSite attribute for cookies (lax, strict, none or null)') + ->end() ->booleanNode('secure')->isRequired()->end() ->scalarNode('device_id_name')->defaultValue('anz_di')->end() ->arrayNode('jwt') diff --git a/src/Domain/Process/GrantAccessOnResponseProcess.php b/src/Domain/Process/GrantAccessOnResponseProcess.php index d408d0f..40283b0 100644 --- a/src/Domain/Process/GrantAccessOnResponseProcess.php +++ b/src/Domain/Process/GrantAccessOnResponseProcess.php @@ -32,7 +32,7 @@ public function __construct( /** * @throws Exception */ - public function execute(string $userId, Request $request, Response $response = null): Response + public function execute(string $userId, Request $request, ?Response $response = null): Response { $jwtExpiresAt = new DateTimeImmutable(sprintf('+%d seconds', $this->jwtConfiguration->getLifetime())); $jwt = $this->jwtUtil->create($userId, $jwtExpiresAt); diff --git a/src/Domain/Process/OAuth2/GrantAccessByOAuth2TokenProcess.php b/src/Domain/Process/OAuth2/GrantAccessByOAuth2TokenProcess.php index 9349285..43ce5d1 100644 --- a/src/Domain/Process/OAuth2/GrantAccessByOAuth2TokenProcess.php +++ b/src/Domain/Process/OAuth2/GrantAccessByOAuth2TokenProcess.php @@ -7,6 +7,7 @@ use AnzuSystems\AuthBundle\Contracts\AnzuAuthUserInterface; use AnzuSystems\AuthBundle\Contracts\OAuth2AuthUserRepositoryInterface; use AnzuSystems\AuthBundle\Domain\Process\GrantAccessOnResponseProcess; +use AnzuSystems\AuthBundle\Event\AuthTargetUrlEvent; use AnzuSystems\AuthBundle\Exception\InvalidJwtException; use AnzuSystems\AuthBundle\Exception\UnsuccessfulAccessTokenRequestException; use AnzuSystems\AuthBundle\Exception\UnsuccessfulUserInfoRequestException; @@ -21,6 +22,7 @@ use Exception; use Lcobucci\JWT\Token\RegisteredClaims; use Psr\Log\LoggerInterface; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -44,6 +46,7 @@ public function __construct( private readonly LoggerInterface $appLogger, private readonly LogContextFactory $contextFactory, private readonly string $authMethod, + private readonly EventDispatcherInterface $eventDispatcher, ) { } @@ -91,7 +94,7 @@ public function execute(Request $request): Response } try { - $response = $this->createRedirectResponseForRequest($request, UserOAuthLoginState::Success); + $response = $this->createRedirectResponseForRequest($request, UserOAuthLoginState::Success, $authUser); return $this->grantAccessOnResponseProcess->execute($authUser->getAuthId(), $request, $response); } catch (Exception $exception) { @@ -101,7 +104,7 @@ public function execute(Request $request): Response } } - public function createRedirectResponseForRequest(Request $request, UserOAuthLoginState $loginState): RedirectResponse + public function createRedirectResponseForRequest(Request $request, UserOAuthLoginState $loginState, ?AnzuAuthUserInterface $authUser = null): RedirectResponse { $redirectUrl = $this->httpUtil->getAuthRedirectUrlFromRequest($request); $redirectUrl .= '?'; @@ -110,7 +113,10 @@ public function createRedirectResponseForRequest(Request $request, UserOAuthLogi self::TIMESTAMP_QUERY_PARAM => time(), ]); - return new RedirectResponse($redirectUrl); + $event = new AuthTargetUrlEvent($redirectUrl, $request, $loginState, $authUser); + $this->eventDispatcher->dispatch($event, AuthTargetUrlEvent::NAME); + + return new RedirectResponse($event->getTargetUrl()); } /** diff --git a/src/Domain/Process/RefreshTokenProcess.php b/src/Domain/Process/RefreshTokenProcess.php index e19e69b..fb00c89 100644 --- a/src/Domain/Process/RefreshTokenProcess.php +++ b/src/Domain/Process/RefreshTokenProcess.php @@ -24,7 +24,7 @@ public function __construct( /** * @throws Exception */ - public function execute(Request $request, Response $response = null): Response + public function execute(Request $request, ?Response $response = null): Response { $response ??= new JsonResponse(); try { diff --git a/src/Event/AuthTargetUrlEvent.php b/src/Event/AuthTargetUrlEvent.php new file mode 100644 index 0000000..ea08693 --- /dev/null +++ b/src/Event/AuthTargetUrlEvent.php @@ -0,0 +1,50 @@ +targetUrl; + } + + public function setTargetUrl(string $targetUrl): self + { + $this->targetUrl = $targetUrl; + + return $this; + } + + public function getRequest(): Request + { + return $this->request; + } + + public function getLoginState(): UserOAuthLoginState + { + return $this->loginState; + } + + public function getAuthUser(): ?AnzuAuthUserInterface + { + return $this->authUser; + } +} diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php index 9a68137..46d935d 100644 --- a/src/Resources/config/services.php +++ b/src/Resources/config/services.php @@ -18,6 +18,7 @@ $services ->set(CookieConfiguration::class) ->arg('$domain', param('anzu_systems.auth_bundle.cookie.domain')) + ->arg('$sameSite', param('anzu_systems.auth_bundle.cookie.same_site')) ->arg('$secure', param('anzu_systems.auth_bundle.cookie.secure')) ->arg('$deviceIdCookieName', param('anzu_systems.auth_bundle.cookie.device_id_name')) ->arg('$jwtPayloadCookieName', param('anzu_systems.auth_bundle.cookie.jwt.payload_part_name')) diff --git a/src/Util/HttpUtil.php b/src/Util/HttpUtil.php index 112ae9a..61ee79b 100644 --- a/src/Util/HttpUtil.php +++ b/src/Util/HttpUtil.php @@ -91,7 +91,7 @@ public function grabDeviceIdFromRequest(Request $request): string /** * @throws InvalidJwtException */ - public function storeJwtOnResponse(Response $response, Token $token, DateTimeImmutable $expiresAt = null): void + public function storeJwtOnResponse(Response $response, Token $token, ?DateTimeImmutable $expiresAt = null): void { $rawToken = $token->toString(); /** @psalm-suppress PossiblyUndefinedArrayOffset */ @@ -190,7 +190,7 @@ private function createCookie( $this->cookieConfiguration->isSecure(), $httpOnly, false, - Cookie::SAMESITE_STRICT + $this->cookieConfiguration->getSameSite(), ); } diff --git a/src/Util/JwtUtil.php b/src/Util/JwtUtil.php index efc5cff..d494c6c 100644 --- a/src/Util/JwtUtil.php +++ b/src/Util/JwtUtil.php @@ -35,7 +35,7 @@ public function __construct( * * @throws MissingConfigurationException */ - public function create(string $authId, DateTimeImmutable $expiresAt = null, array $claims = []): Plain + public function create(string $authId, ?DateTimeImmutable $expiresAt = null, array $claims = []): Plain { $privateCert = $this->jwtConfiguration->getPrivateCert(); diff --git a/tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php b/tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php index 9bab4c7..8bf7fd4 100644 --- a/tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php +++ b/tests/DependencyInjection/AnzuSystemsAuthExtensionTest.php @@ -66,6 +66,7 @@ public function testFullConfiguration(): void $loader->load([$config], $this->configuration); $this->assertParameter('.example.com', 'anzu_systems.auth_bundle.cookie.domain'); + $this->assertParameter('lax', 'anzu_systems.auth_bundle.cookie.same_site'); $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'); @@ -129,6 +130,7 @@ private function getFullConfig(): array $yaml = <<assertSame($token->toString(), $payloadCookie->getValue() . '.' . $signCookie->getValue()); $this->assertTrue($payloadCookie->isSecure()); $this->assertFalse($payloadCookie->isHttpOnly()); + $this->assertSame('strict', $payloadCookie->getSameSite()); $this->assertSame('.example.com', $payloadCookie->getDomain()); $this->assertTrue($signCookie->isSecure()); $this->assertTrue($signCookie->isHttpOnly()); + $this->assertSame('strict', $payloadCookie->getSameSite()); $this->assertSame('.example.com', $signCookie->getDomain()); } }