Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(next-auth): revert to 4.17 to fix host issues but keep other fixes #6132

Merged
merged 7 commits into from Dec 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 0 additions & 10 deletions packages/next-auth/src/core/errors.ts
Expand Up @@ -72,16 +72,6 @@ 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
139 changes: 64 additions & 75 deletions packages/next-auth/src/core/index.ts
@@ -1,21 +1,20 @@
import logger, { setLogger } from "../utils/logger"
import { toInternalRequest, toResponse } from "../utils/web"
import { detectHost } from "../utils/detect-host"
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 { UntrustedHost } from "./errors"
import type { AuthAction, AuthOptions } from "./types"
import type { Cookie } from "./lib/cookie"
import type { ErrorType } from "./pages/error"
import type { AuthAction, AuthOptions } from "./types"
import { parse as parseCookie } from "cookie"

/** @internal */
export interface RequestInternal {
url: URL
/** @default "GET" */
method: string
/** @default "http://localhost:3000" */
host?: string
method?: string
cookies?: Partial<Record<string, string>>
headers?: Record<string, any>
query?: Record<string, any>
Expand All @@ -25,29 +24,67 @@ export interface RequestInternal {
error?: string
}

/** @internal */
export interface NextAuthHeader {
key: string
value: string
}

export interface ResponseInternal<
Body extends string | Record<string, any> | any[] = any
> {
status?: number
headers?: Record<string, string>
headers?: NextAuthHeader[]
body?: Body
redirect?: string
cookies?: Cookie[]
}

const configErrorMessage =
"There is a problem with the server configuration. Check the server logs for more information."
export interface NextAuthHandlerParams {
req: Request | RequestInternal
options: AuthOptions
}

async function getBody(req: Request): Promise<Record<string, any> | undefined> {
try {
return await req.json()
} catch {}
}

// TODO:
async function toInternalRequest(
req: RequestInternal | Request
): Promise<RequestInternal> {
if (req instanceof Request) {
const url = new URL(req.url)
// TODO: handle custom paths?
const nextauth = url.pathname.split("/").slice(3)
const headers = Object.fromEntries(req.headers)
const query: Record<string, any> = Object.fromEntries(url.searchParams)
query.nextauth = nextauth

return {
action: nextauth[0] as AuthAction,
method: req.method,
headers,
body: await getBody(req),
cookies: parseCookie(req.headers.get("cookie") ?? ""),
providerId: nextauth[1],
error: url.searchParams.get("error") ?? nextauth[1],
host: detectHost(headers["x-forwarded-host"] ?? headers.host),
query,
}
}
return req
}

async function AuthHandlerInternal<
export async function AuthHandler<
Body extends string | Record<string, any> | any[]
>(params: {
req: RequestInternal
options: AuthOptions
/** REVIEW: Is this the best way to skip parsing the body in Node.js? */
parsedBody?: any
}): Promise<ResponseInternal<Body>> {
const { options: authOptions, req } = params
>(params: NextAuthHandlerParams): Promise<ResponseInternal<Body>> {
const { options: authOptions, req: incomingRequest } = params

const req = await toInternalRequest(incomingRequest)

setLogger(authOptions.logger, authOptions.debug)

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

Expand All @@ -59,10 +96,11 @@ 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: configErrorMessage } as any,
headers: [{ key: "Content-Type", value: "application/json" }],
body: { message } as any,
}
}
const { pages, theme } = authOptions
Expand All @@ -88,13 +126,13 @@ async function AuthHandlerInternal<
}
}

const { action, providerId, error, method } = req
const { action, providerId, error, method = "GET" } = req

