diff --git a/.changeset/fix-auth-signout-cookie-cache.md b/.changeset/fix-auth-signout-cookie-cache.md new file mode 100644 index 0000000000..4c23acd93b --- /dev/null +++ b/.changeset/fix-auth-signout-cookie-cache.md @@ -0,0 +1,5 @@ +--- +"@agent-native/core": patch +--- + +Clear Better Auth session cache cookies when signing out. diff --git a/packages/core/src/server/auth.spec.ts b/packages/core/src/server/auth.spec.ts index 53f9e63177..425f5bda92 100644 --- a/packages/core/src/server/auth.spec.ts +++ b/packages/core/src/server/auth.spec.ts @@ -224,7 +224,7 @@ describe("server/auth", () => { errorSpy.mockRestore(); }); - it("lets logout opt the current browser out of AUTH_DISABLED inside an HTTPS iframe", async () => { + it("opts the current browser out and forwards Better Auth logout cookies", async () => { vi.stubEnv("NODE_ENV", "production"); vi.stubEnv("AUTH_DISABLED", "1"); delete process.env.ACCESS_TOKEN; @@ -237,7 +237,14 @@ describe("server/auth", () => { getSession: vi.fn(async () => null), signInEmail: vi.fn(), signUpEmail: vi.fn(), - signOut: vi.fn(), + signOut: vi.fn(async () => { + const headers = new Headers(); + headers.append( + "set-cookie", + "better-auth.session_data=; Max-Age=0; Path=/", + ); + return { headers }; + }), }, })), getBetterAuthSync: vi.fn(() => undefined), @@ -264,6 +271,9 @@ describe("server/auth", () => { expect(setCookie).toContain("SameSite=None"); expect(setCookie).toContain("Secure"); expect(setCookie).toContain("Partitioned"); + expect(setCookie).toContain( + "better-auth.session_data=; Max-Age=0; Path=/", + ); }); it("mounts generic Google OAuth routes by default when credentials are configured", async () => { diff --git a/packages/core/src/server/auth.ts b/packages/core/src/server/auth.ts index 0ad53df66f..c315311667 100644 --- a/packages/core/src/server/auth.ts +++ b/packages/core/src/server/auth.ts @@ -661,23 +661,23 @@ async function readDesktopSsoSafely( * user. Returns undefined if no session cookie was minted (the common * case — Better Auth's reset doesn't auto-sign-in by default). */ +function getSetCookieHeaders(headers: Headers): string[] { + const responseHeaders = headers as Headers & { + getSetCookie?: () => string[]; + }; + return typeof responseHeaders.getSetCookie === "function" + ? responseHeaders.getSetCookie() + : (responseHeaders.get("set-cookie") ?? "") + .split(/,(?=[^;]+=)/) + .map((value) => value.trim()) + .filter(Boolean); +} + function extractSessionTokenFromSetCookies( response: Response, ): string | undefined { try { - // Headers may have multiple Set-Cookie entries; iterate via getSetCookie - // when available (Node 20+ / undici), else fall back to comma split. - const headers = response.headers as Headers & { - getSetCookie?: () => string[]; - }; - const setCookies = - typeof headers.getSetCookie === "function" - ? headers.getSetCookie() - : (headers.get("set-cookie") ?? "") - .split(/,(?=[^;]+=)/) - .map((s) => s.trim()) - .filter(Boolean); - for (const sc of setCookies) { + for (const sc of getSetCookieHeaders(response.headers)) { // Better Auth's session cookie name is configurable but defaults to // `.session_token`. Match either the Better Auth default or // our COOKIE_NAME (`an_session`) on the same line. @@ -692,6 +692,15 @@ function extractSessionTokenFromSetCookies( return undefined; } +function forwardBetterAuthSetCookies(event: H3Event, result: unknown): void { + if (!result || typeof result !== "object") return; + const headers = (result as { headers?: Headers }).headers; + if (!headers || typeof headers.get !== "function") return; + for (const cookie of getSetCookieHeaders(headers)) { + event.res?.headers?.append("set-cookie", cookie); + } +} + // --------------------------------------------------------------------------- // ACCESS_TOKEN resolution // --------------------------------------------------------------------------- @@ -3444,7 +3453,11 @@ async function mountBetterAuthRoutes( optOutOfAuthDisabledSession(event); try { - await auth.api.signOut({ headers: event.headers }); + const result = await auth.api.signOut({ + headers: event.headers, + returnHeaders: true, + }); + forwardBetterAuthSetCookies(event, result); } catch { // Ignore if no Better Auth session } @@ -3511,7 +3524,11 @@ async function mountBetterAuthRoutes( clearFrameworkSessionCookies(event); optOutOfAuthDisabledSession(event); try { - await auth.api.signOut({ headers: event.headers }); + const result = await auth.api.signOut({ + headers: event.headers, + returnHeaders: true, + }); + forwardBetterAuthSetCookies(event, result); } catch { // Ignore — sessions are already gone in DB. } @@ -3692,7 +3709,11 @@ function mountAuthFallbackRoutes(app: H3App): void { try { const auth = await getBetterAuth(); - await auth.api.signOut({ headers: event.headers }); + const result = await auth.api.signOut({ + headers: event.headers, + returnHeaders: true, + }); + forwardBetterAuthSetCookies(event, result); } catch { // Ignore if Better Auth is still unavailable } diff --git a/packages/core/src/server/better-auth-instance.ts b/packages/core/src/server/better-auth-instance.ts index 670901aadc..5ffee59947 100644 --- a/packages/core/src/server/better-auth-instance.ts +++ b/packages/core/src/server/better-auth-instance.ts @@ -395,7 +395,10 @@ export interface BetterAuthInstance { }; headers?: Headers; }) => Promise; - signOut: (opts: { headers: Headers }) => Promise; + signOut: (opts: { + headers: Headers; + returnHeaders?: boolean; + }) => Promise; }; } diff --git a/packages/mobile-app/components/AppWebView.tsx b/packages/mobile-app/components/AppWebView.tsx index dd15d8a325..f7a8ee057d 100644 --- a/packages/mobile-app/components/AppWebView.tsx +++ b/packages/mobile-app/components/AppWebView.tsx @@ -90,6 +90,7 @@ const SESSION_BRIDGE_SCRIPT = ` window.__agentNativeSessionBridgeRunning = true; var postToken = function () { fetch('/_agent-native/auth/session', { + cache: 'no-store', credentials: 'include', headers: { Accept: 'application/json' } }) diff --git a/templates/clips/changelog/2026-08-04-signing-out-now-takes-effect-immediately-after-verifying-a-n.md b/templates/clips/changelog/2026-08-04-signing-out-now-takes-effect-immediately-after-verifying-a-n.md new file mode 100644 index 0000000000..e9f10f1e70 --- /dev/null +++ b/templates/clips/changelog/2026-08-04-signing-out-now-takes-effect-immediately-after-verifying-a-n.md @@ -0,0 +1,6 @@ +--- +type: fixed +date: 2026-08-04 +--- + +Signing out now takes effect immediately after verifying a new account. diff --git a/templates/clips/desktop/src/app.tsx b/templates/clips/desktop/src/app.tsx index 62311895b8..75e54d1913 100644 --- a/templates/clips/desktop/src/app.tsx +++ b/templates/clips/desktop/src/app.tsx @@ -1206,7 +1206,7 @@ export function App() { try { const res = await fetch( `${serverUrl.replace(/\/+$/, "")}/_agent-native/auth/session`, - { credentials: "include" }, + { credentials: "include", cache: "no-store" }, ); if (!res.ok) { if (res.status === 401 || res.status === 403) {