Skip to content

Commit

Permalink
Allow to set a default expiration value on the generated token (#52)
Browse files Browse the repository at this point in the history
* Allow to set a default expiration value on the generated token

* move the logic to the token factory

* cs

* Set cookie expiration

* more advanced logic

* cs

* fix tests

* fix tests

* fix gha

* try to fix gha

* changelog

* typo

* nico's review

* fix tests

* fix fallback

* simplify: we can modify the cookie expiration time after

* fix
  • Loading branch information
dunglas authored Apr 3, 2021
1 parent 8daca4a commit 49693bf
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 25 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
restore-keys: "php-${{ matrix.php-version }}-${{ matrix.operating-system }}"

- name: "removing 'lcobucci/jwt' dependency"
if: "${{ matrix.php != '7.4' }} && ${{ matrix.php != '8.0' }}"
if: ${{ matrix.php-version != '7.4' && matrix.php-version != '8.0' }}
run: "composer remove --no-update --dev lcobucci/jwt"

- name: "installing lowest dependencies"
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

0.5.2
-----

* Set a defaut expiration for the JWT and the cookie when using the `Authorization` class

0.5.1
-----

Expand Down
64 changes: 45 additions & 19 deletions src/Authorization.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,15 @@ final class Authorization
private const MERCURE_AUTHORIZATION_COOKIE_NAME = 'mercureAuthorization';

private $registry;
private $cookieLifetime;

public function __construct(HubRegistry $registry)
/**
* @param int|null $cookieLifetime in seconds, 0 for the current session, null to default to the value of "session.cookie_lifetime" or 3600 if "session.cookie_lifetime" is set to 0. The "exp" field of the JWT will be set accordingly if not set explicitly, defaults to 1h in case of session cookies.
*/
public function __construct(HubRegistry $registry, ?int $cookieLifetime = null)
{
$this->registry = $registry;
$this->cookieLifetime = $cookieLifetime ?? (int) ini_get('session.cookie_lifetime');
}

/**
Expand All @@ -42,36 +47,57 @@ public function createCookie(Request $request, array $subscribe = [], array $pub
$hubInstance = $this->registry->getHub($hub);
$tokenFactory = $hubInstance->getFactory();
if (null === $tokenFactory) {
throw new InvalidArgumentException(sprintf('The %s hub does not contain a token factory.', $hub ? '"'.$hub.'"' : 'default'));
throw new InvalidArgumentException(sprintf('The "%s" hub does not contain a token factory.', $hub ? '"'.$hub.'"' : 'default'));
}

$cookieLifetime = $this->cookieLifetime;
if (\array_key_exists('exp', $additionalClaims)) {
if (null !== $additionalClaims['exp']) {
$cookieLifetime = $additionalClaims['exp'];
}
} else {
$additionalClaims['exp'] = new \DateTimeImmutable(0 === $cookieLifetime ? '+1 hour' : "+{$cookieLifetime} seconds");
}

$token = $tokenFactory->create($subscribe, $publish, $additionalClaims);
$url = $hubInstance->getPublicUrl();
/** @var array $urlComponents */
$urlComponents = parse_url($url);

$cookie = Cookie::create(self::MERCURE_AUTHORIZATION_COOKIE_NAME)
->withValue($token)
->withPath(($urlComponents['path'] ?? '/'))
->withSecure('http' !== strtolower($urlComponents['scheme'] ?? 'https'))
->withHttpOnly(true)
->withSameSite(Cookie::SAMESITE_STRICT);
if (!$cookieLifetime instanceof \DateTimeInterface && 0 !== $cookieLifetime) {
$cookieLifetime = new \DateTimeImmutable("+{$cookieLifetime} seconds");
}

if (isset($urlComponents['host'])) {
$cookieDomain = strtolower($urlComponents['host']);
$currentDomain = strtolower($request->getHost());
return Cookie::create(
self::MERCURE_AUTHORIZATION_COOKIE_NAME,
$token,
$cookieLifetime,
$urlComponents['path'] ?? '/',
$this->getCookieDomain($request, $urlComponents),
'http' !== strtolower($urlComponents['scheme'] ?? 'https'),
true,
false,
Cookie::SAMESITE_STRICT
);
}

if ($cookieDomain === $currentDomain) {
return $cookie;
}
private function getCookieDomain(Request $request, array $urlComponents): ?string
{
if (!isset($urlComponents['host'])) {
return null;
}

if (!str_ends_with($cookieDomain, ".${currentDomain}")) {
throw new RuntimeException(sprintf('Unable to create authorization cookie for external domain "%s".', $cookieDomain));
}
$cookieDomain = strtolower($urlComponents['host']);
$currentDomain = strtolower($request->getHost());

if ($cookieDomain === $currentDomain) {
return null;
}

$cookie = $cookie->withDomain($cookieDomain);
if (!str_ends_with($cookieDomain, ".${currentDomain}")) {
throw new RuntimeException(sprintf('Unable to create authorization cookie for a hub on the different second-level domain "%s".', $cookieDomain));
}

return $cookie;
return $cookieDomain;
}
}
2 changes: 1 addition & 1 deletion src/HubRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ final class HubRegistry
/**
* @param array<string, HubInterface> $hubs An array of hub instances, where the keys are the names
*/
public function __construct(HubInterface $defaultHub, array $hubs)
public function __construct(HubInterface $defaultHub, array $hubs = [])
{
$this->defaultHub = $defaultHub;
$this->hubs = $hubs;
Expand Down
15 changes: 13 additions & 2 deletions src/Jwt/LcobucciFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,12 @@ final class LcobucciFactory implements TokenFactoryInterface
];

