Skip to content

Commit dac4d7c

Browse files
committed
SignUrl: Add tests for failings case
1 parent 388eaea commit dac4d7c

File tree

2 files changed

+113
-9
lines changed

2 files changed

+113
-9
lines changed

src/Plugin/SignedUrl.php

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -190,11 +190,7 @@ public function verifyUrl(string $url, bool $allowRedirect = false): array
190190
}
191191

192192
$canonicalUrl = $this->buildUrl(['query' => substr($query, 0, $remainingOffset)] + $url);
193-
header('Cache-Control: s-maxage=0, max-age=0, must-revalidate', true, 302);
194-
header('Expires: Mon, 23 Jan 1978 10:00:00 GMT', true);
195-
header('Location: ' . $canonicalUrl);
196-
$escapedUrl = htmlspecialchars($canonicalUrl, ENT_IGNORE | ENT_QUOTES, 'UTF-8');
197-
echo "<h1>Redirect</h1>\n\n<p><a href=\"{$escapedUrl}\">Please click here to continue</a>.</p>";
193+
$this->sendRedirectResponse($canonicalUrl);
198194
}
199195

200196
$signedUrl = $this->buildUrl(['query' => substr($query, 0, $tokenOffset)] + $url);
@@ -221,7 +217,6 @@ public function verifyToken(string $token): array
221217
// Check mandatory claims presence
222218
if (isset(
223219
$payload->iss,
224-
$payload->aud,
225220
$payload->iat,
226221
$payload->exp,
227222
$payload->sub,
@@ -235,7 +230,7 @@ public function verifyToken(string $token): array
235230
// Check mandatory claims
236231
if (
237232
$payload->iss !== self::ISSUER_ID
238-
|| $payload->aud !== $this->audience
233+
|| ($payload->aud ?? null) !== $this->audience
239234
) {
240235
throw new SignedUrlVerificationException('JWT Token mandatory Claims doesn\'t match');
241236
}
@@ -263,7 +258,7 @@ public function verifyToken(string $token): array
263258
/**
264259
* @param ParsedUrl $parsedUrl
265260
*/
266-
private function buildUrl(array $parsedUrl): string
261+
protected function buildUrl(array $parsedUrl): string
267262
{
268263
return (isset($parsedUrl['scheme']) ? "{$parsedUrl['scheme']}:" : '')
269264
. ((isset($parsedUrl['user']) || isset($parsedUrl['host'])) ? '//' : '')
@@ -277,7 +272,7 @@ private function buildUrl(array $parsedUrl): string
277272
. (isset($parsedUrl['fragment']) ? "#{$parsedUrl['fragment']}" : '');
278273
}
279274

280-
private function urlFromGlobal(): string
275+
protected function urlFromGlobal(): string
281276
{
282277
$urlSegments = [];
283278
$urlSegments['scheme'] = !empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https' : 'http';
@@ -302,4 +297,14 @@ public function setTimestamp(?int $timestamp): void
302297
{
303298
$this->timestamp = $timestamp;
304299
}
300+
301+
protected function sendRedirectResponse(string $canonicalUrl): void
302+
{
303+
header('Cache-Control: s-maxage=0, max-age=0, must-revalidate', true, 302);
304+
header('Expires: Mon, 23 Jan 1978 10:00:00 GMT', true);
305+
header('Location: ' . $canonicalUrl);
306+
$escapedUrl = htmlspecialchars($canonicalUrl, ENT_IGNORE | ENT_QUOTES, 'UTF-8');
307+
echo "<h1>Redirect</h1>\n\n<p><a href=\"{$escapedUrl}\">Please click here to continue</a>.</p>";
308+
die();
309+
}
305310
}

tests/Plugin/SignUrlTest.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
namespace Redbitcz\DebugModeTests\Plugin;
66

77
use Firebase\JWT\JWT;
8+
use LogicException;
89
use Redbitcz\DebugMode\Plugin\SignedUrl;
10+
use Redbitcz\DebugMode\Plugin\SignedUrlVerificationException;
911
use Tester\Assert;
1012

1113
require __DIR__ . '/../bootstrap.php';
@@ -89,6 +91,103 @@ public function testVerifyRequest(): void
8991
$expected = [0, 1, 1600000600];
9092
Assert::equal($expected, $parsed);
9193
}
94+
95+
public function testSignInvalidUrl()
96+
{
97+
Assert::exception(function () {
98+
$url = (string)base64_decode('Ly8Eijrg+qawZw==');
99+
$plugin = new SignedUrl(self::KEY_HS256, 'HS256');
100+
$plugin->signUrl($url, 1600000600);
101+
}, LogicException::class);
102+
}
103+
104+
public function testSignRelativeUrl()
105+
{
106+
Assert::exception(function () {
107+
$url = '/login?email=foo@bar.cz';
108+
$plugin = new SignedUrl(self::KEY_HS256, 'HS256');
109+
$plugin->signUrl($url, 1600000600);
110+
}, LogicException::class);
111+
}
112+
113+
public function testVerifyPostRequest(): void
114+
{
115+
$audience = 'test.' . __FUNCTION__;
116+
$timestamp = 1600000000;
117+
$url = 'https://host.tld/path?query=value';
118+
119+
$plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience);
120+
$plugin->setTimestamp($timestamp);
121+
$tokenUrl = $plugin->signUrl($url, 1600000600);
122+
123+
$plugin = new SignedUrl(self::KEY_HS256, 'HS256', $audience);
124+
$plugin->setTimestamp($timestamp);
125+
JWT::$timestamp = $timestamp;
126+
Assert::exception(function () use ($plugin, $tokenUrl) {
127+
$plugin->verifyRequest(false, $tokenUrl, 'POST');
128+
}, SignedUrlVerificationException::class, 'HTTP method doesn\'t match signed HTTP method');
129+
}
130+
131+
public function testVerifyInvalidRequest(): void
132+
{
133+
Assert::exception(function () {
134+
$plugin = new SignedUrl(self::KEY_HS256, 'HS256');
135+
$url = (string)base64_decode('Ly8Eijrg+qawZw==');
136+
$plugin->verifyRequest(false, $url, 'GET');
137+
}, SignedUrlVerificationException::class, 'Url is invalid');
138+
}
139+
140+
public function testVerifyInvalidUrl()
141+
{
142+
Assert::exception(function () {
143+
$plugin = new SignedUrl(self::KEY_HS256, 'HS256');
144+
$plugin->verifyUrl('https://host.tld/path?query=value');
145+
}, SignedUrlVerificationException::class, 'No token in URL');
146+
}
147+
148+
public function testVerifyUrlWithSuffix(): void
149+
{
150+
$timestamp = 1600000000;
151+
$url = 'https://host.tld/path?query=value';
152+
153+
$plugin = new SignedUrl(self::KEY_HS256, 'HS256');
154+
$plugin->setTimestamp($timestamp);
155+
$tokenUrl = $plugin->signUrl($url, 1600000600);
156+
157+
$tokenUrl .= '&fbclid=123456789';
158+
159+
Assert::exception(
160+
function () use ($timestamp, $tokenUrl) {
161+
$plugin = new SignedUrl(self::KEY_HS256, 'HS256');
162+
$plugin->setTimestamp($timestamp);
163+
JWT::$timestamp = $timestamp;
164+
$plugin->verifyUrl($tokenUrl);
165+
},
166+
SignedUrlVerificationException::class,
167+
'URL contains unallowed queries after Signing Token'
168+
);
169+
}
170+
171+
public function testVerifyUrlWithSuffixRedirect(): void
172+
{
173+
$timestamp = 1600000000;
174+
$expected = 'https://host.tld/path?query=value&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwNjAwLCJzdWIiOiJodHRwczpcL1wvaG9zdC50bGRcL3BhdGg_cXVlcnk9dmFsdWUiLCJtZXRoIjoiZ2V0IiwibW9kIjowLCJ2YWwiOjF9.61Z0pPW3lJN2WDoUhOfsZ4m16Q3hjtVFJep_t_qoQ5c';
175+
176+
$tokenUrl = $expected . '&fbclid=123456789';
177+
178+
// Mock plugin without redirect
179+
$plugin = new class(self::KEY_HS256, 'HS256', 'test.testSign') extends SignedUrl {
180+
protected function sendRedirectResponse(string $canonicalUrl): void
181+
{
182+
$expected = 'https://host.tld/path?query=value&_debug=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjei5yZWRiaXQuZGVidWcudXJsIiwiYXVkIjoidGVzdC50ZXN0U2lnbiIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwNjAwLCJzdWIiOiJodHRwczpcL1wvaG9zdC50bGRcL3BhdGg_cXVlcnk9dmFsdWUiLCJtZXRoIjoiZ2V0IiwibW9kIjowLCJ2YWwiOjF9.61Z0pPW3lJN2WDoUhOfsZ4m16Q3hjtVFJep_t_qoQ5c';
183+
Assert::equal($canonicalUrl, $expected);
184+
}
185+
};
186+
187+
$plugin->setTimestamp($timestamp);
188+
JWT::$timestamp = $timestamp;
189+
$plugin->verifyUrl($tokenUrl, true);
190+
}
92191
}
93192

94193
(new SignUrlTest())->run();

0 commit comments

Comments
 (0)