Skip to content

Commit

Permalink
Add set_cookie option to store JWT in secure cookies
Browse files Browse the repository at this point in the history
  • Loading branch information
chalasr committed May 27, 2020
1 parent fc67ecc commit 5b433fe
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 5 deletions.
13 changes: 13 additions & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ public function getConfigTreeBuilder()
->info('If null, the user ID claim will have the same name as the one defined by the option "user_identity_field"')
->end()
->append($this->getTokenExtractorsNode())
->arrayNode('set_cookie')
->canBeEnabled()
->children()
->scalarNode('name')
->defaultNull()
->info('The cookie name. If null, it will be the same as the one defined by the "token_extractors.cookie.name" option.')
->end()
->scalarNode('lifetime')
->defaultNull()
->info('The cookie lifetime. If null, it will be the same as the one defined by the "token_ttl" option.')
->end()
->end()
->end()
->end();

return $treeBuilder;
Expand Down
15 changes: 14 additions & 1 deletion DependencyInjection/LexikJWTAuthenticationExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,22 @@ public function load(array $configs, ContainerBuilder $container)
$container->setParameter('lexik_jwt_authentication.encoder.signature_algorithm', $encoderConfig['signature_algorithm']);
$container->setParameter('lexik_jwt_authentication.encoder.crypto_engine', $encoderConfig['crypto_engine']);

$tokenExtractors = $this->createTokenExtractors($container, $config['token_extractors']);
$container
->getDefinition('lexik_jwt_authentication.extractor.chain_extractor')
->replaceArgument(0, $this->createTokenExtractors($container, $config['token_extractors']));
->replaceArgument(0, $tokenExtractors);

if ($this->isConfigEnabled($container, $config['set_cookie'])) {
$loader->load('cookie.xml');
$container
->getDefinition('lexik_jwt_authentication.cookie_provider')
->replaceArgument(0, $config['set_cookie']['name'] ?: $config['token_extractors']['cookie']['name'])
->replaceArgument(0, $config['set_cookie']['lifetime'] ?: $config['token_ttl']);

$container
->getDefinition('lexik_jwt_authentication.handler.authentication_success')
->replaceArgument(2, new Reference('lexik_jwt_authentication.cookie_provider'));
}
}

private static function createTokenExtractors(ContainerBuilder $container, array $tokenExtractorsConfig)
Expand Down
14 changes: 14 additions & 0 deletions Resources/config/cookie.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="lexik_jwt_authentication.cookie_provider" class="Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Cookie\JWTCookieProvider">
<argument>null</argument>
<argument>null</argument>
</service>
<service id="Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Cookie\JWTCookieProvider" alias="lexik_jwt_authentication.cookie_provider" />
</services>
</container>
6 changes: 6 additions & 0 deletions Resources/config/response_interceptor.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="lexik_jwt_authentication.cookie_provider" class="Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\JWTCookieProvider">
<argument>null</argument> <!-- Default cookie name -->
<argument>null</argument> <!-- Default cookie lifetime -->
</service>

<service id="lexik_jwt_authentication.handler.authentication_success" class="Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\AuthenticationSuccessHandler">
<argument type="service" id="lexik_jwt_authentication.jwt_manager"/>
<argument type="service" id="event_dispatcher"/>
<argument>null</argument> <!-- Cookie provider -->
<tag name="monolog.logger" channel="security" />
</service>
<service id="Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\AuthenticationSuccessHandler" alias="lexik_jwt_authentication.handler.authentication_success" />
Expand Down
10 changes: 9 additions & 1 deletion Response/JWTAuthenticationSuccessResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Lexik\Bundle\JWTAuthenticationBundle\Response;

use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\JsonResponse;

/**
Expand All @@ -15,8 +16,15 @@ final class JWTAuthenticationSuccessResponse extends JsonResponse
* @param string $token Json Web Token
* @param array $data Extra data passed to the response
*/
public function __construct($token, array $data = [])
public function __construct($token, array $data = [], Cookie $jwtCookie = null)
{
if ($jwtCookie) {
parent::__construct($data);
$this->headers->setCookie($jwtCookie);

return;
}

parent::__construct(['token' => $token] + $data);
}
}
29 changes: 26 additions & 3 deletions Security/Http/Authentication/AuthenticationSuccessHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationSuccessResponse;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Cookie\JWTCookieProvider;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as ContractsEventDispatcherInterface;
Expand All @@ -17,16 +18,22 @@
* AuthenticationSuccessHandler.
*
* @author Dev Lexik <dev@lexik.fr>
* @author Robin Chalas <robin.chalas@gmail.com>
*
* @final
*/
class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
private $cookieProvider;

