fix: persist refreshed idToken in getAccessToken (#8211)

This commit is contained in:
Gautam Manchandani
2026-02-28 12:22:09 +05:30
committed by GitHub
parent 95d30ddea2
commit d3f3e63260
2 changed files with 310 additions and 0 deletions

View File

@@ -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<Account>({
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",

View File

@@ -565,6 +565,7 @@ export const getAccessToken = createAuthEndpoint(
ctx.context,
),
refreshTokenExpiresAt: newTokens?.refreshTokenExpiresAt,
idToken: newTokens?.idToken || account.idToken,
};
let updatedAccount: Record<string, any> | null = null;
if (account.id) {