mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-25 00:22:43 -05:00
fix: persist refreshed idToken in getAccessToken (#8211)
This commit is contained in:
committed by
GitHub
parent
95d30ddea2
commit
d3f3e63260
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user