<?php
namespace App\EventSubscriber;
use Aws\CloudFront\CookieSigner;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
class SignedCookieSubscriber implements EventSubscriberInterface
{
private const SET_COOKIES = 'signed_cookie_auth_success';
/**
* @var \Symfony\Component\HttpFoundation\Session\SessionInterface
*/
private $session;
/**
* @var \Psr\Log\LoggerInterface
*/
private $logger;
/**
* @var string
*/
private $privateKey;
/**
* @var string
*/
private $pairId;
/**
* @var int
*/
private $lifetime;
/**
* @var \Symfony\Component\HttpFoundation\Request
*/
private $masterRequest;
public function __construct(
SessionInterface $session,
LoggerInterface $logger,
RequestStack $requestStack,
string $cloudFrontPrivateKey,
string $cloudFrontKeyPairId,
int $sessionLifetime
) {
$this->session = $session;
$this->logger = $logger;
$this->privateKey = $cloudFrontPrivateKey;
$this->pairId = $cloudFrontKeyPairId;
$this->lifetime = $sessionLifetime;
$this->masterRequest = $requestStack->getMasterRequest();
}
public static function getSubscribedEvents(): array
{
return [
SecurityEvents::INTERACTIVE_LOGIN => [['setAuthSuccessFlag', 10]],
KernelEvents::RESPONSE => [['setResponseCookies', 10]],
];
}
/**
* Set a flag in the session to indicate that signed cookies should be set.
*
* @param \Symfony\Component\Security\Http\Event\InteractiveLoginEvent $event
* Kernel event object
*/
public function setAuthSuccessFlag(InteractiveLoginEvent $event): void
{
// Disable signed cookie functionality if the config is empty.
if (empty($this->privateKey) && empty($this->pairId)) {
return;
}
$this->session->set(self::SET_COOKIES, true);
}
/**
* Adds the necessary cookies for accessing assets through CloudFront.
*
* Note: Cookies are removed on logout by \App\Security\LogoutSuccessHandler
*
* @param \Symfony\Component\HttpKernel\Event\ResponseEvent $event
* Kernel event object
*/
public function setResponseCookies(ResponseEvent $event): void
{
// Set CloudFront signed cookies if a login event is detected.
if (!$this->session->get(self::SET_COOKIES)) {
return;
}
try {
foreach ($this->getSignedCookies() as $signedCookie) {
$event->getResponse()->headers->setCookie($signedCookie);
}
} catch (\Exception $e) {
$this->logger->error('Could not set signed cookies.', ['exception' => $e]);
}
$this->session->remove(self::SET_COOKIES);
}
/**
* Creates signed cookies with a custom policy for all resources.
*
* @return Cookie[]
* Array of Cookie objects ready to be added to a Response
*/
private function getSignedCookies(): array
{
// Create a cookie that allows access to all assets in the S3 bucket through a CloudFront distribution.
// More about custom policies can be found here
// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html
$signer = new CookieSigner($this->pairId, $this->privateKey);
$expire = time() + $this->lifetime;
$policy = json_encode([
'Statement' => [
[
'Condition' => [
'DateLessThan' => ['AWS:EpochTime' => $expire],
],
],
],
], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
$domain = $this->masterRequest->getHost();
$cookies = [];
foreach ($signer->getSignedCookie(null, null, $policy) as $name => $value) {
$cookies[] = Cookie::create($name, $value, $expire, '/', $domain);
}
return $cookies;
}
}