Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.6] Signed Routes #23519

Merged
merged 12 commits into from
Mar 13, 2018
18 changes: 16 additions & 2 deletions src/Illuminate/Foundation/Providers/FoundationServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\AggregateServiceProvider;

class FoundationServiceProvider extends AggregateServiceProvider
Expand All @@ -26,15 +27,16 @@ public function register()
{
parent::register();

$this->registerRequestValidate();
$this->registerRequestValidation();
$this->registerRequestSignatureValidation();
}

/**
* Register the "validate" macro on the request.
*
* @return void
*/
public function registerRequestValidate()
public function registerRequestValidation()
{
Request::macro('validate', function (array $rules, ...$params) {
validator()->validate($this->all(), $rules, ...$params);
Expand All @@ -44,4 +46,16 @@ public function registerRequestValidate()
})->unique()->toArray());
});
}

/**
* Register the "hasValidSignature" macro on the request.
*
* @return void
*/
public function registerRequestSignatureValidation()
{
Request::macro('hasValidSignature', function () {
return URL::hasValidSignature($this);
});
}
}
19 changes: 19 additions & 0 deletions src/Illuminate/Routing/Exceptions/InvalidSignatureException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Illuminate\Routing\Exceptions;

use Exception;
use Symfony\Component\HttpKernel\Exception\HttpException;

class InvalidSignatureException extends HttpException
{
/**
* Create a new exception instance.
*
* @return void
*/
public function __construct()
{
parent::__construct(401, 'Invalid signature.');
}
}
27 changes: 27 additions & 0 deletions src/Illuminate/Routing/Middleware/ValidateSignature.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Illuminate\Routing\Middleware;

use Closure;
use Illuminate\Routing\Exceptions\InvalidSignatureException;

class ValidateSignature
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return \Illuminate\Http\Response
*
* @throws \Illuminate\Routing\Exceptions\InvalidSignatureException
*/
public function handle($request, Closure $next)
{
if ($request->hasValidSignature($request)) {
return $next($request);
}

throw new InvalidSignatureException;
}
}
7 changes: 7 additions & 0 deletions src/Illuminate/Routing/RoutingServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,17 @@ protected function registerUrlGenerator()
)
);

// Next we will set a few service resolvers on the URL generator so it can
// get the information it needs to function. This just provides some of
// the convenience features to this URL generator like "signed" URLs.
$url->setSessionResolver(function () {
return $this->app['session'];
});

$url->setKeyResolver(function () {
return $this->app->make('config')->get('app.key');
});

// If the route collection is "rebound", for example, when the routes stay
// cached for the application, we will need to rebind the routes on the
// URL generator instance so it has the latest version of the routes.
Expand Down
78 changes: 77 additions & 1 deletion src/Illuminate/Routing/UrlGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use InvalidArgumentException;
use Illuminate\Support\Carbon;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Support\InteractsWithTime;
use Illuminate\Contracts\Routing\UrlRoutable;
use Illuminate\Contracts\Routing\UrlGenerator as UrlGeneratorContract;

class UrlGenerator implements UrlGeneratorContract
{
use Macroable;
use InteractsWithTime, Macroable;

/**
* The route collection.
Expand Down Expand Up @@ -71,6 +73,13 @@ class UrlGenerator implements UrlGeneratorContract
*/
protected $sessionResolver;

/**
* The encryption key resolver callable.
*
* @var callable
*/
protected $keyResolver;

/**
* The callback to use to format hosts.
*
Expand Down Expand Up @@ -286,6 +295,60 @@ public function formatScheme($secure)
return $this->cachedSchema;
}

/**
* Create a signed route URL for a named route.
*
* @param string $name
* @param array $parameters
* @param \DateTimeInterface|int $expiration
* @return string
*/
public function signedRoute($name, $parameters = [], $expiration = null)
{
if ($expiration) {
$parameters = $parameters + ['expires' => $this->availableAt($expiration)];
}

$key = call_user_func($this->keyResolver);

return $this->route($name, $parameters + [
'signature' => hash_hmac('sha256', $this->route($name, $parameters), $key),
]);
}

/**
* Create a temporary signed route URL for a named route.
*
* @param string $name
* @param \DateTimeInterface|int $expiration
* @param array $parameters
* @return string
*/
public function temporarySignedRoute($name, $expiration, $parameters = [])
{
return $this->signedRoute($name, $parameters, $expiration);
}

/**
* Determine if the given request has a valid signature.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
public function hasValidSignature(Request $request)
{
$original = rtrim($request->url().'?'.http_build_query(
Arr::except($request->query(), 'signature')
), '?');

$expires = Arr::get($request->query(), 'expires');

$signature = hash_hmac('sha256', $original, call_user_func($this->keyResolver));

return $request->query('signature') === $signature &&
! ($expires && Carbon::now()->getTimestamp() > $expires);
}

/**
* Get the URL to a named route.
*
Expand Down Expand Up @@ -614,6 +677,19 @@ public function setSessionResolver(callable $sessionResolver)
return $this;
}

/**
* Set the encryption key resolver.
*
* @param callable $keyResolver
* @return $this
*/
public function setKeyResolver(callable $keyResolver)
{
$this->keyResolver = $keyResolver;

return $this;
}

/**
* Set the root controller namespace.
*
Expand Down
65 changes: 65 additions & 0 deletions tests/Integration/Routing/UrlSigningTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace Illuminate\Tests\Integration\Routing;

use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Orchestra\Testbench\TestCase;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Route;
use Illuminate\Routing\Middleware\ValidateSignature;

/**
* @group integration
*/
class UrlSigningTest extends TestCase
{
public function test_signing_url()
{
Route::get('/foo/{id}', function (Request $request, $id) {
return $request->hasValidSignature() ? 'valid' : 'invalid';
})->name('foo');

$this->assertTrue(is_string($url = URL::signedRoute('foo', ['id' => 1])));
$this->assertEquals('valid', $this->get($url)->original);
}

public function test_temporary_signed_urls()
{
Route::get('/foo/{id}', function (Request $request, $id) {
return $request->hasValidSignature() ? 'valid' : 'invalid';
})->name('foo');

Carbon::setTestNow(Carbon::create(2018, 1, 1));
$this->assertTrue(is_string($url = URL::temporarySignedRoute('foo', now()->addMinutes(5), ['id' => 1])));
$this->assertEquals('valid', $this->get($url)->original);

Carbon::setTestNow(Carbon::create(2018, 1, 1)->addMinutes(10));
$this->assertEquals('invalid', $this->get($url)->original);
}

public function test_signed_middleware()
{
Route::get('/foo/{id}', function (Request $request, $id) {
return $request->hasValidSignature() ? 'valid' : 'invalid';
})->name('foo')->middleware(ValidateSignature::class);

Carbon::setTestNow(Carbon::create(2018, 1, 1));
$this->assertTrue(is_string($url = URL::temporarySignedRoute('foo', now()->addMinutes(5), ['id' => 1])));
$this->assertEquals('valid', $this->get($url)->original);
}

public function test_signed_middleware_with_invalid_url()
{
Route::get('/foo/{id}', function (Request $request, $id) {
return $request->hasValidSignature() ? 'valid' : 'invalid';
})->name('foo')->middleware(ValidateSignature::class);

Carbon::setTestNow(Carbon::create(2018, 1, 1));
$this->assertTrue(is_string($url = URL::temporarySignedRoute('foo', now()->addMinutes(5), ['id' => 1])));
Carbon::setTestNow(Carbon::create(2018, 1, 1)->addMinutes(10));

$response = $this->get($url);
$response->assertStatus(401);
}
}