diff --git a/CHANGELOG.md b/CHANGELOG.md index 986f8c5..bd55e62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Enh #127: Bump `yiisoft/auth` version to `^3.3.0`, and fix deprecated classes usage (@klsoft-web, @vjik) - Enh #132: Bump `yiisoft/session` version to `^3.0.2` (@vjik) - New #126: Add optional HMAC signing of the auto-login cookie value via `CookieLogin` signature key (@vjik) +- Enh #135: Add the `secureCookie` parameter to enable `CookieLogin` to set a cookie when an HTTP request is made (@klsoft-web) ## 2.3.2 December 23, 2025 diff --git a/README.md b/README.md index fec4ac5..cdcb6f9 100644 --- a/README.md +++ b/README.md @@ -349,12 +349,17 @@ return [ 'cookieLogin' => [ 'forceAddCookie' => true, 'duration' => 'P5D', // 5 days + 'secureCookie' => true, // whether the client should send back the cookie only over HTTPS connection ], ], ]; ``` > If you want the cookie to be a session cookie, change the duration to `null`. +> To enable a cookie to be set when an HTTP request is made, change the `secureCookie` to `false`. + +> [!warning] +> With secureCookie turned off, it travels in cleartext and can be captured and replayed for the full duration of cookie lifetime (5 days by default). #### Removing a cookie diff --git a/config/di-web.php b/config/di-web.php index 2b666f8..f7659ef 100644 --- a/config/di-web.php +++ b/config/di-web.php @@ -44,6 +44,7 @@ new DateInterval($params['yiisoft/user']['cookieLogin']['duration']) : null, 'signatureKey' => $params['yiisoft/user']['cookieLogin']['signatureKey'], + 'secureCookie' => $params['yiisoft/user']['cookieLogin']['secureCookie'], ], ], ]; diff --git a/config/params.php b/config/params.php index 8d8b9c9..03c658c 100644 --- a/config/params.php +++ b/config/params.php @@ -9,6 +9,7 @@ 'forceAddCookie' => false, 'duration' => 'P5D', // 5 days, see format on https://www.php.net/manual/dateinterval.construct.php 'signatureKey' => null, // secret key to sign the auto-login cookie value; keep `null` to store it unsigned + 'secureCookie' => true, // whether the client should send back the cookie only over HTTPS connection ], ], ]; diff --git a/src/Login/Cookie/CookieLogin.php b/src/Login/Cookie/CookieLogin.php index 87abc55..4a27590 100644 --- a/src/Login/Cookie/CookieLogin.php +++ b/src/Login/Cookie/CookieLogin.php @@ -50,10 +50,12 @@ final class CookieLogin * the auto-login cookie is session cookie that expires when browser is closed. * @param string|null $signatureKey Secret key used to sign the auto-login cookie value with HMAC-SHA256. If it * isn't set, the cookie value is stored without a signature and isn't protected against tampering. + * @param bool $secureCookie Whether the client should send back the cookie only over HTTPS connection. */ public function __construct( private readonly ?DateInterval $duration = null, private readonly ?string $signatureKey = null, + private readonly bool $secureCookie = true, ) {} /** @@ -93,7 +95,7 @@ public function addCookie( $cookieValue = $this->createValue((string) $identity->getId(), $identity->getCookieLoginKey(), $expires); - return (new Cookie(name: $this->cookieName, value: $cookieValue, expires: $expires)) + return (new Cookie(name: $this->cookieName, value: $cookieValue, expires: $expires, secure: $this->secureCookie)) ->addToResponse($response); } @@ -106,7 +108,7 @@ public function addCookie( */ public function expireCookie(ResponseInterface $response): ResponseInterface { - return (new Cookie($this->cookieName)) + return (new Cookie($this->cookieName, secure: $this->secureCookie)) ->expire() ->addToResponse($response); } diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index 5f926ff..17c3ca5 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -52,6 +52,7 @@ public function testBase(): void ->getInaccessibleProperty($cookieLogin, 'duration') ->d); $this->assertNull($this->getInaccessibleProperty($cookieLogin, 'signatureKey')); + $this->assertTrue($this->getInaccessibleProperty($cookieLogin, 'secureCookie')); $cookieLoginMiddleware = $container->get(CookieLoginMiddleware::class); @@ -68,6 +69,7 @@ public function testOverrideParams(): void 'forceAddCookie' => true, 'duration' => 'P2D', 'signatureKey' => 'test-signature-key', + 'secureCookie' => false ], ], ]); @@ -86,6 +88,7 @@ public function testOverrideParams(): void ->getInaccessibleProperty($cookieLogin, 'duration') ->d); $this->assertSame('test-signature-key', $this->getInaccessibleProperty($cookieLogin, 'signatureKey')); + $this->assertFalse($this->getInaccessibleProperty($cookieLogin, 'secureCookie')); $cookieLoginMiddleware = $container->get(CookieLoginMiddleware::class); diff --git a/tests/Login/Cookie/CookieLoginTest.php b/tests/Login/Cookie/CookieLoginTest.php index abc5c86..6bb6c09 100644 --- a/tests/Login/Cookie/CookieLoginTest.php +++ b/tests/Login/Cookie/CookieLoginTest.php @@ -238,6 +238,22 @@ public function testParseValueInvalidUnsigned(string $value): void $this->assertNull($cookieLogin->parseValue($value)); } + public function testAddCookieWithDisableSecureCookie(): void + { + $cookieLogin = new CookieLogin(secureCookie: false); + + $identity = new CookieLoginIdentity(); + + $response = new Response(); + $response = $cookieLogin->addCookie($identity, $response); + + $this->assertMatchesRegularExpression( + '#autoLogin=%5B%2242%22%2C%22auto-login-key-correct%22%2C0%5D;' + . ' Path=/; HttpOnly; SameSite=Lax#', + $response->getHeaderLine('Set-Cookie'), + ); + } + private function extractCookieValue(string $setCookieHeader): string { $pair = explode(';', $setCookieHeader, 2)[0];