From e734e12ec14d8c8674329668743398dbd69c4803 Mon Sep 17 00:00:00 2001 From: Morris Jencen Chavez Date: Thu, 3 Sep 2026 19:25:03 +0000 Subject: [PATCH] fix: stop Secure session cookies from breaking login on plain-HTTP deployments config/session.php previously defaulted the 'secure' session cookie flag to true whenever APP_ENV=production, regardless of whether the app was actually served over HTTPS. The shipped docker-compose.yml stack and the quick-start docker run example in DEPLOYMENT.md both set APP_ENV=production but never terminate TLS, so browsers silently dropped the Secure-flagged session cookie and login appeared to just do nothing, with no error surfaced anywhere. Default 'secure' based on whether APP_URL is served over https:// instead, since that reflects the actual public-facing transport rather than the deployment environment name. SESSION_SECURE_COOKIE remains an explicit override for anyone who needs to force the behavior either way. Also documents the new default near the affected DEPLOYMENT.md example so operators know to set APP_URL=https://... once TLS is terminated in front of the app. Fixes #120 --- DEPLOYMENT.md | 8 +++ config/session.php | 9 +++- tests/Unit/SessionSecureCookieConfigTest.php | 55 +++++++++++++++----- 3 files changed, 57 insertions(+), 15 deletions(-) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 4002057..39f00d8 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -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** diff --git a/config/session.php b/config/session.php index c787c61..56f18bc 100644 --- a/config/session.php +++ b/config/session.php @@ -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://')), /* |-------------------------------------------------------------------------- diff --git a/tests/Unit/SessionSecureCookieConfigTest.php b/tests/Unit/SessionSecureCookieConfigTest.php index 421baf2..5924d8a 100644 --- a/tests/Unit/SessionSecureCookieConfigTest.php +++ b/tests/Unit/SessionSecureCookieConfigTest.php @@ -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']; }