diff --git a/src/Illuminate/Routing/RoutingServiceProvider.php b/src/Illuminate/Routing/RoutingServiceProvider.php index 547e195e3866..e26202d85ac6 100755 --- a/src/Illuminate/Routing/RoutingServiceProvider.php +++ b/src/Illuminate/Routing/RoutingServiceProvider.php @@ -77,7 +77,9 @@ protected function registerUrlGenerator() }); $url->setKeyResolver(function () { - return $this->app->make('config')->get('app.key'); + $config = $this->app->make('config'); + + return [$config->get('app.key'), ...($config->get('app.previous_keys') ?? [])]; }); // If the route collection is "rebound", for example, when the routes stay diff --git a/src/Illuminate/Routing/UrlGenerator.php b/src/Illuminate/Routing/UrlGenerator.php index 3bfd6127d464..df4243313b5f 100755 --- a/src/Illuminate/Routing/UrlGenerator.php +++ b/src/Illuminate/Routing/UrlGenerator.php @@ -370,7 +370,11 @@ public function signedRoute($name, $parameters = [], $expiration = null, $absolu $key = call_user_func($this->keyResolver); return $this->route($name, $parameters + [ - 'signature' => hash_hmac('sha256', $this->route($name, $parameters, $absolute), $key), + 'signature' => hash_hmac( + 'sha256', + $this->route($name, $parameters, $absolute), + is_array($key) ? $key[0] : $key + ), ], $absolute); } @@ -455,9 +459,20 @@ public function hasCorrectSignature(Request $request, $absolute = true, array $i $original = rtrim($url.'?'.$queryString, '?'); - $signature = hash_hmac('sha256', $original, call_user_func($this->keyResolver)); + $keys = call_user_func($this->keyResolver); - return hash_equals($signature, (string) $request->query('signature', '')); + $keys = is_array($keys) ? $keys : [$keys]; + + foreach ($keys as $key) { + if (hash_equals( + hash_hmac('sha256', $original, $key), + (string) $request->query('signature', '') + )) { + return true; + } + } + + return false; } /** diff --git a/tests/Integration/Routing/UrlSigningTest.php b/tests/Integration/Routing/UrlSigningTest.php index 66dd9a87cda5..f836066d26f9 100644 --- a/tests/Integration/Routing/UrlSigningTest.php +++ b/tests/Integration/Routing/UrlSigningTest.php @@ -354,6 +354,32 @@ public function testItCanGenerateMiddlewareDefinitionViaStaticMethod() $this->assertSame('Illuminate\Routing\Middleware\ValidateSignature:foo,bar', $signature); } + public function testUrlsSignedByPreviousAppKeysAreValidWhenAddedAsPreviousKeys() + { + Route::get('/foo/{id}', function (Request $request, $id) { + return $request->hasValidSignature() ? 'valid' : 'invalid'; + })->name('foo'); + + config(['app.key' => 'oldest-key']); + $oldestURL = URL::signedRoute('foo', ['id' => 1]); + + config(['app.key' => 'old-key']); + $oldURL = URL::signedRoute('foo', ['id' => 1]); + + config(['app.key' => 'new-key']); + $newUrl = URL::signedRoute('foo', ['id' => 1]); + + tap($this->get($oldestURL), fn ($response) => $this->assertSame('invalid', $response->original)); + tap($this->get($oldURL), fn ($response) => $this->assertSame('invalid', $response->original)); + tap($this->get($newUrl), fn ($response) => $this->assertSame('valid', $response->original)); + + config(['app.previous_keys' => ['old-key', 'oldest-key']]); + + tap($this->get($oldestURL), fn ($response) => $this->assertSame('valid', $response->original)); + tap($this->get($oldURL), fn ($response) => $this->assertSame('valid', $response->original)); + tap($this->get($newUrl), fn ($response) => $this->assertSame('valid', $response->original)); + } + protected function createValidateSignatureMiddleware(array $ignore) { return new class($ignore) extends ValidateSignature diff --git a/tests/Routing/RoutingUrlGeneratorTest.php b/tests/Routing/RoutingUrlGeneratorTest.php index 1d92d5e6e1b0..96ae839ac513 100755 --- a/tests/Routing/RoutingUrlGeneratorTest.php +++ b/tests/Routing/RoutingUrlGeneratorTest.php @@ -894,7 +894,7 @@ public function testSignedUrlWithKeyResolver() $request = Request::create('http://www.foo.com/') ); $url->setKeyResolver(function () { - return 'secret'; + return 'first-secret'; }); $route = new Route(['GET'], 'foo', ['as' => 'foo', function () { @@ -902,24 +902,32 @@ public function testSignedUrlWithKeyResolver() }]); $routes->add($route); - $request = Request::create($url->signedRoute('foo')); + $firstRequest = Request::create($url->signedRoute('foo')); - $this->assertTrue($url->hasValidSignature($request)); + $this->assertTrue($url->hasValidSignature($firstRequest)); $request = Request::create($url->signedRoute('foo').'?tempered=true'); $this->assertFalse($url->hasValidSignature($request)); $url2 = $url->withKeyResolver(function () { - return 'other-secret'; + return 'second-secret'; }); - $this->assertFalse($url2->hasValidSignature($request)); + $this->assertFalse($url2->hasValidSignature($firstRequest)); - $request = Request::create($url2->signedRoute('foo')); + $secondRequest = Request::create($url2->signedRoute('foo')); - $this->assertTrue($url2->hasValidSignature($request)); - $this->assertFalse($url->hasValidSignature($request)); + $this->assertTrue($url2->hasValidSignature($secondRequest)); + $this->assertFalse($url->hasValidSignature($secondRequest)); + + // Key resolver also supports multiple keys, for app key rotation via the config "app.previous_keys" + $url3 = $url->withKeyResolver(function () { + return ['first-secret', 'second-secret']; + }); + + $this->assertTrue($url3->hasValidSignature($firstRequest)); + $this->assertTrue($url3->hasValidSignature($secondRequest)); } public function testMissingNamedRouteResolution()