Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 60b8ba7

Browse files
committed
[HttpFoundation] Add UriSigner::signAndWrap() and UriSigner::verify()
1 parent 904df78 commit 60b8ba7

File tree

7 files changed

+228
-23
lines changed

7 files changed

+228
-23
lines changed

src/Symfony/Component/HttpFoundation/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ CHANGELOG
99
* Add support for `valkey:` / `valkeys:` schemes for sessions
1010
* `Request::getPreferredLanguage()` now favors a more preferred language above exactly matching a locale
1111
* Allow `UriSigner` to use a `ClockInterface`
12+
* Add `UriSigner::verify()`
1213

1314
7.2
1415
---
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[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+
namespace Symfony\Component\HttpFoundation\Exception;
13+
14+
/**
15+
* @author Kevin Bond <[email protected]>
16+
*/
17+
final class ExpiredSignedUriException extends SignedUriException
18+
{
19+
public function __construct(
20+
public readonly \DateTimeImmutable $expiredAt,
21+
string $uri,
22+
) {
23+
parent::__construct($uri, 'The URI has expired.');
24+
}
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[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+
namespace Symfony\Component\HttpFoundation\Exception;
13+
14+
/**
15+
* @author Kevin Bond <[email protected]>
16+
*/
17+
abstract class SignedUriException extends \RuntimeException implements ExceptionInterface
18+
{
19+
public function __construct(
20+
public readonly string $uri,
21+
string $message,
22+
) {
23+
parent::__construct($message);
24+
}
25+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[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+
namespace Symfony\Component\HttpFoundation\Exception;
13+
14+
/**
15+
* @author Kevin Bond <[email protected]>
16+
*/
17+
final class UnsignedUriException extends SignedUriException
18+
{
19+
public function __construct(string $uri)
20+
{
21+
parent::__construct($uri, 'The URI is not signed.');
22+
}
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[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+
namespace Symfony\Component\HttpFoundation\Exception;
13+
14+
/**
15+
* @author Kevin Bond <[email protected]>
16+
*/
17+
final class UnverifiedSignedUriException extends SignedUriException
18+
{
19+
public function __construct(string $uri)
20+
{
21+
parent::__construct($uri, 'The URI signature is invalid.');
22+
}
23+
}

src/Symfony/Component/HttpFoundation/Tests/UriSignerTest.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Clock\MockClock;
16+
use Symfony\Component\HttpFoundation\Exception\ExpiredSignedUriException;
1617
use Symfony\Component\HttpFoundation\Exception\LogicException;
18+
use Symfony\Component\HttpFoundation\Exception\UnsignedUriException;
19+
use Symfony\Component\HttpFoundation\Exception\UnverifiedSignedUriException;
1720
use Symfony\Component\HttpFoundation\Request;
1821
use Symfony\Component\HttpFoundation\UriSigner;
1922

@@ -228,4 +231,50 @@ public function testNonUrlSafeBase64()
228231
$signer = new UriSigner('foobar');
229232
$this->assertTrue($signer->check('http://example.com/foo?_hash=rIOcC%2FF3DoEGo%2FvnESjSp7uU9zA9S%2F%2BOLhxgMexoPUM%3D&baz=bay&foo=bar'));
230233
}
234+
235+
public function testVerifyUnSignedUri()
236+
{
237+
$signer = new UriSigner('foobar');
238+
$uri = 'http://example.com/foo';
239+
240+
try {
241+
$signer->verify($uri);
242+
243+
$this->fail('Exception not thrown.');
244+
} catch (UnsignedUriException $e) {
245+
$this->assertSame('The URI is not signed.', $e->getMessage());
246+
$this->assertSame($uri, $e->uri);
247+
}
248+
}
249+
250+
public function testVerifyUnverifiedUri()
251+
{
252+
$signer = new UriSigner('foobar');
253+
$uri = 'http://example.com/foo?_hash=invalid';
254+
255+
try {
256+
$signer->verify($uri);
257+
258+
$this->fail('Exception not thrown.');
259+
} catch (UnverifiedSignedUriException $e) {
260+
$this->assertSame('The URI signature is invalid.', $e->getMessage());
261+
$this->assertSame($uri, $e->uri);
262+
}
263+
}
264+
265+
public function testVerifyExpiredUri()
266+
{
267+
$signer = new UriSigner('foobar');
268+
$uri = $signer->sign('http://example.com/foo', 123456);
269+
270+
try {
271+
$signer->verify($uri);
272+
273+
$this->fail('Exception not thrown.');
274+
} catch (ExpiredSignedUriException $e) {
275+
$this->assertSame('The URI has expired.', $e->getMessage());
276+
$this->assertSame($uri, $e->uri);
277+
$this->assertSame(123456, $e->expiredAt->getTimestamp());
278+
}
279+
}
231280
}

src/Symfony/Component/HttpFoundation/UriSigner.php

Lines changed: 82 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,22 @@
1212
namespace Symfony\Component\HttpFoundation;
1313

1414
use Psr\Clock\ClockInterface;
15+
use Symfony\Component\HttpFoundation\Exception\ExpiredSignedUriException;
1516
use Symfony\Component\HttpFoundation\Exception\LogicException;
17+
use Symfony\Component\HttpFoundation\Exception\SignedUriException;
18+
use Symfony\Component\HttpFoundation\Exception\UnsignedUriException;
19+
use Symfony\Component\HttpFoundation\Exception\UnverifiedSignedUriException;
1620

1721
/**
1822
* @author Fabien Potencier <[email protected]>
1923
*/
2024
class UriSigner
2125
{
26+
private const STATUS_VALID = 1;
27+
private const STATUS_INVALID = 2;
28+
private const STATUS_MISSING = 3;
29+
private const STATUS_EXPIRED = 4;
30+
2231
/**
2332
* @param string $hashParameter Query string parameter to use
2433
* @param string $expirationParameter Query string parameter to use for expiration
@@ -91,38 +100,44 @@ public function sign(string $uri/* , \DateTimeInterface|\DateInterval|int|null $
91100
*/
92101
public function check(string $uri): bool
93102
{
94-
$url = parse_url($uri);
95-
$params = [];
96-
97-
if (isset($url['query'])) {
98-
parse_str($url['query'], $params);
99-
}
103+
return self::STATUS_VALID === $this->doVerify($uri);
104+
}
100105

101-
if (empty($params[$this->hashParameter])) {
102-
return false;
103-
}
106+
public function checkRequest(Request $request): bool
107+
{
108+
return self::STATUS_VALID === $this->doVerify(self::normalize($request));
109+
}
104110

105-
$hash = $params[$this->hashParameter];
106-
unset($params[$this->hashParameter]);
111+
/**
112+
* Verify a Request or string URI.
113+
*
114+
* @throws UnsignedUriException If the URI is not signed
115+
* @throws UnverifiedSignedUriException If the signature is invalid
116+
* @throws ExpiredSignedUriException If the URI has expired
117+
* @throws SignedUriException
118+
*/
119+
public function verify(Request|string $uri): void
120+
{
121+
$uri = self::normalize($uri);
122+
$status = $this->doVerify($uri);
107123

108-
// In 8.0, remove support for non-url-safe tokens
109-
if (!hash_equals($this->computeHash($this->buildUrl($url, $params)), strtr(rtrim($hash, '='), ['/' => '_', '+' => '-']))) {
110-
return false;
124+
if (self::STATUS_VALID === $status) {
125+
return;
111126
}
112127

113-
if ($expiration = $params[$this->expirationParameter] ?? false) {
114-
return $this->now()->getTimestamp() < $expiration;
128+
if (self::STATUS_MISSING === $status) {
129+
throw new UnsignedUriException($uri);
115130
}
116131

117-
return true;
118-
}
132+
if (self::STATUS_INVALID === $status) {
133+
throw new UnverifiedSignedUriException($uri);
134+
}
119135

120-
public function checkRequest(Request $request): bool
121-
{
122-
$qs = ($qs = $request->server->get('QUERY_STRING')) ? '?'.$qs : '';
136+
$url = parse_url($uri);
137+
parse_str($url['query'], $params);
138+
$expiration = $params[$this->expirationParameter];
123139

124-
// we cannot use $request->getUri() here as we want to work with the original URI (no query string reordering)
125-
return $this->check($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().$qs);
140+
throw new ExpiredSignedUriException(\DateTimeImmutable::createFromFormat('U', $expiration), $uri);
126141
}
127142

128143
private function computeHash(string $uri): string
@@ -165,4 +180,48 @@ private function now(): \DateTimeImmutable
165180
{
166181
return $this->clock?->now() ?? \DateTimeImmutable::createFromFormat('U', time());
167182
}
183+
184+
/**
185+
* @return self::STATUS_*
186+
*/
187+
private function doVerify(string $uri): int
188+
{
189+
$url = parse_url($uri);
190+
$params = [];
191+
192+
if (isset($url['query'])) {
193+
parse_str($url['query'], $params);
194+
}
195+
196+
if (empty($params[$this->hashParameter])) {
197+
return self::STATUS_MISSING;
198+
}
199+
200+
$hash = $params[$this->hashParameter];
201+
unset($params[$this->hashParameter]);
202+
203+
if (!hash_equals($this->computeHash($this->buildUrl($url, $params)), strtr(rtrim($hash, '='), ['/' => '_', '+' => '-']))) {
204+
return self::STATUS_INVALID;
205+
}
206+
207+
if (!$expiration = $params[$this->expirationParameter] ?? false) {
208+
return self::STATUS_VALID;
209+
}
210+
211+
if ($this->now()->getTimestamp() < $expiration) {
212+
return self::STATUS_VALID;
213+
}
214+
215+
return self::STATUS_EXPIRED;
216+
}
217+
218+
private static function normalize(Request|string $uri): string
219+
{
220+
if ($uri instanceof Request) {
221+
$qs = ($qs = $uri->server->get('QUERY_STRING')) ? '?'.$qs : '';
222+
$uri = $uri->getSchemeAndHttpHost().$uri->getBaseUrl().$uri->getPathInfo().$qs;
223+
}
224+
225+
return $uri;
226+
}
168227
}

0 commit comments

Comments
 (0)