protected $jwtManager;
protected $dispatcher;

public function __construct(JWTTokenManagerInterface $jwtManager, EventDispatcherInterface $dispatcher)
public function __construct(JWTTokenManagerInterface $jwtManager, EventDispatcherInterface $dispatcher, JWTCookieProvider $cookieProvider = null)
{
$this->jwtManager = $jwtManager;
$this->dispatcher = $dispatcher;
$this->cookieProvider = $cookieProvider;
}

/**
Expand All @@ -43,7 +50,13 @@ public function handleAuthenticationSuccess(UserInterface $user, $jwt = null)
$jwt = $this->jwtManager->create($user);
}

$response = new JWTAuthenticationSuccessResponse($jwt);
$cookie = null;

if ($this->cookieProvider) {
$cookie = $this->cookieProvider->createCookie($jwt);
}

$response = new JWTAuthenticationSuccessResponse($jwt, [], $cookie);
$event = new AuthenticationSuccessEvent(['token' => $jwt], $user, $response);

if ($this->dispatcher instanceof ContractsEventDispatcherInterface) {
Expand All @@ -52,7 +65,17 @@ public function handleAuthenticationSuccess(UserInterface $user, $jwt = null)
$this->dispatcher->dispatch(Events::AUTHENTICATION_SUCCESS, $event);
}

$response->setData($event->getData());
$responseData = $event->getData();

if ($cookie) {
unset($responseData['token']);
}

if ($responseData) {
$response->setData($responseData);
} else {
$response->setStatusCode(JWTAuthenticationSuccessResponse::HTTP_NO_CONTENT);
}

return $response;
}
Expand Down
56 changes: 56 additions & 0 deletions Security/Http/Cookie/JWTCookieProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Cookie;

use Symfony\Component\HttpFoundation\Cookie;

/**
* Creates secure JWT cookies.
*/
final class JWTCookieProvider
{
private $defaultCookieName;
private $defaultCookieLifetime;

/**
* @param string|null $defaultCookieName
* @param int|null $defaultCookieLifetime
*/
public function __construct($defaultCookieName = null, $defaultCookieLifetime = null)
{
$this->defaultCookieName = $defaultCookieName;
$this->defaultCookieLifetime = $defaultCookieLifetime;
}

/**
* @param string $jwt
* @param string|null $name The cookie name. If null, the default will be used.
* @param int|string|\DateTimeInterface|null $expiresAt The cookie expiration datetime. If null, the default one will be used
* @param null $domain
* @param null $path
*
* @return Cookie
*/
public function createCookie($jwt, $name = null, $expiresAt = null, $sameSite = Cookie::SAMESITE_LAX, $domain = null, $path = '/')
{
if (!$name && !$this->defaultCookieName) {
throw new \LogicException(sprintf('The cookie name must be provided, either pass it as 2nd argument of %s or set a default name via the constructor.', __METHOD__));
}

if (!$expiresAt && !$this->defaultCookieLifetime) {
throw new \LogicException(sprintf('The cookie expiration time must be provided, either pass it as 3rd argument of %s or set a default lifetime via the constructor.', __METHOD__));
}

return new Cookie(
$name ?: $this->defaultCookieName,
$jwt,
null === $expiresAt ? (time() + $this->defaultCookieLifetime) : $expiresAt,
$path,
$domain,
true,
true,
false,
$sameSite
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\AuthenticationSuccessHandler;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Cookie\JWTCookieProvider;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTManager;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as ContractsEventDispatcherInterface;
use Symfony\Component\HttpFoundation\JsonResponse;

Expand Down Expand Up @@ -62,6 +64,25 @@ public function testHandleAuthenticationSuccessWithGivenJWT()
$this->assertEquals('jwt', $content['token']);
}

public function testOnAuthenticationSuccessSetCookie()
{
$request = $this->getRequest();
$token = $this->getToken();

$cookieProvider = new JWTCookieProvider('access_token', 60);

$response = (new AuthenticationSuccessHandler($this->getJWTManager('secrettoken'), $this->getDispatcher(), $cookieProvider))
->onAuthenticationSuccess($request, $token);

$this->assertInstanceOf(JsonResponse::class, $response);
$this->assertSame(204, $response->getStatusCode());
$this->assertEmpty(json_decode($response->getContent(), true));

$cookie = $response->headers->getCookies()[0];
$this->assertSame('access_token', $cookie->getName());
$this->assertSame('secrettoken', $cookie->getValue());
}

/**
* @return \PHPUnit_Framework_MockObject_MockObject
*/
Expand Down

0 comments on commit 5b433fe

Please sign in to comment.