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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Comment thread
klsoft-web marked this conversation as resolved.

> [!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

Expand Down
1 change: 1 addition & 0 deletions config/di-web.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
new DateInterval($params['yiisoft/user']['cookieLogin']['duration']) :
null,
'signatureKey' => $params['yiisoft/user']['cookieLogin']['signatureKey'],
'secureCookie' => $params['yiisoft/user']['cookieLogin']['secureCookie'],
],
],
];
1 change: 1 addition & 0 deletions config/params.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
],
],
];
6 changes: 4 additions & 2 deletions src/Login/Cookie/CookieLogin.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@
* 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,
) {}

/**
Expand All @@ -63,7 +65,7 @@
*/
public function withCookieName(string $name): self
{
$new = clone $this;

Check warning on line 68 in src/Login/Cookie/CookieLogin.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "CloneRemoval": @@ @@ */ public function withCookieName(string $name): self { - $new = clone $this; + $new = $this; $new->cookieName = $name; return $new; }
$new->cookieName = $name;
return $new;
}
Expand Down Expand Up @@ -93,7 +95,7 @@

$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);
}

Expand All @@ -106,7 +108,7 @@
*/
public function expireCookie(ResponseInterface $response): ResponseInterface
{
return (new Cookie($this->cookieName))
return (new Cookie($this->cookieName, secure: $this->secureCookie))
->expire()
->addToResponse($response);
}
Expand Down Expand Up @@ -140,7 +142,7 @@
}

try {
$data = json_decode($payload, true, 512, JSON_THROW_ON_ERROR);

Check warning on line 145 in src/Login/Cookie/CookieLogin.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "IncrementInteger": @@ @@ } try { - $data = json_decode($payload, true, 512, JSON_THROW_ON_ERROR); + $data = json_decode($payload, true, 513, JSON_THROW_ON_ERROR); } catch (Throwable) { return null; }

Check warning on line 145 in src/Login/Cookie/CookieLogin.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "DecrementInteger": @@ @@ } try { - $data = json_decode($payload, true, 512, JSON_THROW_ON_ERROR); + $data = json_decode($payload, true, 511, JSON_THROW_ON_ERROR); } catch (Throwable) { return null; }
} catch (Throwable) {
return null;
}
Expand Down Expand Up @@ -174,7 +176,7 @@
{
$payload = json_encode(
[$id, $key, $expiresDate?->getTimestamp() ?? 0],
JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,

Check warning on line 179 in src/Login/Cookie/CookieLogin.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "BitwiseOr": @@ @@ { $payload = json_encode( [$id, $key, $expiresDate?->getTimestamp() ?? 0], - JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, + JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES & JSON_UNESCAPED_UNICODE, ); return $this->signatureKey === null ? $payload : $this->sign($payload, $this->signatureKey);
);

return $this->signatureKey === null ? $payload : $this->sign($payload, $this->signatureKey);
Expand Down Expand Up @@ -204,7 +206,7 @@
private function getVerifiedPayload(string $value, string $signatureKey): ?string
{
if (strlen($value) <= self::SIGNATURE_LENGTH || $value[self::SIGNATURE_LENGTH] !== '.') {
return null;

Check warning on line 209 in src/Login/Cookie/CookieLogin.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "ReturnRemoval": @@ @@ private function getVerifiedPayload(string $value, string $signatureKey): ?string { if (strlen($value) <= self::SIGNATURE_LENGTH || $value[self::SIGNATURE_LENGTH] !== '.') { - return null; + } $signature = substr($value, 0, self::SIGNATURE_LENGTH);
}

$signature = substr($value, 0, self::SIGNATURE_LENGTH);
Expand Down
3 changes: 3 additions & 0 deletions tests/ConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -68,6 +69,7 @@ public function testOverrideParams(): void
'forceAddCookie' => true,
'duration' => 'P2D',
'signatureKey' => 'test-signature-key',
'secureCookie' => false
],
],
]);
Expand All @@ -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);

Expand Down
16 changes: 16 additions & 0 deletions tests/Login/Cookie/CookieLoginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
Loading