Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 10 additions & 35 deletions src/Http/Middleware/EnsureFrontendRequestsAreStateful.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
namespace Laravel\Sanctum\Http\Middleware;

use Illuminate\Routing\Pipeline;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Laravel\Sanctum\Sanctum;

class EnsureFrontendRequestsAreStateful
{
Expand All @@ -20,11 +17,16 @@ public function handle($request, $next)
{
$this->configureSecureCookieSessions();

return (new Pipeline(app()))->send($request)->through(
static::fromFrontend($request) ? $this->frontendMiddleware() : []
)->then(function ($request) use ($next) {
return $next($request);
});
$middleware = FrontendRequestChecker::isFromFrontend($request)
? $this->frontendMiddleware()
: [];

return (new Pipeline(app()))
->send($request)
->through($middleware)
->then(function ($request) use ($next) {
return $next($request);
});
}

/**
Expand Down Expand Up @@ -63,31 +65,4 @@ protected function frontendMiddleware()

return $middleware;
}

/**
* Determine if the given request is from the first-party application frontend.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
public static function fromFrontend($request)
{
$domain = $request->headers->get('referer') ?: $request->headers->get('origin');

if (is_null($domain)) {
return false;
}

$domain = Str::replaceFirst('https://', '', $domain);
$domain = Str::replaceFirst('http://', '', $domain);
$domain = Str::endsWith($domain, '/') ? $domain : "{$domain}/";

$stateful = array_filter(config('sanctum.stateful', []));

return Str::is(Collection::make($stateful)->map(function ($uri) use ($request) {
$uri = $uri === Sanctum::$currentRequestHostPlaceholder ? $request->getHttpHost() : $uri;

return trim($uri).'/*';
})->all(), $domain);
}
}
69 changes: 69 additions & 0 deletions src/Http/Middleware/FrontendRequestChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace Laravel\Sanctum\Http\Middleware;

use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Laravel\Sanctum\Sanctum;

class FrontendRequestChecker
{
/**
* Determine if the given request is from the first-party application frontend.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
public static function isFromFrontend(Request $request): bool
{
$domain = self::getDomain($request);

if (is_null($domain)) {
return false;
}

$stateful = array_filter(config('sanctum.stateful', []));

$patterns = self::getStatefulDomainPatterns($stateful, $request);

return Str::is($patterns, $domain);
}

/**
* Get the domain from the request.
*
* @param \Illuminate\Http\Request $request
* @return string|null
*/
private static function getDomain(Request $request): ?string
{
$domain = $request->headers->get('referer') ?: $request->headers->get('origin');

if (is_null($domain)) {
return null;
}

$domain = Str::of($domain)->after('://')->finish('/');

return (string) $domain;
}

/**
* Get the stateful domain patterns.
*
* @param array $stateful
* @param \Illuminate\Http\Request $request
* @return array
*/
private static function getStatefulDomainPatterns(array $stateful, Request $request): array
{
return Collection::make($stateful)->map(function ($uri) use ($request) {
if ($uri === Sanctum::$currentRequestHostPlaceholder) {
return trim($request->getHttpHost()).'/*';
}

return trim($uri).'/*';
})->all();
}
}
4 changes: 2 additions & 2 deletions tests/Feature/DefaultConfigContainsAppUrlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Laravel\Sanctum\Tests\Feature;

use Illuminate\Http\Request;
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
use Laravel\Sanctum\Http\Middleware\FrontendRequestChecker;
use Orchestra\Testbench\Concerns\WithWorkbench;
use Orchestra\Testbench\TestCase;

Expand Down Expand Up @@ -52,6 +52,6 @@ public function test_request_from_app_url_is_stateful_with_default_config()

$request->headers->set('referer', config('app.url'));

$this->assertTrue(EnsureFrontendRequestsAreStateful::fromFrontend($request));
$this->assertTrue(FrontendRequestChecker::isFromFrontend($request));
}
}
24 changes: 12 additions & 12 deletions tests/Feature/EnsureFrontendRequestsAreStatefulTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Laravel\Sanctum\Tests\Feature;

use Illuminate\Http\Request;
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
use Laravel\Sanctum\Http\Middleware\FrontendRequestChecker;
use Laravel\Sanctum\Sanctum;
use Orchestra\Testbench\Concerns\WithWorkbench;
use Orchestra\Testbench\TestCase;
Expand All @@ -22,42 +22,42 @@ public function test_request_referer_is_parsed_against_configuration()
$request = Request::create('/');
$request->headers->set('referer', 'https://test.com');

$this->assertTrue(EnsureFrontendRequestsAreStateful::fromFrontend($request));
$this->assertTrue(FrontendRequestChecker::isFromFrontend($request));

$request = Request::create('/');
$request->headers->set('referer', 'https://wrong.com');

$this->assertFalse(EnsureFrontendRequestsAreStateful::fromFrontend($request));
$this->assertFalse(FrontendRequestChecker::isFromFrontend($request));

$request = Request::create('/');
$request->headers->set('referer', 'https://test.com.x');

$this->assertFalse(EnsureFrontendRequestsAreStateful::fromFrontend($request));
$this->assertFalse(FrontendRequestChecker::isFromFrontend($request));

$request = Request::create('/');
$request->headers->set('referer', 'https://foobar.test.com/');

$this->assertTrue(EnsureFrontendRequestsAreStateful::fromFrontend($request));
$this->assertTrue(FrontendRequestChecker::isFromFrontend($request));
}

public function test_request_origin_fallback()
{
$request = Request::create('/');
$request->headers->set('origin', 'test.com');

$this->assertTrue(EnsureFrontendRequestsAreStateful::fromFrontend($request));
$this->assertTrue(FrontendRequestChecker::isFromFrontend($request));

$request = Request::create('/');
$request->headers->set('referer', null);
$request->headers->set('origin', 'test.com');

$this->assertTrue(EnsureFrontendRequestsAreStateful::fromFrontend($request));
$this->assertTrue(FrontendRequestChecker::isFromFrontend($request));

$request = Request::create('/');
$request->headers->set('referer', '');
$request->headers->set('origin', 'test.com');

$this->assertTrue(EnsureFrontendRequestsAreStateful::fromFrontend($request));
$this->assertTrue(FrontendRequestChecker::isFromFrontend($request));
}

public function test_same_domain_stateful()
Expand All @@ -66,18 +66,18 @@ public function test_same_domain_stateful()
$request->headers->set('origin', 'app-domain.com');

config(['sanctum.stateful' => []]);
$this->assertFalse(EnsureFrontendRequestsAreStateful::fromFrontend($request));
$this->assertFalse(FrontendRequestChecker::isFromFrontend($request));

config(['sanctum.stateful' => [Sanctum::$currentRequestHostPlaceholder]]);
$this->assertTrue(EnsureFrontendRequestsAreStateful::fromFrontend($request));
$this->assertTrue(FrontendRequestChecker::isFromFrontend($request));
}

public function test_wildcard_matching()
{
$request = Request::create('/');
$request->headers->set('referer', 'https://foo.test.com');

$this->assertTrue(EnsureFrontendRequestsAreStateful::fromFrontend($request));
$this->assertTrue(FrontendRequestChecker::isFromFrontend($request));
}

public function test_requests_are_not_stateful_without_referer()
Expand All @@ -86,6 +86,6 @@ public function test_requests_are_not_stateful_without_referer()

$request = Request::create('/');

$this->assertFalse(EnsureFrontendRequestsAreStateful::fromFrontend($request));
$this->assertFalse(FrontendRequestChecker::isFromFrontend($request));
}
}