diff --git a/packages/core/src/adapters.ts b/packages/core/src/adapters.ts index 43c4800870..f84c9294a7 100644 --- a/packages/core/src/adapters.ts +++ b/packages/core/src/adapters.ts @@ -360,6 +360,10 @@ export interface Adapter { /** * Updates a session in the database and returns it. * + * :::tip + * If an `AdapterSession` is returned, it will be used in place of the original session. + * ::: + * * See also [Database Session management](https://authjs.dev/guides/creating-a-database-adapter#database-session-management) */ updateSession?( diff --git a/packages/core/src/lib/actions/session.ts b/packages/core/src/lib/actions/session.ts index cd2d815cde..78bbc126e3 100644 --- a/packages/core/src/lib/actions/session.ts +++ b/packages/core/src/lib/actions/session.ts @@ -1,7 +1,7 @@ import { JWTSessionError, SessionTokenError } from "../../errors.js" import { fromDate } from "../utils/date.js" -import type { Adapter } from "../../adapters.js" +import type { Adapter, AdapterSession } from "../../adapters.js" import type { InternalOptions, ResponseInternal, Session } from "../../types.js" import type { Cookie, SessionStore } from "../utils/cookie.js" @@ -119,10 +119,14 @@ export async function session( sessionUpdateAge * 1000 const newExpires = fromDate(sessionMaxAge) + + let newAdapterSession: AdapterSession | null | undefined = null + // Trigger update of session expiry date and write to database, only - // if the session was last updated more than {sessionUpdateAge} ago + // if the session was last updated more than {sessionUpdateAge} ago. + // Use new `AdapterSession` if returned. if (sessionIsDueToBeUpdatedDate <= Date.now()) { - await updateSession({ + newAdapterSession = await updateSession({ sessionToken: sessionToken, expires: newExpires, }) @@ -133,7 +137,7 @@ export async function session( // TODO: user already passed below, // remove from session object in https://github.com/nextauthjs/next-auth/pull/9702 // @ts-expect-error - session: { ...session, user }, + session: { ...(newAdapterSession ? newAdapterSession : session), user }, user, newSession, ...(isUpdate ? { trigger: "update" } : {}), @@ -145,10 +149,12 @@ export async function session( // Set cookie again to update expiry response.cookies?.push({ name: options.cookies.sessionToken.name, - value: sessionToken, + value: newAdapterSession + ? newAdapterSession.sessionToken + : sessionToken, options: { ...options.cookies.sessionToken.options, - expires: newExpires, + expires: newAdapterSession ? newAdapterSession.expires : newExpires, }, }) diff --git a/packages/core/test/actions/session.test.ts b/packages/core/test/actions/session.test.ts index 314d948116..3ce3905542 100644 --- a/packages/core/test/actions/session.test.ts +++ b/packages/core/test/actions/session.test.ts @@ -273,5 +273,80 @@ describe("assert GET session action", () => { assertNoCacheResponseHeaders(response) }) + + it("should return a new session if returned from the adapter", async () => { + vi.spyOn(callbacks, "jwt") + vi.spyOn(callbacks, "session") + const updatedExpires = getExpires() + const currentExpires = getExpires(24 * 60 * 60 * 1000) // 1 day from now + + const expectedSessionToken = randomString(32) + const expectedNewSessionToken = randomString(32) + const expectedUserId = randomString(32) + const expectedUser = { + id: expectedUserId, + name: "test", + email: "test@test.com", + image: "https://test.com/test.png", + emailVerified: null, + } satisfies AdapterUser + + // const expectedUserId = randomString(32) + const memory = initMemory() + memory.users.set(expectedUserId, expectedUser) + memory.sessions.set(expectedSessionToken, { + sessionToken: expectedSessionToken, + userId: expectedUserId, + expires: currentExpires, + }) + + const adapter = MemoryAdapter(memory, expectedNewSessionToken) + + const { response } = await makeAuthRequest({ + action: "session", + cookies: { + [SESSION_COOKIE_NAME]: expectedSessionToken, + }, + config: { + adapter, + }, + }) + + const actualBodySession = await response.json() + + let cookies = response.headers.getSetCookie().reduce((acc, cookie) => { + return { ...acc, ...parseCookie(cookie) } + }, {}) + const actualSessionToken = cookies[SESSION_COOKIE_NAME] + + expect(memory.users.get(expectedUserId)).toEqual(expectedUser) + expect(memory.sessions.get(expectedSessionToken)).toEqual({ + sessionToken: expectedNewSessionToken, + userId: expectedUserId, + expires: updatedExpires, + }) + + expect(callbacks.session).toHaveBeenCalledWith({ + newSession: undefined, + session: { + user: expectedUser, + expires: currentExpires, + sessionToken: expectedNewSessionToken, + userId: expectedUserId, + }, + user: expectedUser, + }) + expect(callbacks.jwt).not.toHaveBeenCalled() + + expect(actualSessionToken).toEqual(expectedNewSessionToken) + expect(actualBodySession.user).toEqual({ + image: expectedUser.image, + name: expectedUser.name, + email: expectedUser.email, + }) + expect(actualBodySession.expires).toEqual(currentExpires.toISOString()) + + assertNoCacheResponseHeaders(response) + }) }) }) diff --git a/packages/core/test/memory-adapter.ts b/packages/core/test/memory-adapter.ts index c8cedae459..8b525485d9 100644 --- a/packages/core/test/memory-adapter.ts +++ b/packages/core/test/memory-adapter.ts @@ -33,7 +33,10 @@ export function initMemory(): Memory { } } -export function MemoryAdapter(memory?: Memory): Adapter { +export function MemoryAdapter( + memory?: Memory, + newSessionToken: string | null = null +): Adapter { const { users, accounts, sessions, verificationTokens } = memory ?? initMemory() @@ -169,6 +172,16 @@ export function MemoryAdapter(memory?: Memory): Adapter { if (!currentSession) throw new Error("Session not found") const updatedSession = { ...currentSession, ...session } + if (newSessionToken) { + // Delete old session token if a new one is provided + sessions.delete(session.sessionToken) + // Create a new session with the new session token + updatedSession.sessionToken = newSessionToken + sessions.set(newSessionToken, updatedSession) + + return updatedSession + } + sessions.set(session.sessionToken, updatedSession) return updatedSession