Skip to content

Commit

Permalink
Add configuration for mercure cookie samesite policy
Browse files Browse the repository at this point in the history
  • Loading branch information
silverbackdan committed Oct 26, 2022
1 parent 6d3c724 commit cbcf51b
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 32 deletions.
26 changes: 26 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\HttpFoundation\Cookie;

/**
* @author Daniel West <[email protected]>
Expand All @@ -33,6 +34,7 @@ public function getConfigTreeBuilder(): TreeBuilder
->scalarNode('metadata_key')->defaultValue('_metadata')->end()
->end();

$this->addMercureNode($rootNode);
$this->addRouteSecurityNode($rootNode);
$this->addRoutableSecurityNode($rootNode);
$this->addRefreshTokenNode($rootNode);
Expand All @@ -43,6 +45,30 @@ public function getConfigTreeBuilder(): TreeBuilder
return $treeBuilder;
}

private function addMercureNode(ArrayNodeDefinition $rootNode): void
{
$rootNode
->children()
->arrayNode('mercure')
->addDefaultsIfNotSet()
->children()
->scalarNode('hub_name')->defaultNull()->end()
->arrayNode('cookie')
->addDefaultsIfNotSet()
->children()
->scalarNode('samesite')->defaultValue(Cookie::SAMESITE_STRICT)
->validate()
->ifNotInArray([Cookie::SAMESITE_STRICT, Cookie::SAMESITE_LAX, Cookie::SAMESITE_NONE])
->thenInvalid('Invalid Mercure cookie samesite value %s')
->end()
->end()
->end()
->end()
->end()
->end()
->end();
}

private function addRouteSecurityNode(ArrayNodeDefinition $rootNode): void
{
$rootNode
Expand Down
5 changes: 5 additions & 0 deletions src/DependencyInjection/SilverbackApiComponentsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Silverback\ApiComponentsBundle\Doctrine\Extension\ORM\TablePrefixExtension;
use Silverback\ApiComponentsBundle\Event\FormSuccessEvent;
use Silverback\ApiComponentsBundle\EventListener\Form\FormSuccessEventListenerInterface;
use Silverback\ApiComponentsBundle\EventListener\Mercure\AddMercureTokenListener;
use Silverback\ApiComponentsBundle\Exception\ApiPlatformAuthenticationException;
use Silverback\ApiComponentsBundle\Exception\UnparseableRequestHeaderException;
use Silverback\ApiComponentsBundle\Exception\UserDisabledException;
Expand Down Expand Up @@ -151,6 +152,10 @@ public function load(array $configs, ContainerBuilder $container): void

$definition = $container->getDefinition(RoutableVoter::class);
$definition->setArgument('$securityStr', $config['routable_security']);

$definition = $container->getDefinition(AddMercureTokenListener::class);
$definition->setArgument('$cookieSameSite', $config['mercure']['cookie']['samesite']);
$definition->setArgument('$hubName', $config['mercure']['hub_name']);
}

private function setEmailVerificationArguments(ContainerBuilder $container, array $emailVerificationConfig, int $passwordRepeatTtl): void
Expand Down
89 changes: 58 additions & 31 deletions src/EventListener/Mercure/AddMercureTokenListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,23 @@
use Silverback\ApiComponentsBundle\Helper\Publishable\PublishableStatusChecker;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\Mercure\Jwt\TokenFactoryInterface;
use Symfony\Component\Mercure\Authorization;
use Symfony\Component\Routing\RequestContext;


class AddMercureTokenListener
{
use CorsTrait;

public function __construct(private TokenFactoryInterface $tokenFactory, private ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private PublishableStatusChecker $publishableStatusChecker, private RequestContext $requestContext)
public function __construct(
private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory,
private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
private readonly PublishableStatusChecker $publishableStatusChecker,
private readonly RequestContext $requestContext,
private readonly Authorization $mercureAuthorization,
private readonly string $cookieSameSite = Cookie::SAMESITE_STRICT,
private readonly ?string $hubName = null
)
{
}

Expand All @@ -47,47 +56,65 @@ public function onKernelResponse(ResponseEvent $event): void

$subscribeIris = [];
$response = $event->getResponse();

foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);

try {
$operation = $resourceMetadataCollection->getOperation(forceCollection: false, httpOperation: true);
} catch (OperationNotFoundException $e) {
continue;
if ($resourceIris = $this->getSubscribeIrisForResource($resourceClass)) {
$subscribeIris[] = $resourceIris;
}
}
$subscribeIris = array_merge([], ...$subscribeIris);

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

$mercure = $operation->getMercure();
private function getSubscribeIrisForResource(string $resourceClass): ?array
{
$operation = $this->getMercureResourceOperation($resourceClass);
if (!$operation) {
return null;
}

if (!$mercure) {
continue;
}
$refl = new \ReflectionClass($operation->getClass());
$isPublishable = \count($refl->getAttributes(Publishable::class));

$uriTemplate = $this->buildAbsoluteUriTemplate() . $operation->getRoutePrefix() . $operation->getUriTemplate();
$subscribeIris = [$uriTemplate];

$refl = new \ReflectionClass($operation->getClass());
$isPublishable = \count($refl->getAttributes(Publishable::class));
if (!$isPublishable) {
return $subscribeIris;
}

$uriTemplate = $this->buildAbsoluteUriTemplate() . $operation->getRoutePrefix() . $operation->getUriTemplate();
// Note that `?draft=1` is also hard coded into the PublishableIriConverter, probably make this configurable somewhere
if ($this->publishableStatusChecker->isGranted($operation->getClass())) {
$subscribeIris[] = $uriTemplate . '?draft=1';
}

if (!$isPublishable) {
$subscribeIris[] = $uriTemplate;
continue;
}
return $subscribeIris;
}

// Note that `?draft=1` is also hard coded into the PublishableIriConverter, probably make this configurable somewhere
if ($this->publishableStatusChecker->isGranted($operation->getClass())) {
$subscribeIris[] = $uriTemplate . '?draft=1';
$subscribeIris[] = $uriTemplate;
continue;
}
private function getMercureResourceOperation(string $resourceClass): ?HttpOperation
{
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);

try {
$operation = $resourceMetadataCollection->getOperation(forceCollection: false, httpOperation: true);
} catch (OperationNotFoundException $e) {
return null;
}

$subscribeIris[] = $uriTemplate;
if (!$operation instanceof HttpOperation) {
return null;
}

$response->headers->setCookie(Cookie::create('mercureAuthorization', $this->tokenFactory->create($subscribeIris, [])));
$mercure = $operation->getMercure();

if (!$mercure) {
return null;
}
return $operation;
}

/**
Expand Down
4 changes: 3 additions & 1 deletion src/Resources/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Mailer\Event\MessageEvent;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mercure\Authorization;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\RouterInterface;
Expand Down Expand Up @@ -619,11 +620,12 @@
->set(AddMercureTokenListener::class)
->args(
[
new Reference('mercure.hub.default.jwt.factory'),
new Reference(ResourceNameCollectionFactoryInterface::class),
new Reference(ResourceMetadataCollectionFactoryInterface::class),
new Reference(PublishableStatusChecker::class),
new Reference('router.request_context'),
new Reference(Authorization::class),
'', // injected with dependency injection
]
)
->tag('kernel.event_listener', ['event' => ResponseEvent::class, 'method' => 'onKernelResponse']);
Expand Down

0 comments on commit cbcf51b

Please sign in to comment.