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']; }