From b2a495e88503c9fe5bb9f1f77a64b93bfcfa822c Mon Sep 17 00:00:00 2001 From: DarkSky <25152247+darkskygit@users.noreply.github.com> Date: Thu, 5 Feb 2026 23:39:26 +0800 Subject: [PATCH] fix: sign out (#14376) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### PR Dependency Tree * **PR #14376** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) ## Summary by CodeRabbit * **Bug Fixes** * Sign-out functionality now works in more scenarios, including when headers are absent or duplicated. * **Tests** * Added test coverage for sign-out behavior across different header configurations. --- .../src/__tests__/auth/controller.spec.ts | 62 +++++++++++++++++++ .../server/src/core/auth/controller.ts | 5 +- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/packages/backend/server/src/__tests__/auth/controller.spec.ts b/packages/backend/server/src/__tests__/auth/controller.spec.ts index cb43a1646..8366462d4 100644 --- a/packages/backend/server/src/__tests__/auth/controller.spec.ts +++ b/packages/backend/server/src/__tests__/auth/controller.spec.ts @@ -202,6 +202,68 @@ test('should be able to sign out', async t => { t.falsy(session); }); +test('should be able to sign out when csrf header is missing (compat)', async t => { + const { app } = t.context; + + const u1 = await app.createUser('u1@affine.pro'); + + const signInRes = await supertest(app.getHttpServer()) + .post('/api/auth/sign-in') + .send({ email: u1.email, password: u1.password }) + .expect(200); + + const cookies = parseCookies(signInRes); + const cookieHeader = Object.entries(cookies) + .map(([k, v]) => `${k}=${v}`) + .join('; '); + + await supertest(app.getHttpServer()) + .post('/api/auth/sign-out') + .set('Cookie', cookieHeader) + .expect(200); + + const sessionRes = await supertest(app.getHttpServer()) + .get('/api/auth/session') + .set('Cookie', cookieHeader) + .expect(200); + + t.falsy(sessionRes.body.user); +}); + +test('should be able to sign out when duplicated csrf cookies exist', async t => { + const { app } = t.context; + + const u1 = await app.createUser('u1@affine.pro'); + + const signInRes = await supertest(app.getHttpServer()) + .post('/api/auth/sign-in') + .send({ email: u1.email, password: u1.password }) + .expect(200); + + const cookies = parseCookies(signInRes); + const csrf = cookies[AuthService.csrfCookieName]; + + const cookieHeader = [ + `${AuthService.sessionCookieName}=${cookies[AuthService.sessionCookieName]}`, + `${AuthService.userCookieName}=${cookies[AuthService.userCookieName]}`, + `${AuthService.csrfCookieName}=${csrf}`, + `${AuthService.csrfCookieName}=${randomUUID()}`, + ].join('; '); + + await supertest(app.getHttpServer()) + .post('/api/auth/sign-out') + .set('Cookie', cookieHeader) + .set('x-affine-csrf-token', csrf) + .expect(200); + + const sessionRes = await supertest(app.getHttpServer()) + .get('/api/auth/session') + .set('Cookie', cookieHeader) + .expect(200); + + t.falsy(sessionRes.body.user); +}); + test('should reject sign out when csrf token mismatched', async t => { const { app } = t.context; diff --git a/packages/backend/server/src/core/auth/controller.ts b/packages/backend/server/src/core/auth/controller.ts index f39a8c07e..fe02cf38e 100644 --- a/packages/backend/server/src/core/auth/controller.ts +++ b/packages/backend/server/src/core/auth/controller.ts @@ -245,7 +245,10 @@ export class AuthController { | string | undefined; const csrfHeader = req.get('x-affine-csrf-token'); - if (!csrfCookie || !csrfHeader || csrfCookie !== csrfHeader) { + if ( + csrfHeader && // optional for backward compatibility, drop after 0.25.0 outdated + (!csrfCookie || csrfCookie !== csrfHeader) + ) { throw new ActionForbidden(); }