Skip to content

Commit 4386578

Browse files
authored
feat(http): add csrf_token function (#1415)
1 parent 361c2fb commit 4386578

File tree

10 files changed

+71
-18
lines changed

10 files changed

+71
-18
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@
151151
"packages/datetime/src/functions.php",
152152
"packages/debug/src/functions.php",
153153
"packages/event-bus/src/functions.php",
154+
"packages/http/src/functions.php",
154155
"packages/icon/src/functions.php",
155156
"packages/intl/src/Number/functions.php",
156157
"packages/intl/src/functions.php",

packages/http/composer.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
"autoload": {
1919
"psr-4": {
2020
"Tempest\\Http\\": "src"
21-
}
21+
},
22+
"files": [
23+
"src/functions.php"
24+
]
2225
},
2326
"autoload-dev": {
2427
"psr-4": {

packages/http/src/Cookie/Cookie.php

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,30 @@
1212
*/
1313
final class Cookie implements Stringable
1414
{
15+
/**
16+
* @param null|int $maxAge The maximum age of the cookie in seconds. This is an alternative to the expiration date. If set, the cookie will expire after the specified number of seconds.
17+
* @param null|string $domain This specifies the domain for which the cookie is valid. If set, the cookie will be sent to this domain and its subdomains. If `⁠null`, it defaults to the current domain.
18+
* @param null|string $path The URL path that must exist in the requested URL for the cookie to be sent. If set, the cookie will only be sent for requests to this path and its subdirectories.
19+
* @param null|bool $secure When `true`, this cookie is only transmitted over secure connections.
20+
* @param null|bool $httpOnly When `true`, this cookie will not be accessible using JavaScript.
21+
* @param null|SameSite $sameSite See {@see \Tempest\Http\Cookie\SameSite}.
22+
*/
1523
public function __construct(
1624
public string $key,
17-
public string $value = '',
25+
public ?string $value = null,
1826
public DateTimeInterface|int|null $expiresAt = null,
1927
public ?int $maxAge = null,
2028
public ?string $domain = null,
21-
public ?string $path = null,
22-
public ?bool $secure = null,
23-
public ?bool $httpOnly = null,
29+
public ?string $path = '/',
30+
public bool $secure = false,
31+
public bool $httpOnly = false,
2432
public ?SameSite $sameSite = null,
2533
) {}
2634

2735
public function __toString(): string
2836
{
2937
$parts = [
30-
$this->key . '=' . rawurlencode($this->value),
38+
$this->key . '=' . rawurlencode($this->value ?? ''),
3139
];
3240

3341
if ($expiresAt = $this->getExpiresAtTime()) {

packages/http/src/Cookie/CookieManager.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public function set(string $key, string $value, DateTimeInterface|int|null $expi
3636
$cookie = $this->get($key) ?? new Cookie(
3737
key: $key,
3838
secure: str($this->appConfig->baseUri)->startsWith('https'),
39+
path: '/',
3940
httpOnly: true,
4041
sameSite: SameSite::LAX,
4142
);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Http\Session;
6+
7+
use Tempest\View\Elements\ViewComponentElement;
8+
use Tempest\View\ViewComponent;
9+
10+
final readonly class CsrfTokenComponent implements ViewComponent
11+
{
12+
public static function getName(): string
13+
{
14+
return 'x-csrf-token';
15+
}
16+
17+
public function compile(ViewComponentElement $element): string
18+
{
19+
$name = Session::CSRF_TOKEN_KEY;
20+
21+
return <<<HTML
22+
<input type="hidden" name="{$name}" value="<?= \Tempest\\Http\\csrf_token() ?>">
23+
HTML;
24+
}
25+
}

packages/http/src/Session/VerifyCsrfMiddleware.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public function __invoke(Request $request, HttpMiddlewareCallable $next): Respon
3636
key: self::CSRF_COOKIE_KEY,
3737
value: $this->session->token,
3838
expiresAt: $this->clock->now()->plus($this->sessionConfig->expiration),
39+
path: '/',
40+
secure: true,
3941
));
4042

4143
if ($this->shouldSkipCheck($request)) {

packages/http/src/Session/x-csrf-token.view.php

Lines changed: 0 additions & 9 deletions
This file was deleted.

packages/http/src/functions.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Tempest\Http;
4+
5+
use Tempest;
6+
use Tempest\Http\Session\Session;
7+
8+
/**
9+
* Gets the session token used for cross-site request forgery protection.
10+
*/
11+
function csrf_token(): string
12+
{
13+
return Tempest\get(Session::class)->token;
14+
}

tests/Integration/Http/CookieManagerTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public function test_creating_a_cookie(): void
3535
$this->http
3636
->get('/')
3737
->assertOk()
38-
->assertHeaderContains('set-cookie', 'new=value; Secure; HttpOnly; SameSite=Lax');
38+
->assertHeaderContains('set-cookie', 'new=value; Path=/; Secure; HttpOnly; SameSite=Lax');
3939
}
4040

4141
public function test_creating_a_cookie_with_unsecure_local_host(): void
@@ -49,7 +49,7 @@ public function test_creating_a_cookie_with_unsecure_local_host(): void
4949
$this->http
5050
->get('/')
5151
->assertOk()
52-
->assertHeaderContains('set-cookie', 'new=value; HttpOnly; SameSite=Lax');
52+
->assertHeaderContains('set-cookie', 'new=value; Path=/; HttpOnly; SameSite=Lax');
5353
}
5454

5555
public function test_removing_a_cookie(): void
@@ -61,7 +61,7 @@ public function test_removing_a_cookie(): void
6161
$this->http
6262
->get('/')
6363
->assertOk()
64-
->assertHeaderContains('set-cookie', 'new=; Expires=Wed, 31-Dec-1969 23:59:59 GMT; Max-Age=0');
64+
->assertHeaderContains('set-cookie', 'new=; Expires=Wed, 31-Dec-1969 23:59:59 GMT; Max-Age=0; Path=/');
6565
}
6666

6767
public function test_manually_adding_a_cookie(): void

tests/Integration/Http/CsrfTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@ public function test_csrf_component(): void
111111
);
112112
}
113113

114+
public function test_csrf_token_function(): void
115+
{
116+
$session = $this->container->get(Session::class);
117+
$session->set(Session::CSRF_TOKEN_KEY, 'test');
118+
119+
$this->assertSame('test', \Tempest\Http\csrf_token());
120+
}
121+
114122
public function test_csrf_with_cached_view(): void
115123
{
116124
$this->get(ViewCache::class)->enabled = true;

0 commit comments

Comments
 (0)