|
|
@@ -84,6 +84,46 @@ final class OidcFlowTest extends AppTestCase
|
|
|
self::assertSame('error', $flash[0]['type']);
|
|
|
}
|
|
|
|
|
|
+ public function testInitiateRotatesSessionIdBeforeAuthenticate(): void
|
|
|
+ {
|
|
|
+ // SEC_REVIEW F9: the session id must be rotated at the top of
|
|
|
+ // /login/oidc, BEFORE the authenticator stashes state/nonce/PKCE
|
|
|
+ // in $_SESSION. An attacker who pre-fixated the victim's cookie
|
|
|
+ // loses contact at this moment.
|
|
|
+ //
|
|
|
+ // Drive an initial request to establish a session id; capture it
|
|
|
+ // and the id observed inside authenticate(); assert they differ.
|
|
|
+ $this->request('GET', '/login');
|
|
|
+ $preId = session_id();
|
|
|
+ self::assertNotSame('', $preId);
|
|
|
+
|
|
|
+ $capture = new \stdClass();
|
|
|
+ $capture->idAtAuthenticate = null;
|
|
|
+ $this->bindOidcAuthenticator(new class ($capture) implements OidcAuthenticator {
|
|
|
+ public function __construct(private readonly \stdClass $capture)
|
|
|
+ {
|
|
|
+ }
|
|
|
+
|
|
|
+ public function authenticate(): OidcClaims
|
|
|
+ {
|
|
|
+ $this->capture->idAtAuthenticate = session_id();
|
|
|
+ // Short-circuit: we only care about the pre-handshake
|
|
|
+ // rotation, not the rest of the OIDC flow.
|
|
|
+ throw new OidcException('captured for test');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ $this->request('GET', '/login/oidc');
|
|
|
+
|
|
|
+ self::assertIsString($capture->idAtAuthenticate);
|
|
|
+ self::assertNotSame('', $capture->idAtAuthenticate);
|
|
|
+ self::assertNotSame(
|
|
|
+ $preId,
|
|
|
+ $capture->idAtAuthenticate,
|
|
|
+ 'session id was not rotated before OIDC handshake (F9 regression)',
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
public function testApiUnreachableDuringUpsertFlashesAndRedirects(): void
|
|
|
{
|
|
|
$this->bindOidcAuthenticator(new class () implements OidcAuthenticator {
|