feat(multi-session): add support for custom session ID logic

This commit is contained in:
Clément Maisonhaute
2026-01-16 13:07:45 +01:00
parent 3b12064382
commit 1bc60dfd2b
2 changed files with 95 additions and 10 deletions

View File

@@ -1,4 +1,5 @@
import type { BetterAuthPlugin } from "@better-auth/core";
import type { Session, User } from "@better-auth/core/db";
import {
createAuthEndpoint,
createAuthMiddleware,
@@ -30,6 +31,13 @@ export interface MultiSessionConfig {
* @default 5
*/
maximumSessions?: number | undefined;
/**
* Determines the unique identifier for a session to prevent duplicates in the session list.
* By default, it groups sessions by User ID.
* @default (data) => data.user.id
*/
getUniqSessionId?: (data: { session: Session; user: User }) => string;
}
import { MULTI_SESSION_ERROR_CODES as ERROR_CODES } from "./error-codes";
@@ -51,8 +59,9 @@ const revokeDeviceSessionBodySchema = z.object({
export const multiSession = (options?: MultiSessionConfig | undefined) => {
const opts = {
maximumSessions: 5,
getUniqSessionId: (data) => data.user.id,
...options,
};
} satisfies Required<MultiSessionConfig>;
const isMultiSessionCookie = (key: string) => key.includes("_multi-");
@@ -102,16 +111,13 @@ export const multiSession = (options?: MultiSessionConfig | undefined) => {
const validSessions = sessions.filter(
(session) => session && session.session.expiresAt > new Date(),
);
const uniqueUserSessions = validSessions.reduce(
(acc, session) => {
if (!acc.find((s) => s.user.id === session.user.id)) {
acc.push(session);
}
return acc;
},
[] as typeof validSessions,
const uniqueSessionsMap = new Map(
validSessions.map((session) => [
opts.getUniqSessionId(session),
session,
]),
);
return ctx.json(uniqueUserSessions);
return ctx.json([...uniqueSessionsMap.values()]);
},
),
/**

View File

@@ -213,4 +213,83 @@ describe("multi-session", async () => {
});
expect(attackerSessionAfter.data).toBeNull();
});
it("should deduplicate sessions based on getUniqSessionId", async () => {
const dedupeHeaders = new Headers();
const userCredentials = {
email: "dedupe@test.com",
password: "password",
name: "Dedupe User",
};
await client.signUp.email(userCredentials, {
onSuccess: cookieSetter(dedupeHeaders),
});
await client.signIn.email(userCredentials, {
onSuccess: cookieSetter(dedupeHeaders),
});
const res = await client.multiSession.listDeviceSessions({
fetchOptions: {
headers: dedupeHeaders,
},
});
expect(res.data).toHaveLength(1);
expect(res.data!.at(0)!.user.email).toBe(userCredentials.email);
});
describe("multi-session with custom unique logic", async () => {
const { client, testUser, cookieSetter } = await getTestInstance(
{
plugins: [
multiSession({
getUniqSessionId: (data) => data.session.token,
}),
],
},
{
clientOptions: {
plugins: [multiSessionClient()],
},
},
);
it("should show multiple sessions for the same user with custom getUniqSessionId", async () => {
const customHeaders = new Headers();
await client.signIn.email(
{
email: testUser.email,
password: testUser.password,
},
{
onSuccess: cookieSetter(customHeaders),
},
);
await client.signIn.email(
{
email: testUser.email,
password: testUser.password,
},
{
onSuccess: cookieSetter(customHeaders),
},
);
const res = await client.multiSession.listDeviceSessions({
fetchOptions: {
headers: customHeaders,
},
});
expect(res.data).toHaveLength(2);
expect(res.data!.at(0)!.session.token).not.toBe(
res.data!.at(1)!.session.token,
);
expect(res.data!.at(0)!.user.id).toBe(res.data!.at(1)!.user.id);
});
});
});