Skip to content

Commit

Permalink
move more to core, refactor getURL
Browse files Browse the repository at this point in the history
  • Loading branch information
balazsorban44 committed Dec 10, 2022
1 parent 8734abe commit 1188b10
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 98 deletions.
10 changes: 10 additions & 0 deletions packages/next-auth/src/core/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ export class InvalidCallbackUrl extends UnknownError {
code = "INVALID_CALLBACK_URL_ERROR"
}

export class UnknownAction extends UnknownError {
name = "UnknownAction"
code = "UNKNOWN_ACTION_ERROR"
}

export class UntrustedHost extends UnknownError {
name = "UntrustedHost"
code = "UNTRUST_HOST_ERROR"
}

type Method = (...args: any[]) => Promise<any>

export function upperSnake(s: string) {
Expand Down
40 changes: 31 additions & 9 deletions packages/next-auth/src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import logger, { setLogger } from "../utils/logger"
import { toInternalRequest, toResponse } from "../utils/web"
import * as routes from "./routes"
import renderPage from "./pages"
import { init } from "./init"
import { assertConfig } from "./lib/assert"
import { SessionStore } from "./lib/cookie"
import renderPage from "./pages"
import * as routes from "./routes"

import type { AuthAction, AuthOptions } from "./types"
import { UntrustedHost } from "./errors"
import type { Cookie } from "./lib/cookie"
import type { ErrorType } from "./pages/error"
import type { AuthAction, AuthOptions } from "./types"

/** @internal */
export interface RequestInternal {
/** @default "http://localhost:3000" */
origin?: string
url: URL
/** @default "GET" */
method: string
cookies?: Partial<Record<string, string>>
Expand All @@ -36,6 +36,9 @@ export interface ResponseInternal<
cookies?: Cookie[]
}

const configErrorMessage =
"There is a problem with the server configuration. Check the server logs for more information."

async function AuthHandlerInternal<
Body extends string | Record<string, any> | any[]
>(params: {
Expand All @@ -45,7 +48,6 @@ async function AuthHandlerInternal<
parsedBody?: any
}): Promise<ResponseInternal<Body>> {
const { options: authOptions, req } = params
setLogger(authOptions.logger, authOptions.debug)

const assertionResult = assertConfig({ options: authOptions, req })

Expand All @@ -57,11 +59,10 @@ async function AuthHandlerInternal<

const htmlPages = ["signin", "signout", "error", "verify-request"]
if (!htmlPages.includes(req.action) || req.method !== "GET") {
const message = `There is a problem with the server configuration. Check the server logs for more information.`
return {
status: 500,
headers: { "Content-Type": "application/json" },
body: { message } as any,
body: { message: configErrorMessage } as any,
}
}
const { pages, theme } = authOptions
Expand Down Expand Up @@ -93,7 +94,7 @@ async function AuthHandlerInternal<
authOptions,
action,
providerId,
origin: req.origin,
origin: req.url.origin,
callbackUrl: req.body?.callbackUrl ?? req.query?.callbackUrl,
csrfToken: req.body?.csrfToken,
cookies: req.cookies,
Expand Down Expand Up @@ -266,7 +267,28 @@ export async function AuthHandler(
request: Request,
options: AuthOptions
): Promise<Response> {
setLogger(options.logger, options.debug)

if (!options.trustHost) {
const error = new UntrustedHost(
`Host must be trusted. URL was: ${request.url}`
)
logger.error(error.code, error)

return new Response(JSON.stringify({ message: configErrorMessage }), {
status: 500,
headers: { "Content-Type": "application/json" },
})
}

const req = await toInternalRequest(request)
if (req instanceof Error) {
logger.error((req as any).code, req)
return new Response(
`Error: This action with HTTP ${request.method} is not supported.`,
{ status: 400 }
)
}
const internalResponse = await AuthHandlerInternal({ req, options })
return toResponse(internalResponse)
}
4 changes: 2 additions & 2 deletions packages/next-auth/src/core/lib/assert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function assertConfig(params: {
const warnings: WarningCode[] = []

if (!warned) {
if (!req.origin) warnings.push("NEXTAUTH_URL")
if (!req.url.origin) warnings.push("NEXTAUTH_URL")

// TODO: Make this throw an error in next major. This will also get rid of `NODE_ENV`
if (!options.secret && process.env.NODE_ENV !== "production")
Expand All @@ -70,7 +70,7 @@ export function assertConfig(params: {

const callbackUrlParam = req.query?.callbackUrl as string | undefined

const url = parseUrl(req.origin)
const url = parseUrl(req.url.origin)

if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, url.base)) {
return new InvalidCallbackUrl(
Expand Down
70 changes: 24 additions & 46 deletions packages/next-auth/src/next/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { AuthHandler } from "../core"
import { getBody, setHeaders } from "../utils/node"
import { detectHost } from "../utils/web"
import { getBody, getURL, setHeaders } from "../utils/node"

import type {
GetServerSidePropsContext,
Expand All @@ -13,70 +12,48 @@ import type {
NextAuthRequest,
NextAuthResponse,
} from "../core/types"
import { MissingAPIRoute } from "../core/errors"

async function NextAuthHandler(
req: NextApiRequest,
res: NextApiResponse,
options: AuthOptions
) {
const errorLogger = options.logger?.error ?? console.error
const { nextauth = [] } = req.query ?? {}
if (!nextauth.length) {
const error = new MissingAPIRoute(
"Cannot find [...nextauth].{js,ts} in `/pages/api/auth`. Make sure the filename is written correctly."
)
if (process.env.NODE_ENV === "production") {
errorLogger(error)
const message = `There is a problem with the server configuration. Check the server logs for more information.`
res.status(500)
return res.json({ message })
}
throw error
const headers = new Headers(req.headers as any)
const url = getURL(req.url, headers)
if (url instanceof Error) {
if (process.env.NODE_ENV !== "production") throw url
const errorLogger = options.logger?.error ?? console.error
errorLogger("INVALID_URL", url)
res.status(500)
return res.json({
message:
"There is a problem with the server configuration. Check the server logs for more information.",
})
}

options.trustHost ??= !!(
process.env.NEXTAUTH_URL ??
process.env.AUTH_TRUST_HOST ??
process.env.VERCEL
)
const host = detectHost(
options.trustHost,
req.headers["x-forwarded-host"],
process.env.NEXTAUTH_URL ??
(process.env.NODE_ENV === "development" && "http://localhost:3000")
)

if (!host) {
const error = new Error("Could not detect host.")
if (process.env.NODE_ENV === "production") {
errorLogger(error)
const message = `There is a problem with the server configuration. Check the server logs for more information.`
res.status(500)
return res.json({ message })
}
throw error
}

const url = `${host.replace("/api/auth", "")}/api/auth/${
Array.isArray(nextauth) ? nextauth.join("/") : nextauth
}`
const request = new Request(url, {
headers: new Headers(req.headers as any),
headers,
method: req.method,
...getBody(req),
})

options.secret ??= options.jwt?.secret ?? process.env.NEXTAUTH_SECRET
options.trustHost ??= !!(
process.env.NEXTAUTH_URL ??
process.env.AUTH_TRUST_HOST ??
process.env.VERCEL ??
process.env.NODE_ENV !== "production"
)

const response = await AuthHandler(request, options)
const { status, headers } = response
const { status } = response
res.status(status)

setHeaders(headers, res)
setHeaders(response.headers, res)

// If the request expects a return URL, send it as JSON
// instead of doing an actual redirect.
const redirect = headers.get("Location")
const redirect = response.headers.get("Location")

if (req.body?.json === "true" && redirect) {
res.removeHeader("Location")
Expand Down Expand Up @@ -179,6 +156,7 @@ export async function unstable_getServerSession<
})

options.secret ??= process.env.NEXTAUTH_SECRET
options.trustHost = true
const response = await AuthHandler(request, options)

const { status = 200, headers } = response
Expand Down
40 changes: 18 additions & 22 deletions packages/next-auth/src/utils/node.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "http"
import type { IncomingMessage, ServerResponse } from "http"
import type { GetServerSidePropsContext, NextApiRequest } from "next"

export function setCookie(res, value: string) {
Expand Down Expand Up @@ -26,30 +26,26 @@ export function getBody(
}

/** Extract the full request URL from the environment */
export function getURL(
url: string,
headers?: IncomingHttpHeaders | Headers
): URL {
export function getURL(url: string | undefined, headers: Headers): URL | Error {
try {
let proto, host
if (headers instanceof Headers) {
proto = headers.get("x-forwarded-proto")
host = headers.get("x-forwarded-host") ?? headers.get("host")
} else {
proto = headers?.["x-forwarded-proto"]
host = headers?.["x-forwarded-host"] ?? headers?.host
}

proto ??= "https"
let base = `${proto}://${host}`

if (process.env.NODE_ENV !== "production" && !base) {
base = "http://localhost:3000"
if (!url) throw new Error("Missing url")
if (process.env.NEXTAUTH_URL) {
const base = process.env.NEXTAUTH_URL
const segments = base.split("/").filter(Boolean)
const hasCustomPath = segments.join("/").endsWith("api/auth")
if (hasCustomPath) {
const custom = segments.slice(0, -2).join("/")
// FIXME: path seems to have duplicate segments
return new URL(custom + url, base)
}
return new URL(url, base)
}

return new URL(url, base)
const proto = headers.get("x-forwarded-proto") ?? "https"
const host = headers.get("x-forwarded-host") ?? headers.get("host")
const origin = `${proto}://${host}`
return new URL(url, origin)
} catch (error) {
return new URL("http://localhost:3000")
return error as Error
}
}

Expand Down
73 changes: 54 additions & 19 deletions packages/next-auth/src/utils/web.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { serialize, parse as parseCookie } from "cookie"
import { UnknownAction } from "../core/errors"
import type { ResponseInternal, RequestInternal } from "../core"
import type { AuthAction } from "../core/types"

Expand Down Expand Up @@ -41,27 +42,60 @@ async function readJSONBody(
}
}

const actions = [
"providers",
"session",
"csrf",
"signin",
"signout",
"callback",
"verify-request",
"error",
"_log",
]

export async function toInternalRequest(
req: Request
): Promise<RequestInternal> {
const url = new URL(req.url)
// TODO: fix supporting custom basePath
const nextauth = url.pathname.split("/").slice(3)
const cookieHeader = req.headers.get("cookie") ?? ""

return {
action: nextauth[0] as AuthAction,
method: req.method ?? "GET",
headers: Object.fromEntries(req.headers),
body: req.body ? await readJSONBody(req.body) : undefined,
cookies:
parseCookie(
Array.isArray(cookieHeader) ? cookieHeader.join(";") : cookieHeader
) ?? {},
providerId: nextauth[1],
error: url.searchParams.get("error") ?? undefined,
origin: url.origin,
query: Object.fromEntries(url.searchParams),
): Promise<RequestInternal | Error> {
try {
const url = new URL(req.url.replace(/\/$/, ""))
const { pathname } = url

const action = actions.find((a) => pathname.includes(a)) as
| AuthAction
| undefined
if (!action) {
throw new UnknownAction("Cannot detect action.")
}

const providerIdOrAction = pathname.split("/").pop()
let providerId
if (
providerIdOrAction &&
!action.includes(providerIdOrAction) &&
["signin", "callback"].includes(action)
) {
providerId = providerIdOrAction
}

const cookieHeader = req.headers.get("cookie") ?? ""

return {
url,
action,
providerId,
method: req.method ?? "GET",
headers: Object.fromEntries(req.headers),
body: req.body ? await readJSONBody(req.body) : undefined,
cookies:
parseCookie(
Array.isArray(cookieHeader) ? cookieHeader.join(";") : cookieHeader
) ?? {},
error: url.searchParams.get("error") ?? undefined,
query: Object.fromEntries(url.searchParams),
}
} catch (error) {
return error
}
}

Expand Down Expand Up @@ -95,6 +129,7 @@ export function toResponse(res: ResponseInternal): Response {
return response
}

// TODO: Remove
/** Extract the host from the environment */
export function detectHost(
trusted: boolean,
Expand Down

0 comments on commit 1188b10

Please sign in to comment.