Skip to content

Commit 7e91d7d

Browse files
refactor(core): use standard Request and Response (#4769)
* WIP use `Request` and `Response` for core * bump Next.js * rename ts types * refactor * simplify * upgrade Next.js * implement body reader * use `Request`/`Response` in `next-auth/next` * make linter happy * revert * fix tests * remove workaround for middleware return type * return session in protected api route example * don't export internal handler * fall back host to localhost * refactor `getBody` * refactor `next-auth/next` * chore: add `@edge-runtime/jest-environment` * fix tests, using Node 18 as runtime * fix test * remove patch * fix neo4j build * remove new-line * reduce file changes in the PR * fix tests * fix tests * refactor * refactor * add host tests * refactor tests * fix body reading * fix tests * use 302 * fix test * fix again * fix tests * handle when body is `Buffer` * move comment
1 parent c142413 commit 7e91d7d

File tree

17 files changed

+588
-305
lines changed

17 files changed

+588
-305
lines changed

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

Lines changed: 25 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import logger, { setLogger } from "../utils/logger"
2-
import { detectHost } from "../utils/detect-host"
2+
import { toInternalRequest, toResponse } from "../utils/web"
33
import * as routes from "./routes"
44
import renderPage from "./pages"
55
import { init } from "./init"
@@ -9,7 +9,6 @@ import { SessionStore } from "./lib/cookie"
99
import type { NextAuthAction, NextAuthOptions } from "./types"
1010
import type { Cookie } from "./lib/cookie"
1111
import type { ErrorType } from "./pages/error"
12-
import { parse as parseCookie } from "cookie"
1312

1413
export interface RequestInternal {
1514
/** @default "http://localhost:3000" */
@@ -29,6 +28,7 @@ export interface NextAuthHeader {
2928
value: string
3029
}
3130

31+
// TODO: Rename to `ResponseInternal`
3232
export interface OutgoingResponse<
3333
Body extends string | Record<string, any> | any[] = any
3434
> {
@@ -39,56 +39,15 @@ export interface OutgoingResponse<
3939
cookies?: Cookie[]
4040
}
4141

42-
export interface NextAuthHandlerParams {
43-
req: Request | RequestInternal
44-
options: NextAuthOptions
45-
}
46-
47-
async function getBody(req: Request): Promise<Record<string, any> | undefined> {
48-
try {
49-
return await req.json()
50-
} catch {}
51-
}
52-
53-
// TODO:
54-
async function toInternalRequest(
55-
req: RequestInternal | Request,
56-
trustHost: boolean = false
57-
): Promise<RequestInternal> {
58-
if (req instanceof Request) {
59-
const url = new URL(req.url)
60-
// TODO: handle custom paths?
61-
const nextauth = url.pathname.split("/").slice(3)
62-
const headers = Object.fromEntries(req.headers)
63-
const query: Record<string, any> = Object.fromEntries(url.searchParams)
64-
query.nextauth = nextauth
65-
66-
return {
67-
action: nextauth[0] as NextAuthAction,
68-
method: req.method,
69-
headers,
70-
body: await getBody(req),
71-
cookies: parseCookie(req.headers.get("cookie") ?? ""),
72-
providerId: nextauth[1],
73-
error: url.searchParams.get("error") ?? nextauth[1],
74-
host: detectHost(
75-
trustHost,
76-
headers["x-forwarded-host"] ?? headers.host,
77-
"http://localhost:3000"
78-
),
79-
query,
80-
}
81-
}
82-
return req
83-
}
84-
85-
export async function NextAuthHandler<
42+
async function AuthHandlerInternal<
8643
Body extends string | Record<string, any> | any[]
87-
>(params: NextAuthHandlerParams): Promise<OutgoingResponse<Body>> {
88-
const { options: userOptions, req: incomingRequest } = params
89-
90-
const req = await toInternalRequest(incomingRequest, userOptions.trustHost)
91-
44+
>(params: {
45+
req: RequestInternal
46+
options: NextAuthOptions
47+
/** REVIEW: Is this the best way to skip parsing the body in Node.js? */
48+
parsedBody?: any
49+
}): Promise<OutgoingResponse<Body>> {
50+
const { options: userOptions, req } = params
9251
setLogger(userOptions.logger, userOptions.debug)
9352

9453
const assertionResult = assertConfig({ options: userOptions, req })
@@ -159,6 +118,7 @@ export async function NextAuthHandler<
159118
case "session": {
160119
const session = await routes.session({ options, sessionStore })
161120
if (session.cookies) cookies.push(...session.cookies)
121+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
162122
return { ...session, cookies } as any
163123
}
164124
case "csrf":
@@ -299,3 +259,17 @@ export async function NextAuthHandler<
299259
body: `Error: This action with HTTP ${method} is not supported by NextAuth.js` as any,
300260
}
301261
}
262+
263+
/**
264+
* The core functionality of `next-auth`.
265+
* It receives a standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
266+
* and returns a standard [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).
267+
*/
268+
export async function AuthHandler(
269+
request: Request,
270+
options: NextAuthOptions
271+
): Promise<Response> {
272+
const req = await toInternalRequest(request)
273+
const internalResponse = await AuthHandlerInternal({ req, options })
274+
return toResponse(internalResponse)
275+
}

packages/next-auth/src/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
export * from "./core/types"
22

3-
export type { RequestInternal, OutgoingResponse } from "./core"
4-
53
export * from "./next"
64
export { default } from "./next"
Lines changed: 56 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { NextAuthHandler } from "../core"
2-
import { detectHost } from "../utils/detect-host"
3-
import { setCookie } from "./utils"
1+
import { AuthHandler } from "../core"
2+
import { getURL, getBody } from "../utils/node"
43

54
import type {
65
GetServerSidePropsContext,
@@ -10,60 +9,49 @@ import type {
109
import type { NextAuthOptions, Session } from ".."
1110
import type {
1211
CallbacksOptions,
13-
NextAuthAction,
1412
NextAuthRequest,
1513
NextAuthResponse,
1614
} from "../core/types"
1715

18-
async function NextAuthNextHandler(
16+
async function NextAuthHandler(
1917
req: NextApiRequest,
2018
res: NextApiResponse,
2119
options: NextAuthOptions
2220
) {
23-
const { nextauth, ...query } = req.query
24-
25-
options.secret ??= options.jwt?.secret ?? process.env.NEXTAUTH_SECRET
26-
options.trustHost ??= !!(process.env.AUTH_TRUST_HOST ?? process.env.VERCEL)
27-
28-
const handler = await NextAuthHandler({
29-
req: {
30-
host: detectHost(
31-
options.trustHost,
32-
req.headers["x-forwarded-host"],
33-
process.env.NEXTAUTH_URL ??
34-
(process.env.NODE_ENV !== "production" && "http://localhost:3000")
35-
),
36-
body: req.body,
37-
query,
38-
cookies: req.cookies,
39-
headers: req.headers,
40-
method: req.method,
41-
action: nextauth?.[0] as NextAuthAction,
42-
providerId: nextauth?.[1],
43-
error: (req.query.error as string | undefined) ?? nextauth?.[1],
44-
},
45-
options,
21+
const url = getURL(
22+
req.url,
23+
options.trustHost,
24+
req.headers["x-forwarded-host"] ?? req.headers.host
25+
)
26+
27+
if (url instanceof Error) return res.status(400).end()
28+
29+
const request = new Request(url, {
30+
headers: new Headers(req.headers as any),
31+
method: req.method,
32+
...getBody(req),
4633
})
4734

48-
res.status(handler.status ?? 200)
35+
options.secret ??= options.jwt?.secret ?? process.env.NEXTAUTH_SECRET
36+
const response = await AuthHandler(request, options)
37+
const { status, headers } = response
38+
res.status(status)
4939

50-
handler.cookies?.forEach((cookie) => setCookie(res, cookie))
40+
for (const [key, val] of headers.entries()) {
41+
const value = key === "set-cookie" ? val.split(",") : val
42+
res.setHeader(key, value)
43+
}
5144

52-
handler.headers?.forEach((h) => res.setHeader(h.key, h.value))
45+
// If the request expects a return URL, send it as JSON
46+
// instead of doing an actual redirect.
47+
const redirect = headers.get("Location")
5348

54-
if (handler.redirect) {
55-
// If the request expects a return URL, send it as JSON
56-
// instead of doing an actual redirect.
57-
if (req.body?.json !== "true") {
58-
// Could chain. .end() when lowest target is Node 14
59-
// https://github.com/nodejs/node/issues/33148
60-
res.status(302).setHeader("Location", handler.redirect)
61-
return res.end()
62-
}
63-
return res.json({ url: handler.redirect })
49+
if (req.body?.json === "true" && redirect) {
50+
res.removeHeader("Location")
51+
return res.json({ url: redirect })
6452
}
6553

66-
return res.send(handler.body)
54+
return res.send(await response.text())
6755
}
6856

6957
function NextAuth(options: NextAuthOptions): any
@@ -81,10 +69,10 @@ function NextAuth(
8169
) {
8270
if (args.length === 1) {
8371
return async (req: NextAuthRequest, res: NextAuthResponse) =>
84-
await NextAuthNextHandler(req, res, args[0])
72+
await NextAuthHandler(req, res, args[0])
8573
}
8674

87-
return NextAuthNextHandler(args[0], args[1], args[2])
75+
return NextAuthHandler(args[0], args[1], args[2])
8876
}
8977

9078
export default NextAuth
@@ -93,7 +81,7 @@ let experimentalWarningShown = false
9381
let experimentalRSCWarningShown = false
9482

9583
type GetServerSessionOptions = Partial<Omit<NextAuthOptions, "callbacks">> & {
96-
callbacks?: Omit<NextAuthOptions['callbacks'], "session"> & {
84+
callbacks?: Omit<NextAuthOptions["callbacks"], "session"> & {
9785
session?: (...args: Parameters<CallbacksOptions["session"]>) => any
9886
}
9987
}
@@ -156,47 +144,34 @@ export async function unstable_getServerSession<
156144
options = Object.assign(args[2], { providers: [] })
157145
}
158146

159-
options.secret ??= process.env.NEXTAUTH_SECRET
160-
options.trustHost ??= !!(process.env.AUTH_TRUST_HOST ?? process.env.VERCEL)
161-
162-
const session = await NextAuthHandler<Session | {} | string>({
163-
options,
164-
req: {
165-
host: detectHost(
166-
options.trustHost,
167-
req.headers["x-forwarded-host"],
168-
process.env.NEXTAUTH_URL ??
169-
(process.env.NODE_ENV !== "production" && "http://localhost:3000")
170-
),
171-
action: "session",
172-
method: "GET",
173-
cookies: req.cookies,
174-
headers: req.headers,
175-
},
176-
})
147+
const urlOrError = getURL(
148+
"/api/auth/session",
149+
options.trustHost,
150+
req.headers["x-forwarded-host"] ?? req.headers.host
151+
)
152+
153+
if (urlOrError instanceof Error) throw urlOrError
177154

178-
const { body, cookies, status = 200 } = session
155+
options.secret ??= process.env.NEXTAUTH_SECRET
156+
const response = await AuthHandler(
157+
new Request(urlOrError, { headers: req.headers }),
158+
options
159+
)
179160

180-
cookies?.forEach((cookie) => setCookie(res, cookie))
161+
const { status = 200, headers } = response
181162

182-
if (body && typeof body !== "string" && Object.keys(body).length) {
183-
if (status === 200) {
184-
// @ts-expect-error
185-
if (isRSC) delete body.expires
186-
return body as R
187-
}
188-
throw new Error((body as any).message)
163+
for (const [key, val] of headers.entries()) {
164+
const value = key === "set-cookie" ? val.split(",") : val
165+
res.setHeader(key, value)
189166
}
190167

191-
return null
192-
}
168+
const data = await response.json()
193169

194-
declare global {
195-
// eslint-disable-next-line @typescript-eslint/no-namespace
196-
namespace NodeJS {
197-
interface ProcessEnv {
198-
NEXTAUTH_URL?: string
199-
VERCEL?: "1"
200-
}
170+
if (!data || !Object.keys(data).length) return null
171+
172+
if (status === 200) {
173+
if (isRSC) delete data.expires
174+
return data as R
201175
}
176+
throw new Error(data.message)
202177
}

packages/next-auth/src/next/middleware.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { NextResponse, NextRequest } from "next/server"
66

77
import { getToken } from "../jwt"
88
import parseUrl from "../utils/parse-url"
9-
import { detectHost } from "../utils/detect-host"
9+
import { getURL } from "../utils/node"
1010

1111
type AuthorizedCallback = (params: {
1212
token: JWT | null
@@ -103,14 +103,10 @@ export interface NextAuthMiddlewareOptions {
103103
trustHost?: NextAuthOptions["trustHost"]
104104
}
105105

106-
// TODO: `NextMiddleware` should allow returning `void`
107-
// Simplify when https://github.com/vercel/next.js/pull/38625 is merged.
108-
type NextMiddlewareResult = ReturnType<NextMiddleware> | void // eslint-disable-line @typescript-eslint/no-invalid-void-type
109-
110106
async function handleMiddleware(
111107
req: NextRequest,
112108
options: NextAuthMiddlewareOptions | undefined = {},
113-
onSuccess?: (token: JWT | null) => Promise<NextMiddlewareResult>
109+
onSuccess?: (token: JWT | null) => ReturnType<NextMiddleware>
114110
) {
115111
const { pathname, search, origin, basePath } = req.nextUrl
116112

@@ -121,13 +117,15 @@ async function handleMiddleware(
121117
options.trustHost ?? process.env.VERCEL ?? process.env.AUTH_TRUST_HOST
122118
)
123119

124-
const host = detectHost(
120+
let authPath
121+
const url = getURL(
122+
null,
125123
options.trustHost,
126-
req.headers.get("x-forwarded-host"),
127-
process.env.NEXTAUTH_URL ??
128-
(process.env.NODE_ENV !== "production" && "http://localhost:3000")
124+
req.headers.get("x-forwarded-host") ?? req.headers.get("host")
129125
)
130-
const authPath = parseUrl(host).path
126+
if (url instanceof URL) authPath = parseUrl(url).path
127+
else authPath = "/api/auth"
128+
131129
const publicPaths = ["/_next", "/favicon.ico"]
132130

133131
// Avoid infinite redirects/invalid response
@@ -140,8 +138,8 @@ async function handleMiddleware(
140138
return
141139
}
142140

143-
const secret = options?.secret ?? process.env.NEXTAUTH_SECRET
144-
if (!secret) {
141+
options.secret ??= process.env.NEXTAUTH_SECRET
142+
if (!options.secret) {
145143
console.error(
146144
`[next-auth][error][NO_SECRET]`,
147145
`\nhttps://next-auth.js.org/errors#no_secret`
@@ -155,9 +153,9 @@ async function handleMiddleware(
155153

156154
const token = await getToken({
157155
req,
158-
decode: options?.jwt?.decode,
156+
decode: options.jwt?.decode,
159157
cookieName: options?.cookies?.sessionToken?.name,
160-
secret,
158+
secret: options.secret,
161159
})
162160

163161
const isAuthorized =
@@ -182,7 +180,7 @@ export interface NextRequestWithAuth extends NextRequest {
182180
export type NextMiddlewareWithAuth = (
183181
request: NextRequestWithAuth,
184182
event: NextFetchEvent
185-
) => NextMiddlewareResult | Promise<NextMiddlewareResult>
183+
) => ReturnType<NextMiddleware>
186184

187185
export type WithAuthArgs =
188186
| [NextRequestWithAuth]

0 commit comments

Comments
 (0)