Skip to content

Commit bc590c8

Browse files
committed
fix: pin remembered-idp cookie expiry to add() result
write() previously recomputed cookieExpiresAt() independently of the value returned by add(), risking a drift between the logged/returned expiry and the actual Set-Cookie expiry. write() now accepts an optional explicit expires so callers can pass the value already computed by add(). Also documents the Secure/SameSite=None coupling on write()/clear().
1 parent 6d2c48e commit bc590c8

3 files changed

Lines changed: 38 additions & 4 deletions

File tree

‎library/EngineBlock/Corto/Module/Service/ContinueToIdp.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ private function _persistRememberedChoice(ServiceProvider $sp, $idp, $rememberCh
153153
}
154154

155155
$result = $rememberedIdpCookie->add($entries, $sp->entityId, $idp->entityId);
156-
$rememberedIdpCookie->write($result['entries']);
156+
$rememberedIdpCookie->write($result['entries'], $result['expires']);
157157

158158
$log->info(sprintf(
159159
'WAYF-remember-my-choice set for SP %s and IdP %s and expiry %d',

‎src/OpenConext/EngineBlock/Service/Wayf/RememberedIdpCookie.php‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,13 +184,21 @@ public function cookieExpiresAt(): int
184184
return $this->timeProvider->time() + $this->lifetime;
185185
}
186186

187-
/** @param array<string, array{idp: string, expires: int}> $entries */
188-
public function write(array $entries): bool
187+
/**
188+
* @param array<string, array{idp: string, expires: int}> $entries
189+
* @param int|null $expires Pass the expiry already computed by add() so the logged/returned
190+
* value and the actual cookie expiry never drift apart. Defaults to a freshly computed
191+
* expiry (e.g. when merely refreshing the cookie's lifetime after normalization).
192+
*/
193+
public function write(array $entries, ?int $expires = null): bool
189194
{
195+
// SameSite=None requires the Secure attribute or browsers silently drop the cookie; this
196+
// is only safe because 'cookie.secure' defaults to (and, per SAML's HTTPS requirement,
197+
// should always remain) true.
190198
return $this->cookieService->setCookieWithSameSite(
191199
self::NAME,
192200
$this->encode($entries),
193-
$this->cookieExpiresAt(),
201+
$expires ?? $this->cookieExpiresAt(),
194202
$this->cookiePath,
195203
$this->cookieDomain,
196204
$this->secure,
@@ -201,6 +209,7 @@ public function write(array $entries): bool
201209

202210
public function clear(): bool
203211
{
212+
// See the note on write() about the Secure/SameSite=None coupling.
204213
return $this->cookieService->clearCookieWithSameSite(
205214
self::NAME,
206215
$this->cookiePath,

‎tests/unit/OpenConext/EngineBlock/Service/Wayf/RememberedIdpCookieTest.php‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,31 @@ public function testWriteAndClearUseSameSiteAwareCookieOperations(): void
250250
);
251251
}
252252

253+
public function testWriteUsesExplicitlyProvidedExpiryWhenGiven(): void
254+
{
255+
$cookieService = Phake::mock(CookieService::class);
256+
Phake::when($cookieService)->setCookieWithSameSite(Phake::anyParameters())->thenReturn(true);
257+
258+
$cookie = $this->buildCookie(cookieService: $cookieService);
259+
$entries = [
260+
'https://sp.example.org' => ['idp' => 'https://idp.example.org', 'expires' => self::NOW + self::LIFETIME],
261+
];
262+
$explicitExpiry = self::NOW + self::LIFETIME + 1234;
263+
264+
$this->assertTrue($cookie->write($entries, $explicitExpiry));
265+
266+
Phake::verify($cookieService)->setCookieWithSameSite(
267+
RememberedIdpCookie::NAME,
268+
$cookie->encode($entries),
269+
$explicitExpiry,
270+
self::COOKIE_PATH,
271+
self::COOKIE_DOMAIN,
272+
true,
273+
true,
274+
'None'
275+
);
276+
}
277+
253278
public function testLoadValidEntriesLogsWarningAndClearsCookieOnInvalidInput(): void
254279
{
255280
$cookieService = Phake::mock(CookieService::class);

0 commit comments

Comments
 (0)