private $configurations;
private $jwtLifetime;

public function __construct(string $secret, string $algorithm = 'hmac.sha256')
/**
* @param int|null $jwtLifetime If not null, an "exp" claim is always set to now + $jwtLifetime (in seconds), defaults to "session.cookie_lifetime" or 3600 if "session.cookie_lifetime" is set to 0.
*/
public function __construct(string $secret, string $algorithm = 'hmac.sha256', ?int $jwtLifetime = 0)
{
if (!class_exists(Key\InMemory::class)) {
throw new \LogicException('You cannot use "Symfony\Component\Mercure\Token\LcobucciFactory" as the "lcobucci/jwt" package is not installed. Try running "composer require lcobucci/jwt".');
Expand All @@ -53,6 +57,7 @@ public function __construct(string $secret, string $algorithm = 'hmac.sha256')
new $signerClass(),
Key\InMemory::plainText($secret)
);
$this->jwtLifetime = 0 === $jwtLifetime ? ((int) ini_get('session.cookie_lifetime') ?: 3600) : $jwtLifetime;
}

/**
Expand All @@ -62,6 +67,10 @@ public function create(array $subscribe = [], array $publish = [], array $additi
{
$builder = $this->configurations->builder();

if (null !== $this->jwtLifetime && !\array_key_exists('exp', $additionalClaims)) {
$additionalClaims['exp'] = new \DateTimeImmutable("+{$this->jwtLifetime} seconds");
}

$additionalClaims['mercure'] = [
'publish' => $publish,
'subscribe' => $subscribe,
Expand All @@ -73,7 +82,9 @@ public function create(array $subscribe = [], array $publish = [], array $additi
$builder = $builder->permittedFor(...(array) $value);
break;
case RegisteredClaims::EXPIRATION_TIME:
$builder = $builder->expiresAt($value);
if (null !== $value) {
$builder = $builder->expiresAt($value);
}
break;
case RegisteredClaims::ISSUED_AT:
$builder = $builder->issuedAt($value);
Expand Down
51 changes: 51 additions & 0 deletions tests/AuthorizationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

/*
* This file is part of the Mercure Component project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Symfony\Component\Mercure\Tests;

use Lcobucci\JWT\Signer\Key\InMemory;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mercure\Authorization;
use Symfony\Component\Mercure\HubRegistry;
use Symfony\Component\Mercure\Jwt\LcobucciFactory;
use Symfony\Component\Mercure\Jwt\StaticTokenProvider;
use Symfony\Component\Mercure\MockHub;
use Symfony\Component\Mercure\Update;

/**
* @author Kévin Dunglas <[email protected]>
*/
class AuthorizationTest extends TestCase
{
public function testJwtLifetime(): void
{
if (!class_exists(InMemory::class)) {
$this->markTestSkipped('"lcobucci/jwt" is not installed');
}

$registry = new HubRegistry(new MockHub(
'https://example.com/.well-known/mercure',
new StaticTokenProvider('foo.bar.baz'),
function (Update $u): string { return 'dummy'; },
new LcobucciFactory('secret', 'hmac.sha256', 3600)
));

$authorization = new Authorization($registry);
$cookie = $authorization->createCookie(Request::create('https://example.com'));

$payload = json_decode(base64_decode(explode('.', $cookie->getValue())[1], true), true);
$this->assertArrayHasKey('exp', $payload);
$this->assertIsFloat($payload['exp']);
}
}
2 changes: 1 addition & 1 deletion tests/Jwt/FactoryTokenProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function testGetToken(): void
$this->markTestSkipped('requires lcobucci/jwt:^4.0.');
}

$factory = new LcobucciFactory('!ChangeMe!');
$factory = new LcobucciFactory('!ChangeMe!', 'hmac.sha256', null);
$provider = new FactoryTokenProvider($factory, [], ['*']);

$this->assertSame(
Expand Down
2 changes: 1 addition & 1 deletion tests/Jwt/LcobucciFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ protected function setUp(): void

public function testCreate(): void
{
$factory = new LcobucciFactory('!ChangeMe!');
$factory = new LcobucciFactory('!ChangeMe!', 'hmac.sha256', null);

$this->assertSame(
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOltdfX0.TywAqS7IPhvLdP7cXq_U-kXWUVPKFUyYz8NyfRe0vAU',
Expand Down

0 comments on commit 49693bf

Please sign in to comment.