const { options, cookies } = await init({
authOptions,
action,
providerId,
url: req.url,
host: req.host,
callbackUrl: req.body?.callbackUrl ?? req.query?.callbackUrl,
csrfToken: req.body?.csrfToken,
cookies: req.cookies,
Expand All @@ -116,12 +154,11 @@ async function AuthHandlerInternal<
case "session": {
const session = await routes.session({ options, sessionStore })
if (session.cookies) cookies.push(...session.cookies)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
return { ...session, cookies } as any
}
case "csrf":
return {
headers: { "Content-Type": "application/json" },
headers: [{ key: "Content-Type", value: "application/json" }],
body: { csrfToken: options.csrfToken } as any,
cookies,
}
Expand Down Expand Up @@ -257,51 +294,3 @@ async function AuthHandlerInternal<
body: `Error: This action with HTTP ${method} is not supported by NextAuth.js` as any,
}
}

/**
* The core functionality of `next-auth`.
* It receives a standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
* and returns a standard [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).
*/
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 })

const response = await toResponse(internalResponse)

// If the request expects a return URL, send it as JSON
// instead of doing an actual redirect.
const redirect = response.headers.get("Location")
if (request.headers.has("X-Auth-Return-Redirect") && redirect) {
response.headers.delete("Location")
response.headers.set("Content-Type", "application/json")
return new Response(JSON.stringify({ url: redirect }), {
headers: response.headers,
})
}
return response
}
15 changes: 5 additions & 10 deletions packages/next-auth/src/core/init.ts
Expand Up @@ -15,7 +15,7 @@ import type { InternalOptions } from "./types"
import parseUrl from "../utils/parse-url"

