From 9db76abbea454f77d2605e35d3e448560ed53203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paola=20Estefan=C3=ADa=20de=20Campos?= <84341268+Paola3stefania@users.noreply.github.com> Date: Mon, 26 Jan 2026 21:36:23 -0300 Subject: [PATCH] fix(multi-session): prevent duplicate cookies when same user signs in multiple times (#7256) Co-authored-by: Alex Yang --- .../src/plugins/multi-session/index.ts | 47 ++++++++++----- .../multi-session/multi-session.test.ts | 60 +++++++++++++++++++ 2 files changed, 93 insertions(+), 14 deletions(-) diff --git a/packages/better-auth/src/plugins/multi-session/index.ts b/packages/better-auth/src/plugins/multi-session/index.ts index 7d806a0aac..4bd044d294 100644 --- a/packages/better-auth/src/plugins/multi-session/index.ts +++ b/packages/better-auth/src/plugins/multi-session/index.ts @@ -305,26 +305,45 @@ export const multiSession = (options?: MultiSessionConfig | undefined) => { handler: createAuthMiddleware(async (ctx) => { const cookieString = ctx.context.responseHeaders?.get("set-cookie"); if (!cookieString) return; - const setCookies = parseSetCookieHeader(cookieString); + const newSession = ctx.context.newSession; + if (!newSession) return; + const sessionCookieConfig = ctx.context.authCookies.sessionToken; - const sessionToken = ctx.context.newSession?.session.token; - if (!sessionToken) return; + const sessionToken = newSession.session.token; + const cookieName = `${sessionCookieConfig.name}_multi-${sessionToken.toLowerCase()}`; + + const setCookies = parseSetCookieHeader(cookieString); const cookies = parseCookies(ctx.headers?.get("cookie") || ""); - - const cookieName = `${ - sessionCookieConfig.name - }_multi-${sessionToken.toLowerCase()}`; - if (setCookies.get(cookieName) || cookies.get(cookieName)) return; - const currentMultiSessions = - Object.keys(Object.fromEntries(cookies)).filter( - isMultiSessionCookie, - ).length + (cookieString.includes("session_token") ? 1 : 0); + const multiSessionKeys = Object.keys( + Object.fromEntries(cookies), + ).filter(isMultiSessionCookie); - if (currentMultiSessions >= opts.maximumSessions) { - return; + const tokensToDelete: string[] = []; + for (const key of multiSessionKeys) { + const token = await ctx.getSignedCookie(key, ctx.context.secret); + if (!token) continue; + const session = + await ctx.context.internalAdapter.findSession(token); + if (session?.user.id === newSession.user.id) { + tokensToDelete.push(token); + ctx.setCookie(key, "", { + ...sessionCookieConfig.attributes, + maxAge: 0, + }); + } } + if (tokensToDelete.length > 0) { + await ctx.context.internalAdapter.deleteSessions(tokensToDelete); + } + + const currentCount = + multiSessionKeys.length - + tokensToDelete.length + + (cookieString.includes(sessionCookieConfig.name) ? 1 : 0); + + if (currentCount > opts.maximumSessions) return; await ctx.setSignedCookie( cookieName, diff --git a/packages/better-auth/src/plugins/multi-session/multi-session.test.ts b/packages/better-auth/src/plugins/multi-session/multi-session.test.ts index bd8c1c4062..2d195b3c49 100644 --- a/packages/better-auth/src/plugins/multi-session/multi-session.test.ts +++ b/packages/better-auth/src/plugins/multi-session/multi-session.test.ts @@ -149,6 +149,66 @@ describe("multi-session", async () => { expect(res2.data).toHaveLength(0); }); + it("should replace old multi-session cookie when same user signs in again", async () => { + const sameUserHeaders = new Headers(); + const sameUser = { + email: "same-user@test.com", + password: "password", + name: "Same User", + }; + + let firstSessionToken = ""; + await client.signUp.email(sameUser, { + onSuccess: cookieSetter(sameUserHeaders), + onResponse(context) { + const header = context.response.headers.get("set-cookie"); + const cookies = parseSetCookieHeader(header || ""); + firstSessionToken = + cookies.get("better-auth.session_token")?.value.split(".")[0] || ""; + }, + }); + + const sessionsAfterFirst = await client.multiSession.listDeviceSessions({ + fetchOptions: { headers: sameUserHeaders }, + }); + const firstUserSessions = sessionsAfterFirst.data?.filter( + (s) => s.user.email === sameUser.email, + ); + expect(firstUserSessions).toHaveLength(1); + + let secondSessionToken = ""; + await client.signIn.email( + { + email: sameUser.email, + password: sameUser.password, + }, + { + onSuccess: cookieSetter(sameUserHeaders), + onResponse(context) { + const header = context.response.headers.get("set-cookie"); + const cookies = parseSetCookieHeader(header || ""); + secondSessionToken = + cookies.get("better-auth.session_token")?.value.split(".")[0] || ""; + // Verify old cookie is being deleted + const oldCookieName = `better-auth.session_token_multi-${firstSessionToken.toLowerCase()}`; + const oldCookie = cookies.get(oldCookieName); + expect(oldCookie?.["max-age"]).toBe(0); + }, + headers: sameUserHeaders, + }, + ); + + expect(secondSessionToken).not.toBe(firstSessionToken); + const sessionsAfterSecond = await client.multiSession.listDeviceSessions({ + fetchOptions: { headers: sameUserHeaders }, + }); + const secondUserSessions = sessionsAfterSecond.data?.filter( + (s) => s.user.email === sameUser.email, + ); + expect(secondUserSessions).toHaveLength(1); + expect(secondUserSessions?.[0]?.session.token).toBe(secondSessionToken); + }); + it("should reject forged multi-session cookies on sign-out", async () => { const attackerUser = { email: "attacker@test.com",