Skip to content

Commit a8d37f8

Browse files
committed
SignUrl: Allow more methods, normalize signed URL (remove #hash)
1 parent 1205342 commit a8d37f8

File tree

3 files changed

+119
-12
lines changed

3 files changed

+119
-12
lines changed

.phpstorm.meta.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,47 @@
3434
\Redbitcz\DebugMode\Detector::MODE_ENV,
3535
\Redbitcz\DebugMode\Detector::MODE_IP
3636
);
37+
38+
expectedArguments(
39+
\Redbitcz\DebugMode\Plugin\SignedUrl::__construct(),
40+
1,
41+
'ES384',
42+
'ES256',
43+
'HS256',
44+
'HS384',
45+
'HS512',
46+
'RS256',
47+
'RS384',
48+
'RS512'
49+
);
50+
51+
expectedArguments(
52+
\Redbitcz\DebugMode\Plugin\SignedUrl::signUrl(),
53+
2,
54+
\Redbitcz\DebugMode\Plugin\SignedUrl::MODE_REQUEST,
55+
\Redbitcz\DebugMode\Plugin\SignedUrl::MODE_ENABLER,
56+
\Redbitcz\DebugMode\Plugin\SignedUrl::MODE_DEACTIVATE_ENABLER
57+
);
58+
59+
expectedArguments(
60+
\Redbitcz\DebugMode\Plugin\SignedUrl::signUrl(),
61+
3,
62+
\Redbitcz\DebugMode\Plugin\SignedUrl::VALUE_DISABLE,
63+
\Redbitcz\DebugMode\Plugin\SignedUrl::VALUE_ENABLE
64+
);
65+
66+
expectedArguments(
67+
\Redbitcz\DebugMode\Plugin\SignedUrl::getToken(),
68+
3,
69+
\Redbitcz\DebugMode\Plugin\SignedUrl::MODE_REQUEST,
70+
\Redbitcz\DebugMode\Plugin\SignedUrl::MODE_ENABLER,
71+
\Redbitcz\DebugMode\Plugin\SignedUrl::MODE_DEACTIVATE_ENABLER
72+
);
73+
74+
expectedArguments(
75+
\Redbitcz\DebugMode\Plugin\SignedUrl::getToken(),
76+
4,
77+
\Redbitcz\DebugMode\Plugin\SignedUrl::VALUE_DISABLE,
78+
\Redbitcz\DebugMode\Plugin\SignedUrl::VALUE_ENABLE
79+
);
3780
}

src/Plugin/SignedUrl.php

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ class SignedUrl implements Plugin
3232

3333
private const URL_QUERY_TOKEN_KEY = '_debug';
3434
private const ISSUER_ID = 'cz.redbit.debug.url';
35-
private const HTTP_METHOD_GET = 'get';
3635

