Skip to content

Commit 384c4a3

Browse files
committed
Add configuration for mercure cookie samesite policy
1 parent 6d3c724 commit 384c4a3

File tree

4 files changed

+92
-33
lines changed

4 files changed

+92
-33
lines changed

src/DependencyInjection/Configuration.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
1717
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
1818
use Symfony\Component\Config\Definition\ConfigurationInterface;
19+
use Symfony\Component\HttpFoundation\Cookie;
1920

2021
/**
2122
* @author Daniel West <daniel@silverback.is>
@@ -33,6 +34,7 @@ public function getConfigTreeBuilder(): TreeBuilder
3334
->scalarNode('metadata_key')->defaultValue('_metadata')->end()
3435
->end();
3536

37+
$this->addMercureNode($rootNode);
3638
$this->addRouteSecurityNode($rootNode);
3739
$this->addRoutableSecurityNode($rootNode);
3840
$this->addRefreshTokenNode($rootNode);
@@ -43,6 +45,30 @@ public function getConfigTreeBuilder(): TreeBuilder
4345
return $treeBuilder;
4446
}
4547

48+
private function addMercureNode(ArrayNodeDefinition $rootNode): void
49+
{
50+
$rootNode
51+
->children()
52+
->arrayNode('mercure')
53+
->addDefaultsIfNotSet()
54+
->children()
55+
->scalarNode('hub_name')->defaultNull()->end()
56+
->arrayNode('cookie')
57+
->addDefaultsIfNotSet()
58+
->children()
59+
->scalarNode('samesite')->defaultValue(Cookie::SAMESITE_STRICT)
60+
->validate()
61+
->ifNotInArray([Cookie::SAMESITE_STRICT, Cookie::SAMESITE_LAX, Cookie::SAMESITE_NONE])
62+
->thenInvalid('Invalid Mercure cookie samesite value %s')
63+
->end()
64+
->end()
65+
->end()
66+
->end()
67+
->end()
68+
->end()
69+
->end();
70+
}
71+
4672
private function addRouteSecurityNode(ArrayNodeDefinition $rootNode): void
4773
{
4874
$rootNode

src/DependencyInjection/SilverbackApiComponentsExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Silverback\ApiComponentsBundle\Doctrine\Extension\ORM\TablePrefixExtension;
2222
use Silverback\ApiComponentsBundle\Event\FormSuccessEvent;
2323
use Silverback\ApiComponentsBundle\EventListener\Form\FormSuccessEventListenerInterface;
24+
use Silverback\ApiComponentsBundle\EventListener\Mercure\AddMercureTokenListener;
2425
use Silverback\ApiComponentsBundle\Exception\ApiPlatformAuthenticationException;
2526
use Silverback\ApiComponentsBundle\Exception\UnparseableRequestHeaderException;
2627
use Silverback\ApiComponentsBundle\Exception\UserDisabledException;
@@ -151,6 +152,10 @@ public function load(array $configs, ContainerBuilder $container): void
151152

152153
$definition = $container->getDefinition(RoutableVoter::class);
153154
$definition->setArgument('$securityStr', $config['routable_security']);
155+
156+
$definition = $container->getDefinition(AddMercureTokenListener::class);
157+
$definition->setArgument('$cookieSameSite', $config['mercure']['cookie']['samesite']);
158+
$definition->setArgument('$hubName', $config['mercure']['hub_name']);
154159
}
155160

156161
private function setEmailVerificationArguments(ContainerBuilder $container, array $emailVerificationConfig, int $passwordRepeatTtl): void

src/EventListener/Mercure/AddMercureTokenListener.php

Lines changed: 58 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,22 @@
2222
use Silverback\ApiComponentsBundle\Helper\Publishable\PublishableStatusChecker;
2323
use Symfony\Component\HttpFoundation\Cookie;
2424
use Symfony\Component\HttpKernel\Event\ResponseEvent;
25-
use Symfony\Component\Mercure\Jwt\TokenFactoryInterface;
25+
use Symfony\Component\Mercure\Authorization;
2626
use Symfony\Component\Routing\RequestContext;
2727

2828
class AddMercureTokenListener
2929
{
3030
use CorsTrait;
3131

32-
public function __construct(private TokenFactoryInterface $tokenFactory, private ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private PublishableStatusChecker $publishableStatusChecker, private RequestContext $requestContext)
33-
{
32+
public function __construct(
33+
private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory,
34+
private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
35+
private readonly PublishableStatusChecker $publishableStatusChecker,
36+
private readonly RequestContext $requestContext,
37+
private readonly Authorization $mercureAuthorization,
38+
private readonly string $cookieSameSite = Cookie::SAMESITE_STRICT,
39+
private readonly ?string $hubName = null
40+
) {
3441
}
3542

3643
/**
@@ -47,47 +54,66 @@ public function onKernelResponse(ResponseEvent $event): void
4754

4855
$subscribeIris = [];
4956
$response = $event->getResponse();
50-
5157
foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
52-
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
53-
54-
try {
55-
$operation = $resourceMetadataCollection->getOperation(forceCollection: false, httpOperation: true);
56-
} catch (OperationNotFoundException $e) {
57-
continue;
58+
if ($resourceIris = $this->getSubscribeIrisForResource($resourceClass)) {
59+
$subscribeIris[] = $resourceIris;
5860
}
61+
}
62+
$subscribeIris = array_merge([], ...$subscribeIris);
5963

60-
if (!$operation instanceof HttpOperation) {
61-
continue;
62-
}
64+
// Todo: await merge of https://github.com/symfony/mercure/pull/93 to remove ability to publish any updates and set to null
65+
// May also be able to await a mercure bundle update to set the cookie samesite in mercure configs
66+
$cookie = $this->mercureAuthorization->createCookie($request, $request, $subscribeIris, [], $this->hubName);
67+
$cookie->withSameSite($this->cookieSameSite);
68+
$response->headers->setCookie($cookie);
69+
}
6370

64-
$mercure = $operation->getMercure();
71+
private function getSubscribeIrisForResource(string $resourceClass): ?array
72+
{
73+
$operation = $this->getMercureResourceOperation($resourceClass);
74+
if (!$operation) {
75+
return null;
76+
}
6577

66-
if (!$mercure) {
67-
continue;
68-
}
78+
$refl = new \ReflectionClass($operation->getClass());
79+
$isPublishable = \count($refl->getAttributes(Publishable::class));
6980

70-
$refl = new \ReflectionClass($operation->getClass());
71-
$isPublishable = \count($refl->getAttributes(Publishable::class));
81+
$uriTemplate = $this->buildAbsoluteUriTemplate() . $operation->getRoutePrefix() . $operation->getUriTemplate();
82+
$subscribeIris = [$uriTemplate];
7283

73-
$uriTemplate = $this->buildAbsoluteUriTemplate() . $operation->getRoutePrefix() . $operation->getUriTemplate();
84+
if (!$isPublishable) {
85+
return $subscribeIris;
86+
}
7487

75-
if (!$isPublishable) {
76-
$subscribeIris[] = $uriTemplate;
77-
continue;
78-
}
88+
// Note that `?draft=1` is also hard coded into the PublishableIriConverter, probably make this configurable somewhere
89+
if ($this->publishableStatusChecker->isGranted($operation->getClass())) {
90+
$subscribeIris[] = $uriTemplate . '?draft=1';
91+
}
7992

80-
// Note that `?draft=1` is also hard coded into the PublishableIriConverter, probably make this configurable somewhere
81-
if ($this->publishableStatusChecker->isGranted($operation->getClass())) {
82-
$subscribeIris[] = $uriTemplate . '?draft=1';
83-
$subscribeIris[] = $uriTemplate;
84-
continue;
85-
}
93+
return $subscribeIris;
94+
}
95+
96+
private function getMercureResourceOperation(string $resourceClass): ?HttpOperation
97+
{
98+
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
99+
100+
try {
101+
$operation = $resourceMetadataCollection->getOperation(forceCollection: false, httpOperation: true);
102+
} catch (OperationNotFoundException $e) {
103+
return null;
104+
}
105+
106+
if (!$operation instanceof HttpOperation) {
107+
return null;
108+
}
109+
110+
$mercure = $operation->getMercure();
86111

87-
$subscribeIris[] = $uriTemplate;
112+
if (!$mercure) {
113+
return null;
88114
}
89115

90-
$response->headers->setCookie(Cookie::create('mercureAuthorization', $this->tokenFactory->create($subscribeIris, [])));
116+
return $operation;
91117
}
92118

93119
/**

src/Resources/config/services.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@
168168
use Symfony\Component\HttpKernel\KernelEvents;
169169
use Symfony\Component\Mailer\Event\MessageEvent;
170170
use Symfony\Component\Mailer\MailerInterface;
171+
use Symfony\Component\Mercure\Authorization;
171172
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
172173
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
173174
use Symfony\Component\Routing\RouterInterface;
@@ -619,11 +620,12 @@
619620
->set(AddMercureTokenListener::class)
620621
->args(
621622
[
622-
new Reference('mercure.hub.default.jwt.factory'),
623623
new Reference(ResourceNameCollectionFactoryInterface::class),
624624
new Reference(ResourceMetadataCollectionFactoryInterface::class),
625625
new Reference(PublishableStatusChecker::class),
626626
new Reference('router.request_context'),
627+
new Reference(Authorization::class),
628+
'', // injected with dependency injection
627629
]
628630
)
629631
->tag('kernel.event_listener', ['event' => ResponseEvent::class, 'method' => 'onKernelResponse']);

0 commit comments

Comments
 (0)