mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 23:53:17 -05:00
fix(multi-session): prevent duplicate cookies when same user signs in multiple times (#7256)
Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
committed by
Alex Yang
co-authored by
Alex Yang
parent
94b7589162
commit
9db76abbea
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user