3736
/** @var resource|string */
3837
private $key;
@@ -58,12 +57,14 @@ public function __construct($key, string $algorithm = 'HS256', ?string $audience
5857

5958
/**
6059
* @param string|int|DateTimeInterface $expire
60+
* @param array<int, string> $allowedHttpMethods
6161
*/
6262
public function signUrl(
6363
string $url,
6464
$expire,
6565
int $mode = self::MODE_REQUEST,
66-
int $value = self::VALUE_ENABLE
66+
int $value = self::VALUE_ENABLE,
67+
array $allowedHttpMethods = ['get']
6768
): string {
6869
/** @var ParsedUrl|false $parsedUrl */
6970
$parsedUrl = parse_url($url);
@@ -76,7 +77,9 @@ public function signUrl(
7677
throw new LogicException('Only absolute URL is allowed to sign');
7778
}
7879

79-
$token = $this->getToken($url, $expire, $mode, $value);
80+
$signUrl = $this->normalizeUrl($parsedUrl);
81+
82+
$token = $this->getToken($this->buildUrl($signUrl), $allowedHttpMethods, $expire, $mode, $value);
8083

8184
$parsedUrl['query'] = ($parsedUrl['query'] ?? '') . ((($parsedUrl['query'] ?? '') === '') ? '?' : '&')
8285
. self::URL_QUERY_TOKEN_KEY . '=' . urlencode($token);
@@ -85,13 +88,15 @@ public function signUrl(
8588
}
8689

8790
/**
91+
* @param array<int, string> $allowedMethods
8892
* @param string|int|DateTimeInterface $expire
8993
*/
9094
public function getToken(
9195
string $url,
96+
array $allowedMethods,
9297
$expire,
93-
int $mode = self::MODE_REQUEST,
94-
int $value = self::VALUE_ENABLE
98+
int $mode,
99+
int $value
95100
): string {
96101
$expire = (int)DateTime::from($expire)->format('U');
97102

@@ -101,7 +106,7 @@ public function getToken(
101106
'iat' => $this->timestamp ?? time(),
102107
'exp' => $expire,
103108
'sub' => $url,
104-
'meth' => [self::HTTP_METHOD_GET],
109+
'meth' => $allowedMethods,
105110
'mod' => $mode,
106111
'val' => $value,
107112
];
@@ -194,6 +199,7 @@ public function verifyUrl(string $url, bool $allowRedirect = false): array
194199
$this->sendRedirectResponse($canonicalUrl);
195200
}
196201

202+
$parsedUrl = $this->normalizeUrl($parsedUrl);
197203
$signedUrl = $this->buildUrl(['query' => substr($query, 0, $tokenOffset)] + $parsedUrl);
198204

199205
if ($signedUrl !== $allowedUrl) {
@@ -308,4 +314,16 @@ protected function sendRedirectResponse(string $canonicalUrl): void
308314
echo "<h1>Redirect</h1>\n\n<p><a href=\"{$escapedUrl}\">Please click here to continue</a>.</p>";
309315
die();
310316
}
317+
318+
/**
319+
* @param ParsedUrl $url
320+
* @return ParsedUrl
321+
*/
322+
protected function normalizeUrl(array $url): array
323+
{
324+
$url['path'] = ($url['path'] ?? '') === '' ? '/' : ($url['path']??'');
325+
unset($url['fragment']);
326+
/** @var ParsedUrl $url (bypass PhpStan bug) */
327+
return $url;
328+
}
311329
}

tests/Plugin/SignUrlTest.php

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,30 @@ public function testSign(): void
2828
Assert::equal($expected, $token);
2929
}
3030

31+
public function testSignFragment(): void
32+
{
33+
$audience = 'test.' . __FUNCTION__;
34+
35+
$plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience);
36+
$plugin->setTimestamp(1600000000);
37+
$token = $plugin->signUrl('https://host.tld/path?query=value#fragment', 1600000600);
38+
$expected = 'https://host.tld/path?query=value&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbkZyYWdtZW50IiwiaWF0IjoxNjAwMDAwMDAwLCJleHAiOjE2MDAwMDA2MDAsInN1YiI6Imh0dHBzOlwvXC9ob3N0LnRsZFwvcGF0aD9xdWVyeT12YWx1ZSIsIm1ldGgiOlsiZ2V0Il0sIm1vZCI6MCwidmFsIjoxfQ.9oIORBXW-hW8vTPdJglEdEMm19nwAvw2wLAxqWvFh3Y#fragment';
39+
Assert::equal($expected, $token);
40+
}
41+
3142
public function testGetToken(): void
3243
{
3344
$audience = 'test.' . __FUNCTION__;
3445

3546
$plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience);
3647
$plugin->setTimestamp(1600000000);
37-
$token = $plugin->getToken('https://host.tld/path?query=value', 1600000600);
48+
$token = $plugin->getToken(
49+
'https://host.tld/path?query=value',
50+
['get'],
51+
1600000600,
52+
SignedUrl::MODE_REQUEST,
53+
SignedUrl::VALUE_ENABLE
54+
);
3855
$expected = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0R2V0VG9rZW4iLCJpYXQiOjE2MDAwMDAwMDAsImV4cCI6MTYwMDAwMDYwMCwic3ViIjoiaHR0cHM6XC9cL2hvc3QudGxkXC9wYXRoP3F1ZXJ5PXZhbHVlIiwibWV0aCI6WyJnZXQiXSwibW9kIjowLCJ2YWwiOjF9.I6tEfFneSxuY9qAjRf5esYFPonChbliZqGoijtv2iHw';
3956
Assert::equal($expected, $token);
4057
}
@@ -46,7 +63,13 @@ public function testVerifyToken(): void
4663

