From 24a5e63709a99cd71fe3c165d8d1493394b78546 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Mon, 15 Dec 2025 04:51:50 +0000 Subject: [PATCH 1/3] Add extensibility methods to Session and Sanctum middleware for multi-tenant applications --- .../EnsureFrontendRequestsAreStateful.php | 16 +++++++- src/session/src/Middleware/StartSession.php | 14 ++++++- .../EnsureFrontendRequestsAreStatefulTest.php | 37 +++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php b/src/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php index 68a4316f3..013c5eee5 100644 --- a/src/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php +++ b/src/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php @@ -64,13 +64,27 @@ 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. + * + * Override this method to dynamically determine stateful domains, + * for example in multi-tenant applications where each tenant has + * a different domain. + * + * @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..9dc835e40 100644 --- a/src/session/src/Middleware/StartSession.php +++ b/src/session/src/Middleware/StartSession.php @@ -189,7 +189,7 @@ protected function addCookieToResponse(ResponseInterface $response, Session $ses $session->getId(), $this->getCookieExpirationDate(), $config['path'] ?? '/', - $config['domain'] ?? '', + $this->getSessionCookieDomain($config), $config['secure'] ?? false, $config['http_only'] ?? true, false, @@ -201,6 +201,18 @@ protected function addCookieToResponse(ResponseInterface $response, Session $ses return $response->withCookie($cookie); } + /** + * Get the session cookie domain. + * + * Override this method to dynamically set the session cookie domain, + * for example in multi-tenant applications where each tenant has + * a different domain. + */ + protected function getSessionCookieDomain(array $config): string + { + return $config['domain'] ?? ''; + } + /** * Save the session data to storage. */ diff --git a/tests/Sanctum/EnsureFrontendRequestsAreStatefulTest.php b/tests/Sanctum/EnsureFrontendRequestsAreStatefulTest.php index 87638c40c..628db7f19 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-tenant.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-tenant.example.com']; + } } From 1ed38d54c306cf24dc7f8ea416e98038f67825ca Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 21 Dec 2025 05:14:56 +0000 Subject: [PATCH 2/3] Remove tenant-specific language from docblocks and tests --- .../src/Http/Middleware/EnsureFrontendRequestsAreStateful.php | 4 ---- src/session/src/Middleware/StartSession.php | 4 ---- tests/Sanctum/EnsureFrontendRequestsAreStatefulTest.php | 4 ++-- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php b/src/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php index 013c5eee5..0f32de865 100644 --- a/src/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php +++ b/src/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php @@ -74,10 +74,6 @@ public static function fromFrontend(RequestInterface $request): bool /** * Get the domains that should be treated as stateful. * - * Override this method to dynamically determine stateful domains, - * for example in multi-tenant applications where each tenant has - * a different domain. - * * @return array */ public static function statefulDomains(): array diff --git a/src/session/src/Middleware/StartSession.php b/src/session/src/Middleware/StartSession.php index 9dc835e40..cd00763f0 100644 --- a/src/session/src/Middleware/StartSession.php +++ b/src/session/src/Middleware/StartSession.php @@ -203,10 +203,6 @@ protected function addCookieToResponse(ResponseInterface $response, Session $ses /** * Get the session cookie domain. - * - * Override this method to dynamically set the session cookie domain, - * for example in multi-tenant applications where each tenant has - * a different domain. */ protected function getSessionCookieDomain(array $config): string { diff --git a/tests/Sanctum/EnsureFrontendRequestsAreStatefulTest.php b/tests/Sanctum/EnsureFrontendRequestsAreStatefulTest.php index 628db7f19..19dfcab4b 100644 --- a/tests/Sanctum/EnsureFrontendRequestsAreStatefulTest.php +++ b/tests/Sanctum/EnsureFrontendRequestsAreStatefulTest.php @@ -102,7 +102,7 @@ public function testStatefulDomainsCanBeOverridden(): void $request = Mockery::mock(RequestInterface::class); $request->shouldReceive('header') ->with('referer') - ->andReturn('https://custom-tenant.example.com'); + ->andReturn('https://custom.example.com'); $request->shouldReceive('header') ->with('origin') ->andReturn(null); @@ -122,6 +122,6 @@ class CustomStatefulMiddleware extends EnsureFrontendRequestsAreStateful { public static function statefulDomains(): array { - return ['custom-tenant.example.com']; + return ['custom.example.com']; } } From eab77420050f107a4bffa5a4496f074cb3b5e20f Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 21 Dec 2025 05:20:45 +0000 Subject: [PATCH 3/3] Use getSessionCookieConfig for full cookie configuration Replace getSessionCookieDomain with getSessionCookieConfig that returns all cookie settings, providing a single extension point for customization. --- src/session/src/Middleware/StartSession.php | 29 +++-- tests/Session/Middleware/StartSessionTest.php | 102 ++++++++++++++++++ 2 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 tests/Session/Middleware/StartSessionTest.php diff --git a/src/session/src/Middleware/StartSession.php b/src/session/src/Middleware/StartSession.php index cd00763f0..7a67a5674 100644 --- a/src/session/src/Middleware/StartSession.php +++ b/src/session/src/Middleware/StartSession.php @@ -184,17 +184,19 @@ protected function addCookieToResponse(ResponseInterface $response, Session $ses return $response; } + $cookieConfig = $this->getSessionCookieConfig($config); + $cookie = new Cookie( $session->getName(), $session->getId(), $this->getCookieExpirationDate(), - $config['path'] ?? '/', - $this->getSessionCookieDomain($config), - $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 */ @@ -202,11 +204,20 @@ protected function addCookieToResponse(ResponseInterface $response, Session $ses } /** - * Get the session cookie domain. + * Get the session cookie configuration. + * + * @return array{path: string, domain: string, secure: bool, http_only: bool, same_site: ?string, partitioned: bool} */ - protected function getSessionCookieDomain(array $config): string + protected function getSessionCookieConfig(array $config): array { - return $config['domain'] ?? ''; + 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, + ]; } /** 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; + } +}