diff --git a/src/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php b/src/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php index 68a4316f3..0f32de865 100644 --- a/src/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php +++ b/src/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php @@ -64,13 +64,23 @@ public static function fromFrontend(RequestInterface $request): bool $domain = Str::replaceFirst('http://', '', $domain); $domain = Str::endsWith($domain, '/') ? $domain : "{$domain}/"; - $stateful = array_filter(config('sanctum.stateful', [])); + $stateful = array_filter(static::statefulDomains()); return Str::is(Collection::make($stateful)->map(function ($uri) { return trim($uri) . '/*'; })->all(), $domain); } + /** + * Get the domains that should be treated as stateful. + * + * @return array + */ + public static function statefulDomains(): array + { + return config('sanctum.stateful', []); + } + /** * Configure secure cookie sessions. */ diff --git a/src/session/src/Middleware/StartSession.php b/src/session/src/Middleware/StartSession.php index e6e7b37ea..7a67a5674 100644 --- a/src/session/src/Middleware/StartSession.php +++ b/src/session/src/Middleware/StartSession.php @@ -184,23 +184,42 @@ protected function addCookieToResponse(ResponseInterface $response, Session $ses return $response; } + $cookieConfig = $this->getSessionCookieConfig($config); + $cookie = new Cookie( $session->getName(), $session->getId(), $this->getCookieExpirationDate(), - $config['path'] ?? '/', - $config['domain'] ?? '', - $config['secure'] ?? false, - $config['http_only'] ?? true, + $cookieConfig['path'], + $cookieConfig['domain'], + $cookieConfig['secure'], + $cookieConfig['http_only'], false, - $config['same_site'] ?? null, - $config['partitioned'] ?? false + $cookieConfig['same_site'], + $cookieConfig['partitioned'] ); /** @var \Hyperf\HttpMessage\Server\Response $response */ return $response->withCookie($cookie); } + /** + * Get the session cookie configuration. + * + * @return array{path: string, domain: string, secure: bool, http_only: bool, same_site: ?string, partitioned: bool} + */ + protected function getSessionCookieConfig(array $config): array + { + return [ + 'path' => $config['path'] ?? '/', + 'domain' => $config['domain'] ?? '', + 'secure' => $config['secure'] ?? false, + 'http_only' => $config['http_only'] ?? true, + 'same_site' => $config['same_site'] ?? null, + 'partitioned' => $config['partitioned'] ?? false, + ]; + } + /** * Save the session data to storage. */ diff --git a/tests/Sanctum/EnsureFrontendRequestsAreStatefulTest.php b/tests/Sanctum/EnsureFrontendRequestsAreStatefulTest.php index 87638c40c..19dfcab4b 100644 --- a/tests/Sanctum/EnsureFrontendRequestsAreStatefulTest.php +++ b/tests/Sanctum/EnsureFrontendRequestsAreStatefulTest.php @@ -87,4 +87,41 @@ public function testRequestsWithoutRefererOrOrigin(): void $this->assertFalse(EnsureFrontendRequestsAreStateful::fromFrontend($request)); } + + public function testStatefulDomainsReturnsConfiguredDomains(): void + { + $domains = EnsureFrontendRequestsAreStateful::statefulDomains(); + + $this->assertIsArray($domains); + $this->assertContains('test.com', $domains); + $this->assertContains('*.test.com', $domains); + } + + public function testStatefulDomainsCanBeOverridden(): void + { + $request = Mockery::mock(RequestInterface::class); + $request->shouldReceive('header') + ->with('referer') + ->andReturn('https://custom.example.com'); + $request->shouldReceive('header') + ->with('origin') + ->andReturn(null); + + // Default middleware should NOT match custom domain + $this->assertFalse(EnsureFrontendRequestsAreStateful::fromFrontend($request)); + + // Custom middleware with overridden statefulDomains SHOULD match + $this->assertTrue(CustomStatefulMiddleware::fromFrontend($request)); + } +} + +/** + * Custom middleware for testing statefulDomains override. + */ +class CustomStatefulMiddleware extends EnsureFrontendRequestsAreStateful +{ + public static function statefulDomains(): array + { + return ['custom.example.com']; + } } diff --git a/tests/Session/Middleware/StartSessionTest.php b/tests/Session/Middleware/StartSessionTest.php new file mode 100644 index 000000000..0650ad57e --- /dev/null +++ b/tests/Session/Middleware/StartSessionTest.php @@ -0,0 +1,102 @@ +createStartSessionMock(); + + $config = $this->invokeGetSessionCookieConfig($middleware, []); + + $this->assertSame('/', $config['path']); + $this->assertSame('', $config['domain']); + $this->assertFalse($config['secure']); + $this->assertTrue($config['http_only']); + $this->assertNull($config['same_site']); + $this->assertFalse($config['partitioned']); + } + + public function testGetSessionCookieConfigReturnsConfiguredValues(): void + { + $middleware = $this->createStartSessionMock(); + + $config = $this->invokeGetSessionCookieConfig($middleware, [ + 'path' => '/app', + 'domain' => '.example.com', + 'secure' => true, + 'http_only' => false, + 'same_site' => 'strict', + 'partitioned' => true, + ]); + + $this->assertSame('/app', $config['path']); + $this->assertSame('.example.com', $config['domain']); + $this->assertTrue($config['secure']); + $this->assertFalse($config['http_only']); + $this->assertSame('strict', $config['same_site']); + $this->assertTrue($config['partitioned']); + } + + public function testGetSessionCookieConfigCanBeOverridden(): void + { + $middleware = new CustomStartSession(); + + $config = $this->invokeGetSessionCookieConfig($middleware, [ + 'path' => '/', + 'domain' => '.example.com', + ]); + + // Custom middleware overrides domain + $this->assertSame('.custom.example.com', $config['domain']); + // Other values come from config + $this->assertSame('/', $config['path']); + } + + private function createStartSessionMock(): StartSession + { + return $this->getMockBuilder(StartSession::class) + ->disableOriginalConstructor() + ->getMock(); + } + + private function invokeGetSessionCookieConfig(StartSession $middleware, array $config): array + { + $method = new ReflectionMethod($middleware, 'getSessionCookieConfig'); + $method->setAccessible(true); + + return $method->invoke($middleware, $config); + } +} + +/** + * Custom middleware for testing getSessionCookieConfig override. + */ +class CustomStartSession extends StartSession +{ + public function __construct() + { + // Skip parent constructor for testing + } + + protected function getSessionCookieConfig(array $config): array + { + $cookieConfig = parent::getSessionCookieConfig($config); + + // Override domain + $cookieConfig['domain'] = '.custom.example.com'; + + return $cookieConfig; + } +}