diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 66b24a92a..b80a6b803 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -175,8 +175,6 @@ export const authOptions: NextAuthOptions = { return token; }, async session({ session, token }) { - if (typeof token.accessToken === "string") - session.accessToken = token.accessToken; if (typeof token.githubId === "string") session.githubId = token.githubId; if (typeof token.githubLogin === "string") diff --git a/src/lib/get-session-token.ts b/src/lib/get-session-token.ts index a9a27e320..6bfb6115e 100644 --- a/src/lib/get-session-token.ts +++ b/src/lib/get-session-token.ts @@ -1,5 +1,7 @@ import "server-only"; import { getServerSession } from "next-auth"; +import { getToken } from "next-auth/jwt"; +import { headers, cookies } from "next/headers"; import { authOptions } from "@/lib/auth"; import { resolveAppUser } from "@/lib/resolve-user"; import type { Session } from "next-auth"; @@ -13,11 +15,24 @@ export async function getSessionWithToken(): Promise { const session = await getServerSession(authOptions); if (!session?.githubId || !session?.githubLogin) return null; - const accessToken = session.accessToken; + // accessToken no longer lives on `session` (see auth.ts session callback). + // Read it server-side, directly from the encrypted JWT cookie instead. + const token = await getToken({ + req: { headers: headers(), cookies: cookies() } as any, + secret: process.env.NEXTAUTH_SECRET, + }); + const accessToken = typeof token?.accessToken === "string" ? token.accessToken : null; if (!accessToken) return null; const userRow = await resolveAppUser(session.githubId, session.githubLogin); if (!userRow) return null; return { session, accessToken }; -} \ No newline at end of file +} +export async function getAccessToken(): Promise { + const token = await getToken({ + req: { headers: headers(), cookies: cookies() } as any, + secret: process.env.NEXTAUTH_SECRET, + }); + return typeof token?.accessToken === "string" ? token.accessToken : null; +} diff --git a/test/auth.test.ts b/test/auth.test.ts index ce54f3046..73854bfca 100644 --- a/test/auth.test.ts +++ b/test/auth.test.ts @@ -162,7 +162,7 @@ describe('auth.ts NextAuth callbacks', () => { }); describe('session callback', () => { - it('populates session.accessToken from token.jwt', async () => { + it('does NOT expose accessToken on the session object (security: token must stay server-side)', async () => { const sessionCallback = authOptions.callbacks?.session; if (!sessionCallback) return; @@ -170,7 +170,9 @@ describe('auth.ts NextAuth callbacks', () => { const token = { accessToken: 'jwt-token-xyz', githubId: '111', githubLogin: 'user1' } as any; const result = await sessionCallback({ session, token, user: {} } as any); - expect((result as any).accessToken).toBe('jwt-token-xyz'); + // session is exposed to client-side code via useSession()/getSession(), + // so the raw GitHub token must never be copied onto it. + expect((result as any).accessToken).toBeUndefined(); }); it('populates session.githubId from token.jwt', async () => {