From 5d15bf4a0a7bcfa27232ffc73172c7eaa4f35eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paola=20Estefan=C3=ADa=20de=20Campos?= <84341268+Paola3stefania@users.noreply.github.com> Date: Tue, 27 Jan 2026 22:36:05 -0300 Subject: [PATCH] fix(2fa): server-side trust device expiration and configurable maxAge (#7644) Co-authored-by: Alex Yang --- .../src/plugins/two-factor/index.ts | 103 +++- .../src/plugins/two-factor/two-factor.test.ts | 469 +++++++++++++++++- .../src/plugins/two-factor/types.ts | 8 + .../plugins/two-factor/verify-two-factor.ts | 21 +- packages/better-auth/src/state.ts | 4 +- packages/expo/src/routes.ts | 4 +- 6 files changed, 571 insertions(+), 38 deletions(-) diff --git a/packages/better-auth/src/plugins/two-factor/index.ts b/packages/better-auth/src/plugins/two-factor/index.ts index 192459808a..fedfbb46a5 100644 --- a/packages/better-auth/src/plugins/two-factor/index.ts +++ b/packages/better-auth/src/plugins/two-factor/index.ts @@ -9,7 +9,11 @@ import { createOTP } from "@better-auth/utils/otp"; import { APIError } from "better-call"; import * as z from "zod"; import { sessionMiddleware } from "../../api"; -import { deleteSessionCookie, setSessionCookie } from "../../cookies"; +import { + deleteSessionCookie, + expireCookie, + setSessionCookie, +} from "../../cookies"; import { symmetricEncrypt } from "../../crypto"; import { generateRandomString } from "../../crypto/random"; import { mergeSchema } from "../../db/schema"; @@ -51,6 +55,8 @@ export const twoFactor = (options?: O) => { const opts = { twoFactorTable: "twoFactor", }; + const trustDeviceMaxAge = + options?.trustDeviceMaxAge ?? TRUST_DEVICE_COOKIE_MAX_AGE; const backupCodeOptions = { storeBackupCodes: "encrypted", ...options?.backupCodeOptions, @@ -280,6 +286,25 @@ export const twoFactor = (options?: O) => { await ctx.context.internalAdapter.deleteSession( ctx.context.session.session.token, ); + const disableTrustCookie = ctx.context.createAuthCookie( + TRUST_DEVICE_COOKIE_NAME, + { + maxAge: trustDeviceMaxAge, + }, + ); + const disableTrustValue = await ctx.getSignedCookie( + disableTrustCookie.name, + ctx.context.secret, + ); + if (disableTrustValue) { + const [, trustId] = disableTrustValue.split("!"); + if (trustId) { + await ctx.context.internalAdapter.deleteVerificationByIdentifier( + trustId, + ); + } + expireCookie(ctx, disableTrustCookie); + } return ctx.json({ status: true }); }, ), @@ -308,7 +333,7 @@ export const twoFactor = (options?: O) => { const trustDeviceCookieAttrs = ctx.context.createAuthCookie( TRUST_DEVICE_COOKIE_NAME, { - maxAge: TRUST_DEVICE_COOKIE_MAX_AGE, + maxAge: trustDeviceMaxAge, }, ); // Check for trust device cookie @@ -318,36 +343,62 @@ export const twoFactor = (options?: O) => { ); if (trustDeviceCookie) { - const [token, sessionToken] = trustDeviceCookie.split("!"); - const expectedToken = await createHMAC( - "SHA-256", - "base64urlnopad", - ).sign(ctx.context.secret, `${data.user.id}!${sessionToken}`); - - // Checks if the token is signed correctly, not that its the current session token - if (token === expectedToken) { - // Trust device cookie is valid, refresh it and skip 2FA - const newTrustDeviceCookie = ctx.context.createAuthCookie( - TRUST_DEVICE_COOKIE_NAME, - { - maxAge: TRUST_DEVICE_COOKIE_MAX_AGE, - }, - ); - const newToken = await createHMAC( + const [token, trustIdentifier] = trustDeviceCookie.split("!"); + if (token && trustIdentifier) { + const expectedToken = await createHMAC( "SHA-256", "base64urlnopad", ).sign( ctx.context.secret, - `${data.user.id}!${data.session.token}`, + `${data.user.id}!${trustIdentifier}`, ); - await ctx.setSignedCookie( - newTrustDeviceCookie.name, - `${newToken}!${data.session.token}`, - ctx.context.secret, - trustDeviceCookieAttrs.attributes, - ); - return; + + if (token === expectedToken) { + // HMAC is valid; verify the server-side record + const verificationRecord = + await ctx.context.internalAdapter.findVerificationValue( + trustIdentifier, + ); + if ( + verificationRecord && + verificationRecord.value === data.user.id && + verificationRecord.expiresAt > new Date() + ) { + await ctx.context.internalAdapter.deleteVerificationValue( + verificationRecord.id, + ); + const newTrustIdentifier = `trust-device-${generateRandomString(32)}`; + const newToken = await createHMAC( + "SHA-256", + "base64urlnopad", + ).sign( + ctx.context.secret, + `${data.user.id}!${newTrustIdentifier}`, + ); + await ctx.context.internalAdapter.createVerificationValue({ + value: data.user.id, + identifier: newTrustIdentifier, + expiresAt: new Date( + Date.now() + trustDeviceMaxAge * 1000, + ), + }); + const newTrustDeviceCookie = ctx.context.createAuthCookie( + TRUST_DEVICE_COOKIE_NAME, + { + maxAge: trustDeviceMaxAge, + }, + ); + await ctx.setSignedCookie( + newTrustDeviceCookie.name, + `${newToken}!${newTrustIdentifier}`, + ctx.context.secret, + trustDeviceCookieAttrs.attributes, + ); + return; + } + } } + expireCookie(ctx, trustDeviceCookieAttrs); } /** diff --git a/packages/better-auth/src/plugins/two-factor/two-factor.test.ts b/packages/better-auth/src/plugins/two-factor/two-factor.test.ts index 2c33f85e05..46ff745fec 100644 --- a/packages/better-auth/src/plugins/two-factor/two-factor.test.ts +++ b/packages/better-auth/src/plugins/two-factor/two-factor.test.ts @@ -437,7 +437,8 @@ describe("two factor", async () => { }); expect(signInRes.data?.user).toBeDefined(); - // Should still work with original headers + // Old trust device cookie should no longer work because the + // server-side record was rotated on sign-in const signIn2Res = await client.signIn.email({ email: testUser.email, password: testUser.password, @@ -445,9 +446,9 @@ describe("two factor", async () => { headers: newHeaders, }, }); - expect(signIn2Res.data?.user).toBeDefined(); + expect((signIn2Res.data as any)?.twoFactorRedirect).toBe(true); - // Should work with updated headers + // Should work with the refreshed (rotated) headers const signIn3Res = await client.signIn.email({ email: testUser.email, password: testUser.password, @@ -848,6 +849,468 @@ describe("view backup codes", async () => { }); }); +describe("trust device server-side validation", async () => { + let OTP = ""; + const { auth, signInWithTestUser, testUser, db } = await getTestInstance({ + secret: DEFAULT_SECRET, + plugins: [ + twoFactor({ + otpOptions: { + sendOTP({ otp }) { + OTP = otp; + }, + }, + skipVerificationOnEnable: true, + }), + ], + }); + + let { headers } = await signInWithTestUser(); + + it("should force 2FA when server-side trust record is expired", async () => { + // Enable 2FA + const enableRes = await auth.api.enableTwoFactor({ + body: { password: testUser.password }, + headers, + asResponse: true, + }); + headers = convertSetCookieToCookie(enableRes.headers); + + // Sign in to trigger 2FA + const signInRes = await auth.api.signInEmail({ + body: { + email: testUser.email, + password: testUser.password, + }, + asResponse: true, + }); + const twoFactorHeaders = convertSetCookieToCookie(signInRes.headers); + + // Send and verify OTP with trustDevice + await auth.api.sendTwoFactorOTP({ + headers: twoFactorHeaders, + body: {}, + }); + + const verifyRes = await auth.api.verifyTwoFactorOTP({ + headers: twoFactorHeaders, + body: { + code: OTP, + trustDevice: true, + }, + asResponse: true, + }); + expect(verifyRes.status).toBe(200); + + // Extract the trust device cookie + const parsed = parseSetCookieHeader( + verifyRes.headers.get("Set-Cookie") || "", + ); + const trustDeviceCookieValue = parsed.get( + "better-auth.trust_device", + )?.value; + expect(trustDeviceCookieValue).toBeDefined(); + + // The cookie value is signed: "value.signature" where value is "token!trustIdentifier" + // Extract the unsigned value (everything before the last dot) + const lastDotIndex = trustDeviceCookieValue!.lastIndexOf("."); + const unsignedValue = trustDeviceCookieValue!.substring(0, lastDotIndex); + const [, trustIdentifier] = unsignedValue.split("!"); + expect(trustIdentifier).toBeDefined(); + + const verificationRecord = await db.findOne<{ + id: string; + identifier: string; + value: string; + expiresAt: Date; + }>({ + model: "verification", + where: [{ field: "identifier", value: trustIdentifier! }], + }); + expect(verificationRecord).toBeDefined(); + + // Manually expire the record by setting expiresAt to the past + await db.update({ + model: "verification", + where: [{ field: "id", value: verificationRecord!.id }], + update: { + expiresAt: new Date(Date.now() - 1000), // 1 second ago + }, + }); + + // Now sign in with the trust device cookie - should require 2FA again + const trustHeaders = new Headers(); + trustHeaders.set( + "cookie", + `better-auth.trust_device=${trustDeviceCookieValue}`, + ); + + const signIn2Res = await auth.api.signInEmail({ + body: { + email: testUser.email, + password: testUser.password, + }, + headers: trustHeaders, + asResponse: true, + }); + + const signIn2Json = (await signIn2Res.json()) as { + twoFactorRedirect?: boolean; + }; + expect(signIn2Json.twoFactorRedirect).toBe(true); + + // The expired trust cookie should have been cleared + const signIn2Parsed = parseSetCookieHeader( + signIn2Res.headers.get("Set-Cookie") || "", + ); + const clearedTrustCookie = signIn2Parsed.get("better-auth.trust_device"); + expect(clearedTrustCookie?.value).toBe(""); + }); + + it("should preserve trust device after sign-out", async () => { + // Sign in to trigger 2FA + const signInRes = await auth.api.signInEmail({ + body: { + email: testUser.email, + password: testUser.password, + }, + asResponse: true, + }); + const twoFactorHeaders = convertSetCookieToCookie(signInRes.headers); + + // Send and verify OTP with trustDevice + await auth.api.sendTwoFactorOTP({ + headers: twoFactorHeaders, + body: {}, + }); + + const verifyRes = await auth.api.verifyTwoFactorOTP({ + headers: twoFactorHeaders, + body: { + code: OTP, + trustDevice: true, + }, + asResponse: true, + }); + expect(verifyRes.status).toBe(200); + const sessionHeaders = convertSetCookieToCookie(verifyRes.headers); + + // Extract the trust device cookie + const parsed = parseSetCookieHeader( + verifyRes.headers.get("Set-Cookie") || "", + ); + const trustDeviceCookieValue = parsed.get( + "better-auth.trust_device", + )?.value; + expect(trustDeviceCookieValue).toBeDefined(); + + // Extract trust identifier (cookie is signed: "value.signature") + const lastDotIndex = trustDeviceCookieValue!.lastIndexOf("."); + const unsignedValue = trustDeviceCookieValue!.substring(0, lastDotIndex); + const [, trustIdentifier] = unsignedValue.split("!"); + expect(trustIdentifier).toBeDefined(); + + const verificationBefore = await db.findOne<{ + id: string; + identifier: string; + value: string; + expiresAt: Date; + }>({ + model: "verification", + where: [{ field: "identifier", value: trustIdentifier! }], + }); + expect(verificationBefore).toBeDefined(); + + // Sign out + const signOutRes = await auth.api.signOut({ + headers: sessionHeaders, + asResponse: true, + }); + expect(signOutRes.status).toBe(200); + + // Verify the trust device cookie was NOT cleared (trust survives sign-out) + const signOutParsed = parseSetCookieHeader( + signOutRes.headers.get("Set-Cookie") || "", + ); + const trustCookieAfterSignOut = signOutParsed.get( + "better-auth.trust_device", + ); + // Cookie should either not be set (unchanged) or still have its value + expect(trustCookieAfterSignOut?.value || "preserved").not.toBe(""); + + // Verify the DB record still exists + const verificationAfter = await db.findOne<{ + id: string; + identifier: string; + value: string; + expiresAt: Date; + }>({ + model: "verification", + where: [{ field: "identifier", value: trustIdentifier! }], + }); + expect(verificationAfter).toBeDefined(); + + // Sign in again with the trust device cookie - should skip 2FA + const trustHeaders = new Headers(); + trustHeaders.set( + "cookie", + `better-auth.trust_device=${trustDeviceCookieValue}`, + ); + + const signIn2Res = await auth.api.signInEmail({ + body: { + email: testUser.email, + password: testUser.password, + }, + headers: trustHeaders, + asResponse: true, + }); + + const signIn2Json = (await signIn2Res.json()) as { + user?: { email: string }; + }; + // Should NOT require 2FA - trust device survived sign-out + expect(signIn2Json.user?.email).toBe(testUser.email); + }); + + it("should revoke trust device when disabling 2FA", async () => { + // Sign in to trigger 2FA + const signInRes = await auth.api.signInEmail({ + body: { + email: testUser.email, + password: testUser.password, + }, + asResponse: true, + }); + const twoFactorHeaders = convertSetCookieToCookie(signInRes.headers); + + // Send and verify OTP with trustDevice + await auth.api.sendTwoFactorOTP({ + headers: twoFactorHeaders, + body: {}, + }); + + const verifyRes = await auth.api.verifyTwoFactorOTP({ + headers: twoFactorHeaders, + body: { + code: OTP, + trustDevice: true, + }, + asResponse: true, + }); + expect(verifyRes.status).toBe(200); + const sessionHeaders = convertSetCookieToCookie(verifyRes.headers); + + // Extract the trust device cookie + const parsed = parseSetCookieHeader( + verifyRes.headers.get("Set-Cookie") || "", + ); + const trustDeviceCookieValue = parsed.get( + "better-auth.trust_device", + )?.value; + expect(trustDeviceCookieValue).toBeDefined(); + + // Extract trust identifier (cookie is signed: "value.signature") + const lastDotIndex = trustDeviceCookieValue!.lastIndexOf("."); + const unsignedValue = trustDeviceCookieValue!.substring(0, lastDotIndex); + const [, trustIdentifier] = unsignedValue.split("!"); + expect(trustIdentifier).toBeDefined(); + + const verificationBefore = await db.findOne<{ + id: string; + identifier: string; + value: string; + expiresAt: Date; + }>({ + model: "verification", + where: [{ field: "identifier", value: trustIdentifier! }], + }); + expect(verificationBefore).toBeDefined(); + + // Disable 2FA + const disableRes = await auth.api.disableTwoFactor({ + headers: sessionHeaders, + body: { + password: testUser.password, + }, + asResponse: true, + }); + expect(disableRes.status).toBe(200); + + // Verify the trust device cookie was cleared + const disableParsed = parseSetCookieHeader( + disableRes.headers.get("Set-Cookie") || "", + ); + const clearedCookie = disableParsed.get("better-auth.trust_device"); + expect(clearedCookie?.value).toBe(""); + + // Verify the DB record was deleted + const verificationAfter = await db.findOne<{ + id: string; + identifier: string; + value: string; + expiresAt: Date; + }>({ + model: "verification", + where: [{ field: "identifier", value: trustIdentifier! }], + }); + expect(verificationAfter).toBeNull(); + }); +}); + +describe("trustDeviceMaxAge", async () => { + const customMaxAge = 7 * 24 * 60 * 60; // 7 days instead of default 30 + + let OTP = ""; + const { auth, signInWithTestUser, testUser, db } = await getTestInstance({ + secret: DEFAULT_SECRET, + plugins: [ + twoFactor({ + otpOptions: { + sendOTP({ otp }) { + OTP = otp; + }, + }, + skipVerificationOnEnable: true, + trustDeviceMaxAge: customMaxAge, + }), + ], + }); + + let { headers } = await signInWithTestUser(); + + it("should use custom trustDeviceMaxAge for the trust device cookie", async () => { + // Enable 2FA + const enableRes = await auth.api.enableTwoFactor({ + body: { password: testUser.password }, + headers, + asResponse: true, + }); + headers = convertSetCookieToCookie(enableRes.headers); + + // Sign in to trigger 2FA + const signInRes = await auth.api.signInEmail({ + body: { + email: testUser.email, + password: testUser.password, + }, + asResponse: true, + }); + const twoFactorHeaders = convertSetCookieToCookie(signInRes.headers); + + // Send and verify OTP with trustDevice + await auth.api.sendTwoFactorOTP({ + headers: twoFactorHeaders, + body: {}, + }); + + const verifyRes = await auth.api.verifyTwoFactorOTP({ + headers: twoFactorHeaders, + body: { + code: OTP, + trustDevice: true, + }, + asResponse: true, + }); + expect(verifyRes.status).toBe(200); + + // Verify the trust device cookie has the custom max-age + const parsed = parseSetCookieHeader( + verifyRes.headers.get("Set-Cookie") || "", + ); + const trustDeviceCookie = parsed.get("better-auth.trust_device"); + expect(trustDeviceCookie).toBeDefined(); + expect(Number(trustDeviceCookie?.["max-age"])).toBe(customMaxAge); + + // Also verify the DB record has the correct expiration + const trustDeviceCookieValue = trustDeviceCookie?.value; + // Extract trust identifier (cookie is signed: "value.signature") + const lastDotIndex = trustDeviceCookieValue!.lastIndexOf("."); + const unsignedValue = trustDeviceCookieValue!.substring(0, lastDotIndex); + const [, trustIdentifier] = unsignedValue.split("!"); + expect(trustIdentifier).toBeDefined(); + + const verificationRecord = await db.findOne<{ + id: string; + identifier: string; + value: string; + expiresAt: Date; + }>({ + model: "verification", + where: [{ field: "identifier", value: trustIdentifier! }], + }); + expect(verificationRecord).toBeDefined(); + + // Expiration should be approximately customMaxAge seconds from now + const expiresAt = new Date(verificationRecord!.expiresAt).getTime(); + const expectedExpiry = Date.now() + customMaxAge * 1000; + // Allow 5 second tolerance for test execution time + expect(Math.abs(expiresAt - expectedExpiry)).toBeLessThan(5000); + }); + + it("should use default 30 days when trustDeviceMaxAge is not specified", async () => { + let defaultOTP = ""; + const { auth: authDefault, signInWithTestUser: signInDefault } = + await getTestInstance({ + secret: DEFAULT_SECRET, + plugins: [ + twoFactor({ + otpOptions: { + sendOTP({ otp }) { + defaultOTP = otp; + }, + }, + skipVerificationOnEnable: true, + }), + ], + }); + + const { headers: defaultHeaders } = await signInDefault(); + + // Enable 2FA + await authDefault.api.enableTwoFactor({ + body: { password: testUser.password }, + headers: defaultHeaders, + asResponse: true, + }); + + // Sign in to trigger 2FA + const signInRes = await authDefault.api.signInEmail({ + body: { + email: testUser.email, + password: testUser.password, + }, + asResponse: true, + }); + const twoFactorHeaders = convertSetCookieToCookie(signInRes.headers); + + // Send and verify OTP with trustDevice + await authDefault.api.sendTwoFactorOTP({ + headers: twoFactorHeaders, + body: {}, + }); + + const verifyRes = await authDefault.api.verifyTwoFactorOTP({ + headers: twoFactorHeaders, + body: { + code: defaultOTP, + trustDevice: true, + }, + asResponse: true, + }); + expect(verifyRes.status).toBe(200); + + // Verify the trust device cookie has the default max-age (30 days) + const parsed = parseSetCookieHeader( + verifyRes.headers.get("Set-Cookie") || "", + ); + const trustDeviceCookie = parsed.get("better-auth.trust_device"); + expect(trustDeviceCookie).toBeDefined(); + // Default is 30 days = 30 * 24 * 60 * 60 = 2592000 seconds + expect(Number(trustDeviceCookie?.["max-age"])).toBe(30 * 24 * 60 * 60); + }); +}); + describe("twoFactorCookieMaxAge", async () => { const customMaxAge = 15 * 60; // 15 minutes const { auth, signInWithTestUser, testUser } = await getTestInstance({ diff --git a/packages/better-auth/src/plugins/two-factor/types.ts b/packages/better-auth/src/plugins/two-factor/types.ts index 7246f476c8..2937a641a8 100644 --- a/packages/better-auth/src/plugins/two-factor/types.ts +++ b/packages/better-auth/src/plugins/two-factor/types.ts @@ -39,6 +39,14 @@ export interface TwoFactorOptions { * @default 600 (10 minutes) */ twoFactorCookieMaxAge?: number | undefined; + /** + * Maximum age (in seconds) for the trusted device cookie. + * When a user opts to trust a device, this controls how long + * the device stays trusted before requiring 2FA again. + * + * @default 2592000 (30 days) + */ + trustDeviceMaxAge?: number | undefined; } export interface UserWithTwoFactor extends User { diff --git a/packages/better-auth/src/plugins/two-factor/verify-two-factor.ts b/packages/better-auth/src/plugins/two-factor/verify-two-factor.ts index f9b7ba6224..7a7e90543f 100644 --- a/packages/better-auth/src/plugins/two-factor/verify-two-factor.ts +++ b/packages/better-auth/src/plugins/two-factor/verify-two-factor.ts @@ -3,6 +3,7 @@ import { createHMAC } from "@better-auth/utils/hmac"; import { APIError } from "better-call"; import { getSessionFromCtx } from "../../api"; import { expireCookie, setSessionCookie } from "../../cookies"; +import { generateRandomString } from "../../crypto/random"; import { parseUserOutput } from "../../db/schema"; import { TRUST_DEVICE_COOKIE_MAX_AGE, @@ -76,23 +77,33 @@ export async function verifyTwoFactor(ctx: GenericEndpointContext) { // Always clear the two factor cookie after successful verification expireCookie(ctx, twoFactorCookie); if (ctx.body.trustDevice) { + const plugin = ctx.context.getPlugin("two-factor"); + const trustDeviceMaxAge = plugin!.options!.trustDeviceMaxAge; + const maxAge = trustDeviceMaxAge ?? TRUST_DEVICE_COOKIE_MAX_AGE; const trustDeviceCookie = ctx.context.createAuthCookie( TRUST_DEVICE_COOKIE_NAME, { - maxAge: TRUST_DEVICE_COOKIE_MAX_AGE, + maxAge, }, ); /** - * create a token that will be used to - * verify the device + * Create a random identifier for the trust device record. + * Store it in the verification table with an expiration + * so the server can validate and revoke it. */ + const trustIdentifier = `trust-device-${generateRandomString(32)}`; const token = await createHMAC("SHA-256", "base64urlnopad").sign( ctx.context.secret, - `${user.id}!${session.token}`, + `${user.id}!${trustIdentifier}`, ); + await ctx.context.internalAdapter.createVerificationValue({ + value: user.id, + identifier: trustIdentifier, + expiresAt: new Date(Date.now() + maxAge * 1000), + }); await ctx.setSignedCookie( trustDeviceCookie.name, - `${token}!${session.token}`, + `${token}!${trustIdentifier}`, ctx.context.secret, trustDeviceCookie.attributes, ); diff --git a/packages/better-auth/src/state.ts b/packages/better-auth/src/state.ts index 844e9c6c4a..1d83880f0d 100644 --- a/packages/better-auth/src/state.ts +++ b/packages/better-auth/src/state.ts @@ -67,7 +67,7 @@ export async function generateGenericState( const stateCookie = c.context.createAuthCookie( settings?.cookieName ?? "oauth_state", { - maxAge: 10 * 60 * 1000, // 10 minutes + maxAge: 10 * 60, // 10 minutes }, ); @@ -84,7 +84,7 @@ export async function generateGenericState( const stateCookie = c.context.createAuthCookie( settings?.cookieName ?? "state", { - maxAge: 5 * 60 * 1000, // 5 minutes + maxAge: 5 * 60, // 5 minutes }, ); diff --git a/packages/expo/src/routes.ts b/packages/expo/src/routes.ts index 7946533d85..42d93fe051 100644 --- a/packages/expo/src/routes.ts +++ b/packages/expo/src/routes.ts @@ -16,7 +16,7 @@ export const expoAuthorizationProxy = createAuthEndpoint( const { oauthState } = ctx.query; if (oauthState) { const oauthStateCookie = ctx.context.createAuthCookie("oauth_state", { - maxAge: 10 * 60 * 1000, // 10 minutes + maxAge: 10 * 60, // 10 minutes }); ctx.setCookie( oauthStateCookie.name, @@ -35,7 +35,7 @@ export const expoAuthorizationProxy = createAuthEndpoint( }); } const stateCookie = ctx.context.createAuthCookie("state", { - maxAge: 5 * 60 * 1000, // 5 minutes + maxAge: 5 * 60, // 5 minutes }); await ctx.setSignedCookie( stateCookie.name,