From d3f3e63260ed2fd58a1cf93caba97e8b2f75db28 Mon Sep 17 00:00:00 2001 From: Gautam Manchandani Date: Sat, 28 Feb 2026 12:22:09 +0530 Subject: [PATCH] fix: persist refreshed idToken in getAccessToken (#8211) --- .../src/api/routes/account.test.ts | 309 ++++++++++++++++++ .../better-auth/src/api/routes/account.ts | 1 + 2 files changed, 310 insertions(+) diff --git a/packages/better-auth/src/api/routes/account.test.ts b/packages/better-auth/src/api/routes/account.test.ts index 5692c99cbc..24cf5f7c9c 100644 --- a/packages/better-auth/src/api/routes/account.test.ts +++ b/packages/better-auth/src/api/routes/account.test.ts @@ -516,6 +516,315 @@ describe("account", async () => { expect(accessTokenRes.data?.accessToken).toBe("test"); }); + it("should persist refreshed idToken in database during getAccessToken auto-refresh", async () => { + const { auth, client, cookieSetter } = await getTestInstance({ + socialProviders: { + google: { + clientId: "test", + clientSecret: "test", + enabled: true, + }, + }, + account: { + storeAccountCookie: false, + }, + }); + + const ctx = await auth.$context; + const headers = new Headers(); + email = "persist-id-token-db@test.com"; + + const now = Math.floor(Date.now() / 1000); + const oldIdToken = await signJWT( + { + email, + email_verified: true, + name: "First Last", + picture: "https://lh3.googleusercontent.com/a-/AOh14GjQ4Z7Vw", + exp: now + 3600, + sub: "persist-id-token-db", + iat: now, + aud: "test", + azp: "test", + nbf: now, + iss: "test", + locale: "en", + jti: "old-id-token", + given_name: "First", + family_name: "Last", + } satisfies GoogleProfile, + DEFAULT_SECRET, + ); + const newIdToken = await signJWT( + { + email, + email_verified: true, + name: "First Last", + picture: "https://lh3.googleusercontent.com/a-/AOh14GjQ4Z7Vw", + exp: now + 7200, + sub: "persist-id-token-db", + iat: now, + aud: "test", + azp: "test", + nbf: now, + iss: "test", + locale: "en", + jti: "new-id-token", + given_name: "First", + family_name: "Last", + } satisfies GoogleProfile, + DEFAULT_SECRET, + ); + + let refreshTokenCalls = 0; + server.use( + http.post("https://oauth2.googleapis.com/token", async ({ request }) => { + const body = await request.text(); + const grantType = new URLSearchParams(body).get("grant_type"); + + if (grantType === "refresh_token") { + refreshTokenCalls += 1; + return HttpResponse.json({ + access_token: "refreshed-access-token", + refresh_token: "refreshed-refresh-token", + expires_in: 3600, + id_token: newIdToken, + }); + } + + return HttpResponse.json({ + access_token: "initial-access-token", + refresh_token: "initial-refresh-token", + expires_in: 1, + id_token: oldIdToken, + }); + }), + ); + + const signInRes = await client.signIn.social({ + provider: "google", + callbackURL: "/callback", + fetchOptions: { + onSuccess: cookieSetter(headers), + }, + }); + + expect(signInRes.data).toMatchObject({ + url: expect.stringContaining("google.com"), + redirect: true, + }); + + const state = + signInRes.data && "url" in signInRes.data && signInRes.data.url + ? new URL(signInRes.data.url).searchParams.get("state") || "" + : ""; + + await client.$fetch("/callback/google", { + query: { + state, + code: "test", + }, + headers, + method: "GET", + onError(context) { + expect(context.response.status).toBe(302); + cookieSetter(headers)({ response: context.response }); + }, + }); + + const firstAccessToken = await client.getAccessToken( + { + providerId: "google", + }, + { + headers, + onSuccess: cookieSetter(headers), + }, + ); + expect(firstAccessToken.error).toBeFalsy(); + expect(firstAccessToken.data?.idToken).toBe(newIdToken); + + const secondAccessToken = await client.getAccessToken( + { + providerId: "google", + }, + { + headers, + }, + ); + expect(secondAccessToken.error).toBeFalsy(); + expect(secondAccessToken.data?.idToken).toBe(newIdToken); + expect(refreshTokenCalls).toBe(1); + + const account = await ctx.adapter.findOne({ + model: "account", + where: [{ field: "providerId", value: "google" }], + }); + expect(account).toBeTruthy(); + expect(account?.idToken).toBe(newIdToken); + }); + + it("should persist refreshed idToken in account cookie during getAccessToken auto-refresh in stateless mode", async () => { + const { auth, client, cookieSetter } = await getTestInstance({ + database: undefined as any, + socialProviders: { + google: { + clientId: "test", + clientSecret: "test", + enabled: true, + }, + }, + account: { + storeAccountCookie: true, + }, + }); + const ctx = await auth.$context; + const accountDataCookieName = ctx.authCookies.accountData.name; + + const headers = new Headers(); + email = "persist-id-token-cookie@test.com"; + + const now = Math.floor(Date.now() / 1000); + const oldIdToken = await signJWT( + { + email, + email_verified: true, + name: "First Last", + picture: "https://lh3.googleusercontent.com/a-/AOh14GjQ4Z7Vw", + exp: now + 3600, + sub: "persist-id-token-cookie", + iat: now, + aud: "test", + azp: "test", + nbf: now, + iss: "test", + locale: "en", + jti: "old-cookie-id-token", + given_name: "First", + family_name: "Last", + } satisfies GoogleProfile, + DEFAULT_SECRET, + ); + const newIdToken = await signJWT( + { + email, + email_verified: true, + name: "First Last", + picture: "https://lh3.googleusercontent.com/a-/AOh14GjQ4Z7Vw", + exp: now + 7200, + sub: "persist-id-token-cookie", + iat: now, + aud: "test", + azp: "test", + nbf: now, + iss: "test", + locale: "en", + jti: "new-cookie-id-token", + given_name: "First", + family_name: "Last", + } satisfies GoogleProfile, + DEFAULT_SECRET, + ); + + let refreshTokenCalls = 0; + server.use( + http.post("https://oauth2.googleapis.com/token", async ({ request }) => { + const body = await request.text(); + const grantType = new URLSearchParams(body).get("grant_type"); + + if (grantType === "refresh_token") { + refreshTokenCalls += 1; + return HttpResponse.json({ + access_token: "refreshed-cookie-access-token", + refresh_token: "refreshed-cookie-refresh-token", + expires_in: 3600, + id_token: newIdToken, + }); + } + + return HttpResponse.json({ + access_token: "initial-cookie-access-token", + refresh_token: "initial-cookie-refresh-token", + expires_in: 1, + id_token: oldIdToken, + }); + }), + ); + + const signInRes = await client.signIn.social({ + provider: "google", + callbackURL: "/callback", + fetchOptions: { + onSuccess: cookieSetter(headers), + }, + }); + + expect(signInRes.data).toMatchObject({ + url: expect.stringContaining("google.com"), + redirect: true, + }); + + const state = + signInRes.data && "url" in signInRes.data && signInRes.data.url + ? new URL(signInRes.data.url).searchParams.get("state") || "" + : ""; + + await client.$fetch("/callback/google", { + query: { + state, + code: "test", + }, + headers, + method: "GET", + onError(context) { + expect(context.response.status).toBe(302); + cookieSetter(headers)({ response: context.response }); + }, + }); + + let refreshedAccountCookie: string | undefined; + const firstAccessToken = await client.getAccessToken( + { + providerId: "google", + }, + { + headers, + onSuccess(context) { + cookieSetter(headers)(context); + const cookies = parseSetCookieHeader( + context.response.headers.get("set-cookie") || "", + ); + refreshedAccountCookie = + cookies.get(accountDataCookieName)?.value || undefined; + }, + }, + ); + expect(firstAccessToken.error).toBeFalsy(); + expect(firstAccessToken.data?.idToken).toBe(newIdToken); + expect(refreshedAccountCookie).toBeDefined(); + await expect( + symmetricDecodeJWT( + refreshedAccountCookie!, + ctx.secret, + "better-auth-account", + ), + ).resolves.toMatchObject({ + idToken: newIdToken, + }); + + const secondAccessToken = await client.getAccessToken( + { + providerId: "google", + }, + { + headers, + }, + ); + expect(secondAccessToken.error).toBeFalsy(); + expect(secondAccessToken.data?.idToken).toBe(newIdToken); + expect(refreshTokenCalls).toBeGreaterThan(0); + }); + it("should NOT chunk account data cookies when exceeding 4KB", async () => { const { client, cookieSetter } = await getTestInstance({ secret: "better-auth.secret", diff --git a/packages/better-auth/src/api/routes/account.ts b/packages/better-auth/src/api/routes/account.ts index 0ee1e77a43..a5b6ff5a10 100644 --- a/packages/better-auth/src/api/routes/account.ts +++ b/packages/better-auth/src/api/routes/account.ts @@ -565,6 +565,7 @@ export const getAccessToken = createAuthEndpoint( ctx.context, ), refreshTokenExpiresAt: newTokens?.refreshTokenExpiresAt, + idToken: newTokens?.idToken || account.idToken, }; let updatedAccount: Record | null = null; if (account.id) {