Skip to content
Merged
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
37 changes: 37 additions & 0 deletions src/boost/docs/session.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [Regenerating the Session ID](#regenerating-the-session-id)
- [Session Cache](#session-cache)
- [Session Blocking](#session-blocking)
- [Configuring the Session Cookie](#configuring-the-session-cookie)
- [Adding Custom Session Drivers](#adding-custom-session-drivers)
- [Implementing the Driver](#implementing-the-driver)
- [Registering the Driver](#registering-the-driver)
Expand Down Expand Up @@ -329,6 +330,42 @@ Route::post('/profile', function () {
})->block();
```

<a name="configuring-the-session-cookie"></a>
## Configuring the Session Cookie

Session cookie attributes are normally configured using your application's `config/session.php` configuration file. If you need to determine session cookie attributes dynamically for each request, you may register a callback using the `configureSessionCookieUsing` method on the `StartSession` middleware.

This method should typically be called from the `boot` method of a [service provider](/docs/{{version}}/providers):

```php
<?php

namespace App\Providers;

use Hypervel\Http\Request;
use Hypervel\Session\Middleware\StartSession;
use Hypervel\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
StartSession::configureSessionCookieUsing(function (Request $request, array $cookie): array {
return array_replace($cookie, [
'domain' => tenant()->sessionCookieDomain(),
]);
});
}
}
```

The callback receives the current request and the session cookie configuration, and should return the modified cookie configuration. This is useful when cookie attributes, such as the cookie domain, depend on the current request. In this example, `tenant()` represents an application-specific helper that returns the current tenant.

If multiple callbacks are registered, they will be executed in the order they were registered. Each callback receives the cookie configuration returned by the previous callback.

<a name="adding-custom-session-drivers"></a>
## Adding Custom Session Drivers

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ class EnsureFrontendRequestsAreStateful
*/
public function handle(Request $request, Closure $next): Response
{
$this->configureSecureCookieSessions();

return (new Pipeline(app()))->send($request)->through(
static::fromFrontend($request) ? $this->frontendMiddleware() : []
)->then(function ($request) use ($next) {
Expand Down Expand Up @@ -85,15 +83,4 @@ public static function statefulDomains(): array
{
return config('sanctum.stateful', []);
}

/**
* Configure secure cookie sessions.
*/
protected function configureSecureCookieSessions(): void
{
config([
'session.http_only' => true,
'session.same_site' => 'lax',
]);
}
}
10 changes: 10 additions & 0 deletions src/sanctum/src/SanctumGuard.php
Original file line number Diff line number Diff line change
Expand Up @@ -275,4 +275,14 @@ protected function getContextKeyForToken(?string $token): string

return "__auth.guards.{$this->name}.user." . md5($token);
}

/**
* Flush all static state back to defaults.
*
* Boot or tests only. Resets registered macros on the guard class.
*/
public static function flushState(): void
{
static::flushMacros();
}
}
21 changes: 21 additions & 0 deletions src/sanctum/src/SanctumServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
namespace Hypervel\Sanctum;

use Hypervel\Auth\AuthManager;
use Hypervel\Http\Request;
use Hypervel\Sanctum\Console\Commands\PruneExpired;
use Hypervel\Session\Middleware\StartSession;
use Hypervel\Support\Facades\Route;
use Hypervel\Support\ServiceProvider;

Expand All @@ -28,6 +30,8 @@ public function register(): void
public function boot(): void
{
$this->registerSanctumGuard();
$this->configureSessionCookies();

if ($this->app->runningInConsole()) {
$this->registerPublishing();
$this->registerCommands();
Expand All @@ -54,6 +58,23 @@ protected function registerSanctumGuard(): void
});
}

/**
* Configure session cookies for stateful frontend requests.
*/
protected function configureSessionCookies(): void
{
StartSession::configureSessionCookieUsing(function (Request $request, array $cookie): array {
if (! $request->attributes->get('sanctum')) {
return $cookie;
}

return array_replace($cookie, [
'http_only' => true,
'same_site' => 'lax',
]);
});
}

/**
* Register the package routes.
*/
Expand Down
51 changes: 43 additions & 8 deletions src/session/src/Middleware/StartSession.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@

class StartSession
{
/**
* The callbacks used to configure the session cookie attributes.
*
* @var array<int, Closure>
*/
protected static array $sessionCookieCallbacks = [];

/**
* Create a new session middleware.
*/
Expand Down Expand Up @@ -106,7 +113,7 @@ protected function handleStatefulRequest(Request $request, Session $session, Clo

$this->storeCurrentUrl($request, $session);

$this->addCookieToResponse($response, $session);
$this->addCookieToResponse($request, $response, $session);

// Again, if the session has been configured we will need to close out the session
// so that the attributes may be persisted to some storage medium. We will also
Expand Down Expand Up @@ -189,10 +196,10 @@ protected function storeCurrentUrl(Request $request, Session $session): void
/**
* Add the session cookie to the application response.
*/
protected function addCookieToResponse(Response $response, Session $session): void
protected function addCookieToResponse(Request $request, Response $response, Session $session): void
{
if ($this->sessionIsPersistent($config = $this->manager->getSessionConfig())) {
$cookieConfig = $this->getSessionCookieConfig($config);
$cookieConfig = $this->resolveSessionCookieConfig($request, $config);

$response->headers->setCookie(new Cookie(
$session->getName(),
Expand All @@ -212,21 +219,39 @@ protected function addCookieToResponse(Response $response, Session $session): vo
/**
* Get the session cookie configuration.
*
* Extracted as an extension point so subclasses can provide dynamic
* cookie settings without duplicating the rest of addCookieToResponse.
*
* @return array{path: string, domain: string, secure: ?bool, http_only: bool, same_site: ?string, partitioned: bool}
*/
protected function getSessionCookieConfig(array $config): array
protected function resolveSessionCookieConfig(Request $request, array $config): array
{
return [
$cookieConfig = [
'path' => $config['path'] ?? '/',
'domain' => $config['domain'] ?? '',
'secure' => $config['secure'] ?? null,
'http_only' => $config['http_only'] ?? true,
'same_site' => $config['same_site'] ?? null,
'partitioned' => $config['partitioned'] ?? false,
];

foreach (static::$sessionCookieCallbacks as $callback) {
$cookieConfig = $callback($request, $cookieConfig);
}

return $cookieConfig;
}

/**
* Register a callback to configure the session cookie attributes.
*
* Boot-only. The callback persists in a static property for the worker
* lifetime and runs on every session cookie write across all coroutines.
* Callbacks run in registration order; later callbacks receive the values
* returned by earlier callbacks and may overwrite them.
*
* @param Closure(Request, array): array $callback
*/
public static function configureSessionCookieUsing(Closure $callback): void
{
static::$sessionCookieCallbacks[] = $callback;
}

/**
Expand Down Expand Up @@ -276,4 +301,14 @@ protected function sessionIsPersistent(?array $config = null): bool

return ! is_null($config['driver'] ?? null);
}

/**
* Flush all static state back to defaults.
*
* Boot or tests only. Resets the registered cookie configuration callbacks.
*/
public static function flushState(): void
{
static::$sessionCookieCallbacks = [];
}
}
2 changes: 2 additions & 0 deletions tests/AfterEachTestSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,14 @@ public function notify(Finished $event): void
\Hypervel\Routing\SortedMiddleware::flushCache();
\Hypervel\Routing\UrlGenerator::flushState();
\Hypervel\Sanctum\Sanctum::flushState();
\Hypervel\Sanctum\SanctumGuard::flushState();
\Hypervel\Scout\Builder::flushState();
\Hypervel\Scout\Scout::flushState();
\Hypervel\Server\ServerManager::flushState();
\Hypervel\ServerProcess\ProcessCollector::flushState();
\Hypervel\ServerProcess\ProcessManager::flushState();
\Hypervel\Session\Middleware\AuthenticateSession::flushState();
\Hypervel\Session\Middleware\StartSession::flushState();
\Hypervel\Session\Store::flushState();
\Hypervel\Support\Arr::flushState();
\Hypervel\Support\BinaryCodec::flushState();
Expand Down
30 changes: 15 additions & 15 deletions tests/Routing/RoutingStaticStateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
use Hypervel\Routing\ResponseFactory;
use Hypervel\Routing\Route;
use Hypervel\Routing\RouteCollection;
use Hypervel\Routing\RouteRegistrar;
use Hypervel\Routing\Router;
use Hypervel\Routing\RouteRegistrar;
use Hypervel\Routing\UrlGenerator;
use Hypervel\Tests\Routing\RoutingTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
Expand All @@ -36,12 +36,24 @@ public function testFlushStateClearsRoutingMacros(string $class): void
$this->assertFalse($class::hasMacro('routingFlushProbe'));
}

public static function macroableRoutingClasses(): array
{
return [
[PendingResourceRegistration::class],
[PendingSingletonResourceRegistration::class],
[Redirector::class],
[ResponseFactory::class],
[RouteRegistrar::class],
[Router::class],
];
}

public function testRouteFlushStateClearsEnumCache(): void
{
$enumCache = new ReflectionProperty(Route::class, 'enumCache');
$enumCache->setValue(null, ['Some\\Enum' => true]);
$enumCache->setValue(null, ['Some\Enum' => true]);

$this->assertSame(['Some\\Enum' => true], $enumCache->getValue());
$this->assertSame(['Some\Enum' => true], $enumCache->getValue());

Route::flushState();

Expand Down Expand Up @@ -79,16 +91,4 @@ public function testThrottleRequestsFlushStateRestoresHashedKeys(): void

$this->assertTrue($shouldHashKeys->getValue());
}

public static function macroableRoutingClasses(): array
{
return [
[PendingResourceRegistration::class],
[PendingSingletonResourceRegistration::class],
[Redirector::class],
[ResponseFactory::class],
[RouteRegistrar::class],
[Router::class],
];
}
}
16 changes: 16 additions & 0 deletions tests/Sanctum/EnsureFrontendRequestsAreStatefulTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Hypervel\Tests\Sanctum;

use Hypervel\Http\Request;
use Hypervel\Http\Response;
use Hypervel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
use Hypervel\Testbench\TestCase;

Expand Down Expand Up @@ -71,6 +72,21 @@ public function testStatefulDomainsCanBeOverridden(): void
// Custom middleware with overridden statefulDomains SHOULD match
$this->assertTrue(CustomStatefulMiddleware::fromFrontend($request));
}

public function testMiddlewareDoesNotMutateSessionConfig(): void
{
$this->app->make('config')->set([
'session.http_only' => false,
'session.same_site' => 'strict',
]);

$request = Request::create('http://localhost', server: ['HTTP_ORIGIN' => 'https://wrong.com']);

(new EnsureFrontendRequestsAreStateful)->handle($request, fn () => new Response('ok'));

$this->assertFalse($this->app->make('config')->get('session.http_only'));
$this->assertSame('strict', $this->app->make('config')->get('session.same_site'));
}
}

/**
Expand Down
Loading