From 419d1bc99b209faa016c0baabcffd79cc2b09e1b Mon Sep 17 00:00:00 2001 From: Michael Thomas Date: Fri, 27 Mar 2026 11:44:23 -0400 Subject: [PATCH] fix: ensure that OIDC correlation cookies are present in callback --- server/routes/auth.test.ts | 58 ++++++++++++++++++++++++++++++++++++++ server/routes/auth.ts | 13 +++++++++ 2 files changed, 71 insertions(+) diff --git a/server/routes/auth.test.ts b/server/routes/auth.test.ts index d4918d2c7..293331476 100644 --- a/server/routes/auth.test.ts +++ b/server/routes/auth.test.ts @@ -768,5 +768,63 @@ describe('OpenID Connect', () => { assert.strictEqual(response.status, 403); assert.strictEqual(response.body.error, ApiErrorCode.Unauthorized); }); + + it('rejects callback when correlation cookies are missing', async function () { + await setupFetchMock(); + + const callbackUrl = new URL(OIDC_REDIRECT_URL); + callbackUrl.searchParams.set('code', '123456'); + callbackUrl.searchParams.set('state', 'somestate'); + + // Send callback with no signed cookies at all + const response = await request(app) + .post('/auth/oidc/callback/test') + .set('Accept', 'application/json') + .send({ callbackUrl: callbackUrl.toString() }); + + assert.strictEqual(response.status, 400); + assert.strictEqual( + response.body.error, + ApiErrorCode.OidcAuthorizationFailed + ); + }); + + it('rejects callback when only one correlation cookie is present', async function () { + await setupFetchMock(); + + // Perform login to get only the state cookie + const loginResponse = await request(app) + .get('/auth/oidc/login/test') + .set('Accept', 'application/json'); + + assert.strictEqual(loginResponse.status, 200); + + const redirectUrl = new URL(loginResponse.body.redirectUrl); + const state = redirectUrl.searchParams.get('state'); + + // Extract only the state cookie, dropping the PKCE verifier cookie + const cookies = loginResponse.get('Set-Cookie'); + assert.notStrictEqual(cookies, undefined); + const stateCookieOnly = cookies! + .filter((c) => c.includes('oidc-state')) + .map((c) => c.split(';')[0]) + .join('; '); + + const callbackUrl = new URL(OIDC_REDIRECT_URL); + callbackUrl.searchParams.set('code', '123456'); + if (state) callbackUrl.searchParams.set('state', state); + + const response = await request(app) + .post('/auth/oidc/callback/test') + .set('Accept', 'application/json') + .set('Cookie', stateCookieOnly) + .send({ callbackUrl: callbackUrl.toString() }); + + assert.strictEqual(response.status, 400); + assert.strictEqual( + response.body.error, + ApiErrorCode.OidcAuthorizationFailed + ); + }); }); }); diff --git a/server/routes/auth.ts b/server/routes/auth.ts index c082b4a78..322f6eec0 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -798,6 +798,19 @@ authRoutes.post( const pkceCodeVerifier: string | undefined = req.signedCookies[OIDC_CODE_VERIFIER_KEY]; const expectedState: string | undefined = req.signedCookies[OIDC_STATE_KEY]; + + if (!pkceCodeVerifier || !expectedState) { + logger.warn('Rejected OIDC callback without correlation cookies', { + label: 'Auth', + provider: provider.slug, + ip: req.ip, + }); + return next({ + status: 400, + error: ApiErrorCode.OidcAuthorizationFailed, + }); + } + res.clearCookie(OIDC_CODE_VERIFIER_KEY); res.clearCookie(OIDC_STATE_KEY);