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
8 changes: 8 additions & 0 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ docker run -d \
webhook-platform:latest
```

> **Note:** Session cookies get the `Secure` flag automatically whenever
> `APP_URL` starts with `https://` (not merely because `APP_ENV=production`).
> If this container sits behind a TLS-terminating reverse proxy/load
> balancer, set `APP_URL=https://your-domain.com` so the cookie flag matches
> reality. Leave `APP_URL` on `http://` (the default) only if you're
> genuinely serving plain HTTP — otherwise browsers will silently drop the
> session cookie and login will appear to do nothing.

## ☁️ Cloud Provider Specific Deployments

### 1. **DigitalOcean App Platform**
Expand Down
9 changes: 8 additions & 1 deletion config/session.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,16 @@
| to the server if the browser has a HTTPS connection. This will keep
| the cookie from being sent to you when it can't be done securely.
|
| Defaults to whether APP_URL is served over https:// rather than to
| APP_ENV, since APP_ENV=production says nothing about whether a TLS
| terminator actually sits in front of this app. Keying off APP_ENV
| alone previously caused Secure-flagged cookies to be issued for
| plain-HTTP production deployments (e.g. the shipped docker-compose.yml
| stack), which browsers silently discard, making login appear broken.
|
*/

'secure' => env('SESSION_SECURE_COOKIE', env('APP_ENV', 'production') === 'production'),
'secure' => env('SESSION_SECURE_COOKIE', str_starts_with(env('APP_URL', 'http://localhost'), 'https://')),

/*
|--------------------------------------------------------------------------
Expand Down
55 changes: 41 additions & 14 deletions tests/Unit/SessionSecureCookieConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,67 @@

class SessionSecureCookieConfigTest extends TestCase
{
public function test_secure_cookie_defaults_to_true_in_production(): void
public function test_secure_cookie_defaults_to_true_when_app_url_is_https(): void
{
$this->assertTrue($this->resolveSecureCookieConfig('production', null));
$this->assertTrue($this->resolveSecureCookieConfig('https://example.com', null));
}

public function test_secure_cookie_defaults_to_false_outside_production(): void
public function test_secure_cookie_defaults_to_false_when_app_url_is_http(): void
{
$this->assertFalse($this->resolveSecureCookieConfig('local', null));
$this->assertFalse($this->resolveSecureCookieConfig('testing', null));
$this->assertFalse($this->resolveSecureCookieConfig('http://example.com', null));
$this->assertFalse($this->resolveSecureCookieConfig('http://localhost', null));
}

public function test_explicit_env_value_overrides_the_environment_based_default(): void
/**
* Regression test for #120: the shipped docker-compose.yml stack (and the
* quick-start `docker run` example in DEPLOYMENT.md) set APP_ENV=production
* but never terminate TLS, so APP_URL stays http://. Secure cookies must
* not be forced on in that case, or browsers silently drop the session
* cookie and login appears to do nothing.
*/
public function test_secure_cookie_defaults_to_false_in_production_without_tls(): void
{
$this->assertFalse($this->resolveSecureCookieConfig('http://localhost', null, appEnv: 'production'));
}

public function test_secure_cookie_defaults_to_true_in_production_behind_tls(): void
{
$this->assertTrue($this->resolveSecureCookieConfig('https://your-domain.com', null, appEnv: 'production'));
}

public function test_explicit_env_value_overrides_the_app_url_based_default(): void
{
$this->assertFalse($this->resolveSecureCookieConfig('production', 'false'));
$this->assertTrue($this->resolveSecureCookieConfig('local', 'true'));
$this->assertFalse($this->resolveSecureCookieConfig('https://example.com', 'false'));
$this->assertTrue($this->resolveSecureCookieConfig('http://example.com', 'true'));
}

/**
* Evaluate config/session.php's 'secure' entry under a given APP_ENV /
* SESSION_SECURE_COOKIE combination, restoring the original values afterward.
* Evaluate config/session.php's 'secure' entry under a given APP_URL /
* SESSION_SECURE_COOKIE (and optionally APP_ENV) combination, restoring
* the original values afterward.
*/
private function resolveSecureCookieConfig(string $appEnv, ?string $secureCookieEnv): mixed
private function resolveSecureCookieConfig(string $appUrl, ?string $secureCookieEnv, ?string $appEnv = null): mixed
{
$originalAppEnv = getenv('APP_ENV');
$originalAppUrl = getenv('APP_URL');
$originalSecureCookieEnv = getenv('SESSION_SECURE_COOKIE');
$originalAppEnv = getenv('APP_ENV');

$this->putOrClearEnv('APP_ENV', $appEnv);
$this->putOrClearEnv('APP_URL', $appUrl);
$this->putOrClearEnv('SESSION_SECURE_COOKIE', $secureCookieEnv);

if ($appEnv !== null) {
$this->putOrClearEnv('APP_ENV', $appEnv);
}

$config = require base_path('config/session.php');

$this->putOrClearEnv('APP_ENV', $originalAppEnv === false ? null : $originalAppEnv);
$this->putOrClearEnv('APP_URL', $originalAppUrl === false ? null : $originalAppUrl);
$this->putOrClearEnv('SESSION_SECURE_COOKIE', $originalSecureCookieEnv === false ? null : $originalSecureCookieEnv);

if ($appEnv !== null) {
$this->putOrClearEnv('APP_ENV', $originalAppEnv === false ? null : $originalAppEnv);
}

return $config['secure'];
}

Expand Down
Loading