Skip to content

Commit f4da1bd

Browse files
authored
refactor(http): improve session handling (#1293)
1 parent 0306cbd commit f4da1bd

23 files changed

+191
-63
lines changed

packages/core/src/AppConfig.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ final class AppConfig
1313
public string $baseUri;
1414

1515
public function __construct(
16+
public ?string $name = null,
17+
1618
?Environment $environment = null,
1719

1820
?string $baseUri = null,
@@ -26,7 +28,6 @@ public function __construct(
2628
public array $insightsProviders = [],
2729
) {
2830
$this->environment = $environment ?? Environment::fromEnv();
29-
3031
$this->baseUri = $baseUri ?? env('BASE_URI') ?? '';
3132
}
3233
}

packages/core/src/Config/app.config.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,9 @@
44

55
use Tempest\Core\AppConfig;
66

7-
return new AppConfig();
7+
use function Tempest\env;
8+
9+
return new AppConfig(
10+
name: env('APPLICATION_NAME'),
11+
baseUri: env('BASE_URI'),
12+
);

packages/http/src/Cookie/CookieManager.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ public function get(string $key): ?Cookie
2929

3030
public function set(string $key, string $value, DateTimeInterface|int|null $expiresAt = null): Cookie
3131
{
32-
$cookie = $this->get($key) ?? new Cookie(key: $key);
32+
$cookie = $this->get($key) ?? new Cookie(
33+
key: $key,
34+
secure: true,
35+
httpOnly: true,
36+
sameSite: SameSite::LAX,
37+
);
3338

3439
$cookie->value = $value;
3540
$cookie->expiresAt = $expiresAt ?? $cookie->expiresAt;

packages/http/src/Cookie/SameSite.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,25 @@
44

55
namespace Tempest\Http\Cookie;
66

7+
/**
8+
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#samesitesamesite-value
9+
*/
710
enum SameSite: string
811
{
12+
/**
13+
* Send the cookie only for requests originating from the same site that set the cookie.
14+
*/
915
case STRICT = 'Strict';
16+
17+
/**
18+
* Send the cookie for requests originating from the same site that set the cookie, and for cross-site requests that meet both of the following criteria:
19+
* - The request is a top-level navigation: this essentially means that the request causes the URL shown in the browser's address bar to change.
20+
* - The request uses a safe method: in particular, this excludes `POST`, `PUT`, and `DELETE`.
21+
*/
1022
case LAX = 'Lax';
23+
24+
/**
25+
* Send the cookie with both cross-site and same-site requests. The `Secure` attribute must also be set when using this value.
26+
*/
1127
case NONE = 'None';
1228
}

packages/http/src/Session/CleanupSessionsCommand.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Tempest\Console\ConsoleCommand;
99
use Tempest\Console\Schedule;
1010
use Tempest\Console\Scheduler\Every;
11+
use Tempest\EventBus\EventBus;
1112

1213
use function Tempest\listen;
1314

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Tempest\Http\Session\Config;
4+
5+
use Tempest\Container\Container;
6+
use Tempest\DateTime\Duration;
7+
use Tempest\Http\Session\Managers\FileSessionManager;
8+
use Tempest\Http\Session\Resolvers\CookieSessionIdResolver;
9+
use Tempest\Http\Session\SessionConfig;
10+
11+
final class FileSessionConfig implements SessionConfig
12+
{
13+
public function __construct(
14+
/**
15+
* Path to the sessions storage directory, relative to the internal storage.
16+
*/
17+
public string $path,
18+
19+
public Duration $expiration,
20+
21+
public string $sessionIdResolver = CookieSessionIdResolver::class,
22+
) {}
23+
24+
public function createManager(Container $container): FileSessionManager
25+
{
26+
return $container->get(FileSessionManager::class);
27+
}
28+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
use Tempest\DateTime\Duration;
4+
use Tempest\Http\Session\Config\FileSessionConfig;
5+
6+
return new FileSessionConfig(
7+
path: 'sessions',
8+
expiration: Duration::hours(2),
9+
);

packages/http/src/Session/Managers/FileSessionManager.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public function isValid(SessionId $id): bool
6262
}
6363

6464
return $this->clock->now()->before(
65-
other: $session->createdAt->plusSeconds($this->sessionConfig->expirationInSeconds),
65+
other: $session->createdAt->plus($this->sessionConfig->expiration),
6666
);
6767
}
6868

packages/http/src/Session/Resolvers/CookieSessionIdResolver.php

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,46 @@
66

77
use Symfony\Component\Uid\Uuid;
88
use Tempest\Clock\Clock;
9+
use Tempest\Core\AppConfig;
10+
use Tempest\Http\Cookie\Cookie;
911
use Tempest\Http\Cookie\CookieManager;
10-
use Tempest\Http\Session\Session;
12+
use Tempest\Http\Cookie\SameSite;
1113
use Tempest\Http\Session\SessionConfig;
1214
use Tempest\Http\Session\SessionId;
1315
use Tempest\Http\Session\SessionIdResolver;
1416

17+
use function Tempest\Support\str;
18+
1519
final readonly class CookieSessionIdResolver implements SessionIdResolver
1620
{
1721
public function __construct(
22+
private AppConfig $appConfig,
1823
private CookieManager $cookies,
1924
private SessionConfig $sessionConfig,
2025
private Clock $clock,
2126
) {}
2227

2328
public function resolve(): SessionId
2429
{
25-
$id = $this->cookies->get(Session::ID)->value ?? null;
30+
$sessionKey = str($this->appConfig->name ?? 'tempest')
31+
->snake()
32+
->append('_session_id')
33+
->toString();
34+
35+
$id = $this->cookies->get($sessionKey)->value ?? null;
2636

2737
if (! $id) {
2838
$id = (string) Uuid::v4();
2939

30-
$this->cookies->set(
31-
key: Session::ID,
40+
$this->cookies->add(new Cookie(
41+
key: $sessionKey,
3242
value: $id,
33-
expiresAt: $this->clock->now()->plusSeconds($this->sessionConfig->expirationInSeconds),
34-
);
43+
path: '/',
44+
secure: true,
45+
httpOnly: true,
46+
expiresAt: $this->clock->now()->plus($this->sessionConfig->expiration),
47+
sameSite: SameSite::LAX,
48+
));
3549
}
3650

3751
return new SessionId($id);

packages/http/src/Session/Resolvers/HeaderSessionIdResolver.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,27 @@
55
namespace Tempest\Http\Session\Resolvers;
66

77
use Symfony\Component\Uid\Uuid;
8+
use Tempest\Core\AppConfig;
89
use Tempest\Http\Request;
9-
use Tempest\Http\Session\Session;
1010
use Tempest\Http\Session\SessionId;
1111
use Tempest\Http\Session\SessionIdResolver;
1212

1313
final readonly class HeaderSessionIdResolver implements SessionIdResolver
1414
{
1515
public function __construct(
16+
private AppConfig $appConfig,
1617
private Request $request,
1718
) {}
1819

1920
public function resolve(): SessionId
2021
{
21-
$id = $this->request->headers[Session::ID] ?? null;
22+
$sessionKey = str($this->appConfig->name ?? 'tempest')
23+
->snake()
24+
->append('_session_id')
25+
->toString();
2226

23-
return new SessionId($id ?? ((string) Uuid::v4()));
27+
return new SessionId(
28+
id: $this->request->headers[$sessionKey] ?? Uuid::v4()->toString(),
29+
);
2430
}
2531
}

0 commit comments

Comments
 (0)