interface InitParams {
url: URL
host?: string
authOptions: AuthOptions
providerId?: string
action: InternalOptions["action"]
Expand All @@ -33,7 +33,7 @@ export async function init({
authOptions,
providerId,
action,
url: reqUrl,
host,
cookies: reqCookies,
callbackUrl: reqCallbackUrl,
csrfToken: reqCsrfToken,
Expand All @@ -42,12 +42,7 @@ export async function init({
options: InternalOptions
cookies: cookie.Cookie[]
}> {
// TODO: move this to web.ts
const parsed = parseUrl(
reqUrl.origin +
reqUrl.pathname.replace(`/${action}`, "").replace(`/${providerId}`, "")
)
const url = new URL(parsed.toString())
const url = parseUrl(host)

const secret = createSecret({ authOptions, url })

Expand All @@ -72,15 +67,15 @@ export async function init({
},
// Custom options override defaults
...authOptions,
// These computed settings can have values in userOptions but we override them
// These computed settings can have values in authOptions but we override them
// and are request-specific.
url,
action,
// @ts-expect-errors
provider,
cookies: {
...cookie.defaultCookies(
authOptions.useSecureCookies ?? url.protocol === "https:"
authOptions.useSecureCookies ?? url.base.startsWith("https://")
),
// Allow user cookie options to override any cookie settings above
...authOptions.cookies,
Expand Down
13 changes: 8 additions & 5 deletions packages/next-auth/src/core/lib/assert.ts
Expand Up @@ -7,6 +7,7 @@ import {
InvalidCallbackUrl,
MissingAdapterMethods,
} from "../errors"
import parseUrl from "../../utils/parse-url"
import { defaultCookies } from "./cookie"

import type { RequestInternal } from ".."
Expand Down Expand Up @@ -43,11 +44,11 @@ export function assertConfig(params: {
req: RequestInternal
}): ConfigError | WarningCode[] {
const { options, req } = params
const { url } = req

const warnings: WarningCode[] = []

if (!warned) {
if (!url.origin) warnings.push("NEXTAUTH_URL")
if (!req.host) 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 @@ -69,19 +70,21 @@ export function assertConfig(params: {

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

if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, url.origin)) {
const url = parseUrl(req.host)

if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, url.base)) {
return new InvalidCallbackUrl(
`Invalid callback URL. Received: ${callbackUrlParam}`
)
}

const { callbackUrl: defaultCallbackUrl } = defaultCookies(
options.useSecureCookies ?? url.protocol === "https://"
options.useSecureCookies ?? url.base.startsWith("https://")
)
const callbackUrlCookie =
req.cookies?.[options.cookies?.callbackUrl?.name ?? defaultCallbackUrl.name]

if (callbackUrlCookie && !isValidHttpUrl(callbackUrlCookie, url.origin)) {
if (callbackUrlCookie && !isValidHttpUrl(callbackUrlCookie, url.base)) {
return new InvalidCallbackUrl(
`Invalid callback URL. Received: ${callbackUrlCookie}`
)
Expand Down
3 changes: 2 additions & 1 deletion packages/next-auth/src/core/lib/providers.ts
Expand Up @@ -6,14 +6,15 @@ import type {
OAuthConfig,
Provider,
} from "../../providers"
import type { InternalUrl } from "../../utils/parse-url"

/**
* Adds `signinUrl` and `callbackUrl` to each provider
* and deep merge user-defined options.
*/
export default function parseProviders(params: {
providers: Provider[]
url: URL
url: InternalUrl
providerId?: string
}): {
providers: InternalProvider[]
Expand Down
6 changes: 5 additions & 1 deletion packages/next-auth/src/core/lib/utils.ts
Expand Up @@ -2,6 +2,7 @@ import { createHash } from "crypto"

import type { AuthOptions } from "../.."
import type { InternalOptions } from "../types"
import type { InternalUrl } from "../../utils/parse-url"

/**
* Takes a number in seconds and returns the date in the future.
Expand All @@ -27,7 +28,10 @@ export function hashToken(token: string, options: InternalOptions<"email">) {
* If no secret option is specified then it creates one on the fly
* based on options passed here. If options contains unique data, such as
* OAuth provider secrets and database credentials it should be sufficent. If no secret provided in production, we throw an error. */
export function createSecret(params: { authOptions: AuthOptions; url: URL }) {
export function createSecret(params: {
authOptions: AuthOptions
url: InternalUrl
}) {
const { authOptions, url } = params

return (
Expand Down
3 changes: 2 additions & 1 deletion packages/next-auth/src/core/pages/error.tsx
@@ -1,4 +1,5 @@
import { Theme } from "../.."
import { InternalUrl } from "../../utils/parse-url"

/**
* The following errors are passed as error query parameters to the default or overridden error page.
Expand All @@ -11,7 +12,7 @@ export type ErrorType =
| "verification"

export interface ErrorProps {
url?: URL
url?: InternalUrl
theme?: Theme
error?: ErrorType
}
Expand Down
2 changes: 1 addition & 1 deletion packages/next-auth/src/core/pages/index.ts
Expand Up @@ -31,7 +31,7 @@ export default function renderPage(params: RenderPageParams) {
return {
cookies,
status,
headers: { "Content-Type": "text/html" },
headers: [{ key: "Content-Type", value: "text/html" }],
body: `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>${css()}</style><title>${title}</title></head><body class="__next-auth-theme-${
theme?.colorScheme ?? "auto"
}"><div class="page">${renderToString(html)}</div></body></html>`,
Expand Down
3 changes: 2 additions & 1 deletion packages/next-auth/src/core/pages/signout.tsx
@@ -1,7 +1,8 @@
import { Theme } from "../.."
import { InternalUrl } from "../../utils/parse-url"

export interface SignoutProps {
url: URL
url: InternalUrl
csrfToken: string
theme: Theme
}
Expand Down
2 changes: 1 addition & 1 deletion packages/next-auth/src/core/routes/providers.ts
Expand Up @@ -18,7 +18,7 @@ export default function providers(
providers: InternalProvider[]
): ResponseInternal<Record<string, PublicProvider>> {
return {
headers: { "Content-Type": "application/json" },
headers: [{ key: "Content-Type", value: "application/json" }],
body: providers.reduce<Record<string, PublicProvider>>(
(acc, { id, name, type, signinUrl, callbackUrl }) => {
acc[id] = { id, name, type, signinUrl, callbackUrl }
Expand Down