Skip to content

Commit 1280cdf

Browse files
authored
fix: expo client with cookie-cache overwrites the original token (#1269)
In cases where the session_data cookie is expired, but session_token is not, the server returns only a newly signed session_data. The current implementation causes the existing session_token to be erased, causing a log-out. This commit fixes it, along with test cases to verify.
1 parent 89b28d0 commit 1280cdf

File tree

2 files changed

+109
-6
lines changed

2 files changed

+109
-6
lines changed

packages/expo/src/client.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ interface StoredCookie {
5151
expires: Date | null;
5252
}
5353

54-
function getSetCookie(header: string) {
54+
function getSetCookie(header: string, prevCookie?: string) {
5555
const parsed = parseSetCookieHeader(header);
56-
const toSetCookie: Record<string, StoredCookie> = {};
56+
let toSetCookie: Record<string, StoredCookie> = {};
5757
parsed.forEach((cookie, key) => {
5858
const expiresAt = cookie["expires"];
5959
const maxAge = cookie["max-age"];
@@ -67,6 +67,17 @@ function getSetCookie(header: string) {
6767
expires,
6868
};
6969
});
70+
if (prevCookie) {
71+
try {
72+
const prevCookieParsed = JSON.parse(prevCookie);
73+
toSetCookie = {
74+
...prevCookieParsed,
75+
...toSetCookie,
76+
};
77+
} catch {
78+
//
79+
}
80+
}
7081
return JSON.stringify(toSetCookie);
7182
}
7283

@@ -140,7 +151,11 @@ export const expoClient = (opts: ExpoClientOptions) => {
140151
if (isWeb) return;
141152
const setCookie = context.response.headers.get("set-cookie");
142153
if (setCookie) {
143-
const toSetCookie = getSetCookie(setCookie || "");
154+
const prevCookie = await storage.getItem(cookieName);
155+
const toSetCookie = getSetCookie(
156+
setCookie || "",
157+
prevCookie ?? undefined,
158+
);
144159
await storage.setItem(cookieName, toSetCookie);
145160
store?.notify("$sessionSignal");
146161
}

packages/expo/src/expo.test.ts

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createAuthClient } from "better-auth/client";
22
import Database from "better-sqlite3";
3-
import { beforeAll, describe, expect, it, vi } from "vitest";
3+
import { beforeAll, afterAll, describe, expect, it, vi } from "vitest";
44
import { expo } from ".";
55
import { expoClient } from "./client";
66
import { betterAuth } from "better-auth";
@@ -44,7 +44,7 @@ vi.mock("expo-linking", async () => {
4444

4545
const fn = vi.fn();
4646

47-
describe("expo", async () => {
47+
function testUtils(extraOpts?: Parameters<typeof betterAuth>[0]) {
4848
const storage = new Map<string, string>();
4949

5050
const auth = betterAuth({
@@ -61,6 +61,7 @@ describe("expo", async () => {
6161
},
6262
plugins: [expo()],
6363
trustedOrigins: ["better-auth://"],
64+
...extraOpts,
6465
});
6566

6667
const client = createAuthClient({
@@ -80,9 +81,20 @@ describe("expo", async () => {
8081
}),
8182
],
8283
});
84+
85+
return { storage, auth, client };
86+
}
87+
88+
describe("expo", async () => {
89+
const { auth, client, storage } = testUtils();
90+
8391
beforeAll(async () => {
8492
const { runMigrations } = await getMigrations(auth.options);
8593
await runMigrations();
94+
vi.useFakeTimers();
95+
});
96+
afterAll(() => {
97+
vi.useRealTimers();
8698
});
8799

88100
it("should store cookie with expires date", async () => {
@@ -96,7 +108,7 @@ describe("expo", async () => {
96108
expect(storedCookie).toBeDefined();
97109
const parsedCookie = JSON.parse(storedCookie || "");
98110
expect(parsedCookie["better-auth.session_token"]).toMatchObject({
99-
value: expect.any(String),
111+
value: expect.stringMatching(/.+/),
100112
expires: expect.any(String),
101113
});
102114
});
@@ -128,3 +140,79 @@ describe("expo", async () => {
128140
expect(c).includes("better-auth.session_token");
129141
});
130142
});
143+
144+
describe("expo with cookieCache", async () => {
145+
const { auth, client, storage } = testUtils({
146+
session: {
147+
expiresIn: 5,
148+
cookieCache: {
149+
enabled: true,
150+
maxAge: 1,
151+
},
152+
},
153+
});
154+
beforeAll(async () => {
155+
const { runMigrations } = await getMigrations(auth.options);
156+
await runMigrations();
157+
vi.useFakeTimers();
158+
});
159+
afterAll(() => {
160+
vi.useRealTimers();
161+
});
162+
163+
it("should store cookie with expires date", async () => {
164+
const testUser = {
165+
166+
password: "password",
167+
name: "Test User",
168+
};
169+
await client.signUp.email(testUser);
170+
const storedCookie = storage.get("better-auth_cookie");
171+
expect(storedCookie).toBeDefined();
172+
const parsedCookie = JSON.parse(storedCookie || "");
173+
expect(parsedCookie["better-auth.session_token"]).toMatchObject({
174+
value: expect.stringMatching(/.+/),
175+
expires: expect.any(String),
176+
});
177+
expect(parsedCookie["better-auth.session_data"]).toMatchObject({
178+
value: expect.stringMatching(/.+/),
179+
expires: expect.any(String),
180+
});
181+
});
182+
it("should refresh session_data when it expired without erasing session_token", async () => {
183+
vi.advanceTimersByTime(1000);
184+
const { data } = await client.getSession();
185+
expect(data).toMatchObject({
186+
session: expect.any(Object),
187+
user: expect.any(Object),
188+
});
189+
const storedCookie = storage.get("better-auth_cookie");
190+
expect(storedCookie).toBeDefined();
191+
const parsedCookie = JSON.parse(storedCookie || "");
192+
expect(parsedCookie["better-auth.session_token"]).toMatchObject({
193+
value: expect.stringMatching(/.+/),
194+
expires: expect.any(String),
195+
});
196+
expect(parsedCookie["better-auth.session_data"]).toMatchObject({
197+
value: expect.stringMatching(/.+/),
198+
expires: expect.any(String),
199+
});
200+
});
201+
202+
it("should erase both session_data and session_token when token expired", async () => {
203+
vi.advanceTimersByTime(5000);
204+
const { data } = await client.getSession();
205+
expect(data).toBeNull();
206+
const storedCookie = storage.get("better-auth_cookie");
207+
expect(storedCookie).toBeDefined();
208+
const parsedCookie = JSON.parse(storedCookie || "");
209+
expect(parsedCookie["better-auth.session_token"]).toMatchObject({
210+
value: expect.stringMatching(/^$/),
211+
expires: expect.any(String),
212+
});
213+
expect(parsedCookie["better-auth.session_data"]).toMatchObject({
214+
value: expect.stringMatching(/^$/),
215+
expires: expect.any(String),
216+
});
217+
});
218+
});

0 commit comments

Comments
 (0)