Skip to content

Commit

Permalink
fix(next-auth): revert to 4.17 to fix host issues but keep other fixes (
Browse files Browse the repository at this point in the history
#6132)

* fix(next-auth): revert to 4.17 and replay other fixes

* revert line change

* replay some TS changes to reduce diff

* fix tests

* revert more renames

* revert renames

* fix test, cleanup
  • Loading branch information
balazsorban44 committed Dec 21, 2022
1 parent 6242aa7 commit a83573e
Show file tree
Hide file tree
Showing 31 changed files with 418 additions and 1,193 deletions.
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

1 comment on commit a83573e

@vercel
Copy link

@vercel vercel bot commented on a83573e Dec 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.