Skip to content

Commit d008b9b

Browse files
authored
Merge commit from fork
getSession() calls @auth/core, which returns a 500 with a JSON body of { message: "There was a problem with the server configuration..." } when the provider configuration is invalid (e.g. a provider missing both `issuer` and `authorization` endpoints). The next-auth wrappers read that body with .json() without checking the response status, so the error object was assigned to req.auth. Truthiness checks such as `!!auth` / `if (!req.auth)` - the pattern shown in the docs - then evaluated as authenticated for every request, failing open and exposing protected routes during a misconfiguration. Parse the session response through a helper that returns null on any non-OK response, so all auth() entry points (RSC, middleware inline/wrapper, API routes) fail closed. Add a regression test reproducing the InvalidEndpoints misconfiguration. Fixes GHSA-8fpg-xm3f-6cx3
1 parent af42d8f commit d008b9b

2 files changed

Lines changed: 90 additions & 7 deletions

File tree

packages/next-auth/src/lib/index.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,23 @@ async function getSession(headers: Headers, config: NextAuthConfig) {
9696
}) as Promise<Response>
9797
}
9898

99+
/**
100+
* Reads the session payload from a `getSession()` response.
101+
*
102+
* On any non-OK response (e.g. a `500` returned by `@auth/core` when the
103+
* provider configuration is invalid), the body is an error object such as
104+
* `{ message: "There was a problem with the server configuration..." }`.
105+
* Returning that object as the session would make truthiness checks like
106+
* `!!auth` pass for everyone, failing open. Treat any non-OK response as
107+
* "no session" so auth checks fail closed.
108+
*/
109+
async function parseSessionResponse(
110+
response: Response
111+
): Promise<Session | null> {
112+
if (!response.ok) return null
113+
return (await response.json()) satisfies Session | null
114+
}
115+
99116
export interface NextAuthRequest extends NextRequest {
100117
auth: Session | null
101118
}
@@ -131,7 +148,7 @@ export function initAuth(
131148
const _config = await config(undefined) // Review: Should we pass headers() here instead?
132149
onLazyLoad?.(_config)
133150

134-
return getSession(_headers, _config).then((r) => r.json())
151+
return getSession(_headers, _config).then(parseSessionResponse)
135152
}
136153

137154
if (args[0] instanceof Request) {
@@ -168,14 +185,14 @@ export function initAuth(
168185
// @ts-expect-error -- request is NextRequest
169186
return getSession(new Headers(request.headers), _config).then(
170187
async (authResponse) => {
171-
const auth = await authResponse.json()
188+
const auth = await parseSessionResponse(authResponse)
172189

173190
for (const cookie of authResponse.headers.getSetCookie())
174191
if ("headers" in response)
175192
response.headers.append("set-cookie", cookie)
176193
else response.appendHeader("set-cookie", cookie)
177194

178-
return auth satisfies Session | null
195+
return auth
179196
}
180197
)
181198
}
@@ -184,7 +201,7 @@ export function initAuth(
184201
if (!args.length) {
185202
// React Server Components
186203
return Promise.resolve(headers()).then((h: Headers) =>
187-
getSession(h, config).then((r) => r.json())
204+
getSession(h, config).then(parseSessionResponse)
188205
)
189206
}
190207
if (args[0] instanceof Request) {
@@ -218,13 +235,13 @@ export function initAuth(
218235
new Headers(request.headers),
219236
config
220237
).then(async (authResponse) => {
221-
const auth = await authResponse.json()
238+
const auth = await parseSessionResponse(authResponse)
222239

223240
for (const cookie of authResponse.headers.getSetCookie())
224241
if ("headers" in response) response.headers.append("set-cookie", cookie)
225242
else response.appendHeader("set-cookie", cookie)
226243

227-
return auth satisfies Session | null
244+
return auth
228245
})
229246
}
230247
}
@@ -236,7 +253,7 @@ async function handleAuth(
236253
) {
237254
const request = reqWithEnvURL(args[0])
238255
const sessionResponse = await getSession(request.headers, config)
239-
const auth = await sessionResponse.json()
256+
const auth = await parseSessionResponse(sessionResponse)
240257

241258
let authorized: boolean | NextResponse | Response | undefined = true
242259

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
2+
import NextAuth, { type NextAuthConfig } from "../src"
3+
4+
// A provider with neither `issuer` nor an `authorization` endpoint makes
5+
// `@auth/core` bail out of every request with a `500 { message }` response.
6+
// See GHSA-8fpg-xm3f-6cx3.
7+
const brokenConfig: NextAuthConfig = {
8+
providers: [
9+
{
10+
id: "broken",
11+
name: "Broken",
12+
type: "oidc",
13+
clientId: "client-id",
14+
clientSecret: "client-secret",
15+
},
16+
],
17+
}
18+
19+
let mockedHeaders = vi.hoisted(() => new globalThis.Headers())
20+
21+
vi.mock("next/headers", async (importOriginal) => {
22+
const originalModule = await importOriginal<typeof import("next/headers")>()
23+
return {
24+
...originalModule,
25+
headers: () => mockedHeaders,
26+
}
27+
})
28+
29+
describe("auth checks fail closed on provider configuration errors", () => {
30+
beforeEach(() => {
31+
mockedHeaders = new globalThis.Headers()
32+
process.env.AUTH_SECRET = "secret"
33+
process.env.AUTH_URL = "https://app.example.com/api/auth"
34+
})
35+
36+
afterEach(() => {
37+
vi.clearAllMocks()
38+
delete process.env.AUTH_SECRET
39+
delete process.env.AUTH_URL
40+
})
41+
42+
it("returns null instead of an error object from the RSC `auth()` call", async () => {
43+
const { auth } = NextAuth(brokenConfig)
44+
45+
const session = await auth()
46+
47+
// Before the fix, the 500 error body `{ message: "..." }` was returned
48+
// verbatim, making `!!auth` truthy for everyone (fail open).
49+
expect(session).toBeNull()
50+
})
51+
52+
it("sets `req.auth` to null in the middleware wrapper", async () => {
53+
const { auth } = NextAuth(brokenConfig)
54+
55+
let observed: unknown = "unset"
56+
const handler = auth((req) => {
57+
observed = req.auth
58+
})
59+
60+
const req: any = new Request("https://app.example.com/dashboard")
61+
req.nextUrl = new URL("https://app.example.com/dashboard")
62+
await handler(req, {} as any)
63+
64+
expect(observed).toBeNull()
65+
})
66+
})

0 commit comments

Comments
 (0)