4764
$plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience);
4865
$plugin->setTimestamp($timestamp);
49-
$token = $plugin->getToken('https://host.tld/path?query=value', 1600000600);
66+
$token = $plugin->getToken(
67+
'https://host.tld/path?query=value',
68+
['get'],
69+
1600000600,
70+
SignedUrl::MODE_REQUEST,
71+
SignedUrl::VALUE_ENABLE
72+
);
5073

5174
$plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience);
5275
$plugin->setTimestamp($timestamp);
@@ -171,16 +194,39 @@ function () use ($timestamp, $tokenUrl) {
171194
public function testVerifyUrlWithSuffixRedirect(): void
172195
{
173196
$timestamp = 1600000000;
174-
$expected = 'https://host.tld/path?query=value&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwNjAwLCJzdWIiOiJodHRwczpcL1wvaG9zdC50bGRcL3BhdGg_cXVlcnk9dmFsdWUiLCJtZXRoIjoiZ2V0IiwibW9kIjowLCJ2YWwiOjF9.61Z0pPW3lJN2WDoUhOfsZ4m16Q3hjtVFJep_t_qoQ5c';
175-
176-
$tokenUrl = $expected . '&fbclid=123456789';
197+
$tokenUrl = 'https://host.tld/path?query=value&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwNjAwLCJzdWIiOiJodHRwczpcL1wvaG9zdC50bGRcL3BhdGg_cXVlcnk9dmFsdWUiLCJtZXRoIjoiZ2V0IiwibW9kIjowLCJ2YWwiOjF9.61Z0pPW3lJN2WDoUhOfsZ4m16Q3hjtVFJep_t_qoQ5c'
198+
. '&fbclid=123456789';
177199

178200
// Mock plugin without redirect
179201
$plugin = new class(self::KEY_HS256, 'HS256', 'test.testSign') extends SignedUrl {
180202
protected function sendRedirectResponse(string $canonicalUrl): void
181203
{
182204
$expected = 'https://host.tld/path?query=value&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwNjAwLCJzdWIiOiJodHRwczpcL1wvaG9zdC50bGRcL3BhdGg_cXVlcnk9dmFsdWUiLCJtZXRoIjoiZ2V0IiwibW9kIjowLCJ2YWwiOjF9.61Z0pPW3lJN2WDoUhOfsZ4m16Q3hjtVFJep_t_qoQ5c';
183-
Assert::equal($canonicalUrl, $expected);
205+
Assert::equal($expected, $canonicalUrl);
206+
}
207+
};
208+
209+
$plugin->setTimestamp($timestamp);
210+
JWT::$timestamp = $timestamp;
211+
$plugin->verifyUrl($tokenUrl, true);
212+
}
213+
214+
public function testVerifyUrlWithSuffixRedirectFragment(): void
215+
{
216+
$timestamp = 1600000000;
217+
$tokenUrl = 'https://host.tld/path?query=value'
218+
. '&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwNjAwLCJzdWIiOiJodHRwczpcL1wvaG9zdC50bGRcL3BhdGg_cXVlcnk9dmFsdWUiLCJtZXRoIjoiZ2V0IiwibW9kIjowLCJ2YWwiOjF9.61Z0pPW3lJN2WDoUhOfsZ4m16Q3hjtVFJep_t_qoQ5c'
219+
. '&fbclid=123456789'
220+
. '#hash';
221+
222+
// Mock plugin without redirect
223+
$plugin = new class(self::KEY_HS256, 'HS256', 'test.testSign') extends SignedUrl {
224+
protected function sendRedirectResponse(string $canonicalUrl): void
225+
{
226+
$expected = 'https://host.tld/path?query=value'
227+
. '&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwNjAwLCJzdWIiOiJodHRwczpcL1wvaG9zdC50bGRcL3BhdGg_cXVlcnk9dmFsdWUiLCJtZXRoIjoiZ2V0IiwibW9kIjowLCJ2YWwiOjF9.61Z0pPW3lJN2WDoUhOfsZ4m16Q3hjtVFJep_t_qoQ5c'
228+
. '#hash';
229+
Assert::equal($expected, $canonicalUrl);
184230
}
185231
};
186232

0 commit comments

Comments
 (0)