Skip to content

Commit 49693bf

Browse files
authored
Allow to set a default expiration value on the generated token (#52)
* 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
1 parent 8daca4a commit 49693bf

File tree

8 files changed

+118
-25
lines changed

8 files changed

+118
-25
lines changed

.github/workflows/unit-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
restore-keys: "php-${{ matrix.php-version }}-${{ matrix.operating-system }}"
4444

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

4949
- name: "installing lowest dependencies"

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
0.5.2
5+
-----
6+
7+
* Set a defaut expiration for the JWT and the cookie when using the `Authorization` class
8+
49
0.5.1
510
-----
611

src/Authorization.php

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,15 @@ final class Authorization
2323
private const MERCURE_AUTHORIZATION_COOKIE_NAME = 'mercureAuthorization';
2424

2525
private $registry;
26+
private $cookieLifetime;
2627

27-
public function __construct(HubRegistry $registry)
28+
/**
29+
* @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.
30+
*/
31+
public function __construct(HubRegistry $registry, ?int $cookieLifetime = null)
2832
{
2933
$this->registry = $registry;
34+
$this->cookieLifetime = $cookieLifetime ?? (int) ini_get('session.cookie_lifetime');
3035
}
3136

3237
/**
@@ -42,36 +47,57 @@ public function createCookie(Request $request, array $subscribe = [], array $pub
4247
$hubInstance = $this->registry->getHub($hub);
4348
$tokenFactory = $hubInstance->getFactory();
4449
if (null === $tokenFactory) {
45-
throw new InvalidArgumentException(sprintf('The %s hub does not contain a token factory.', $hub ? '"'.$hub.'"' : 'default'));
50+
throw new InvalidArgumentException(sprintf('The "%s" hub does not contain a token factory.', $hub ? '"'.$hub.'"' : 'default'));
51+
}
52+
53+
$cookieLifetime = $this->cookieLifetime;
54+
if (\array_key_exists('exp', $additionalClaims)) {
55+
if (null !== $additionalClaims['exp']) {
56+
$cookieLifetime = $additionalClaims['exp'];
57+
}
58+
} else {
59+
$additionalClaims['exp'] = new \DateTimeImmutable(0 === $cookieLifetime ? '+1 hour' : "+{$cookieLifetime} seconds");
4660
}
4761

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

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

60-
if (isset($urlComponents['host'])) {
61-
$cookieDomain = strtolower($urlComponents['host']);
62-
$currentDomain = strtolower($request->getHost());
71+
return Cookie::create(
72+
self::MERCURE_AUTHORIZATION_COOKIE_NAME,
73+
$token,
74+
$cookieLifetime,
75+
$urlComponents['path'] ?? '/',
76+
$this->getCookieDomain($request, $urlComponents),
77+
'http' !== strtolower($urlComponents['scheme'] ?? 'https'),
78+
true,
79+
false,
80+
Cookie::SAMESITE_STRICT
81+
);
82+
}
6383

64-
if ($cookieDomain === $currentDomain) {
65-
return $cookie;
66-
}
84+
private function getCookieDomain(Request $request, array $urlComponents): ?string
85+
{
86+
if (!isset($urlComponents['host'])) {
87+
return null;
88+
}
6789

68-
if (!str_ends_with($cookieDomain, ".${currentDomain}")) {
69-
throw new RuntimeException(sprintf('Unable to create authorization cookie for external domain "%s".', $cookieDomain));
70-
}
90+
$cookieDomain = strtolower($urlComponents['host']);
91+
$currentDomain = strtolower($request->getHost());
92+
93+
if ($cookieDomain === $currentDomain) {
94+
return null;
95+
}
7196

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

75-
return $cookie;
101+
return $cookieDomain;
76102
}
77103
}

src/HubRegistry.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ final class HubRegistry
2323
/**
2424
* @param array<string, HubInterface> $hubs An array of hub instances, where the keys are the names
2525
*/
26-
public function __construct(HubInterface $defaultHub, array $hubs)
26+
public function __construct(HubInterface $defaultHub, array $hubs = [])
2727
{
2828
$this->defaultHub = $defaultHub;
2929
$this->hubs = $hubs;

src/Jwt/LcobucciFactory.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,12 @@ final class LcobucciFactory implements TokenFactoryInterface
3737
];
3838

3939
private $configurations;
40+
private $jwtLifetime;
4041

41-
public function __construct(string $secret, string $algorithm = 'hmac.sha256')
42+
/**
43+
* @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.
44+
*/
45+
public function __construct(string $secret, string $algorithm = 'hmac.sha256', ?int $jwtLifetime = 0)
4246
{
4347
if (!class_exists(Key\InMemory::class)) {
4448
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".');
@@ -53,6 +57,7 @@ public function __construct(string $secret, string $algorithm = 'hmac.sha256')
5357
new $signerClass(),
5458
Key\InMemory::plainText($secret)
5559
);
60+
$this->jwtLifetime = 0 === $jwtLifetime ? ((int) ini_get('session.cookie_lifetime') ?: 3600) : $jwtLifetime;
5661
}
5762

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

70+
if (null !== $this->jwtLifetime && !\array_key_exists('exp', $additionalClaims)) {
71+
$additionalClaims['exp'] = new \DateTimeImmutable("+{$this->jwtLifetime} seconds");
72+
}
73+
6574
$additionalClaims['mercure'] = [
6675
'publish' => $publish,
6776
'subscribe' => $subscribe,
@@ -73,7 +82,9 @@ public function create(array $subscribe = [], array $publish = [], array $additi
7382
$builder = $builder->permittedFor(...(array) $value);
7483
break;
7584
case RegisteredClaims::EXPIRATION_TIME:
76-
$builder = $builder->expiresAt($value);
85+
if (null !== $value) {
86+
$builder = $builder->expiresAt($value);
87+
}
7788
break;
7889
case RegisteredClaims::ISSUED_AT:
7990
$builder = $builder->issuedAt($value);

tests/AuthorizationTest.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Mercure Component project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\Mercure\Tests;
15+
16+
use Lcobucci\JWT\Signer\Key\InMemory;
17+
use PHPUnit\Framework\TestCase;
18+
use Symfony\Component\HttpFoundation\Request;
19+
use Symfony\Component\Mercure\Authorization;
20+
use Symfony\Component\Mercure\HubRegistry;
21+
use Symfony\Component\Mercure\Jwt\LcobucciFactory;
22+
use Symfony\Component\Mercure\Jwt\StaticTokenProvider;
23+
use Symfony\Component\Mercure\MockHub;
24+
use Symfony\Component\Mercure\Update;
25+
26+
/**
27+
* @author Kévin Dunglas <[email protected]>
28+
*/
29+
class AuthorizationTest extends TestCase
30+
{
31+
public function testJwtLifetime(): void
32+
{
33+
if (!class_exists(InMemory::class)) {
34+
$this->markTestSkipped('"lcobucci/jwt" is not installed');
35+
}
36+
37+
$registry = new HubRegistry(new MockHub(
38+
'https://example.com/.well-known/mercure',
39+
new StaticTokenProvider('foo.bar.baz'),
40+
function (Update $u): string { return 'dummy'; },
41+
new LcobucciFactory('secret', 'hmac.sha256', 3600)
42+
));
43+
44+
$authorization = new Authorization($registry);
45+
$cookie = $authorization->createCookie(Request::create('https://example.com'));
46+
47+
$payload = json_decode(base64_decode(explode('.', $cookie->getValue())[1], true), true);
48+
$this->assertArrayHasKey('exp', $payload);
49+
$this->assertIsFloat($payload['exp']);
50+
}
51+
}

tests/Jwt/FactoryTokenProviderTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public function testGetToken(): void
2626
$this->markTestSkipped('requires lcobucci/jwt:^4.0.');
2727
}
2828

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

3232
$this->assertSame(

tests/Jwt/LcobucciFactoryTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ protected function setUp(): void
2929

3030
public function testCreate(): void
3131
{
32-
$factory = new LcobucciFactory('!ChangeMe!');
32+
$factory = new LcobucciFactory('!ChangeMe!', 'hmac.sha256', null);
3333

3434
$this->assertSame(
3535
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOltdfX0.TywAqS7IPhvLdP7cXq_U-kXWUVPKFUyYz8NyfRe0vAU',

0 commit comments

Comments
 (0)