mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
4 Commits
next-auth@
...
@next-auth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5dd901b03 | ||
|
|
62f672ae30 | ||
|
|
2c669b32fc | ||
|
|
2dea8919e5 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@next-auth/sequelize-adapter",
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.7",
|
||||
"description": "Sequelize adapter for next-auth.",
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/next-auth",
|
||||
@@ -42,4 +42,4 @@
|
||||
"jest": {
|
||||
"preset": "@next-auth/adapter-test/jest"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export const Account = {
|
||||
expires_at: { type: DataTypes.INTEGER },
|
||||
token_type: { type: DataTypes.STRING },
|
||||
scope: { type: DataTypes.STRING },
|
||||
id_token: { type: DataTypes.STRING },
|
||||
id_token: { type: DataTypes.TEXT },
|
||||
session_state: { type: DataTypes.STRING },
|
||||
userId: { type: DataTypes.UUID },
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-auth",
|
||||
"version": "4.18.4",
|
||||
"version": "4.18.5",
|
||||
"description": "Authentication for Next.js",
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/next-auth.git",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
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" */
|
||||
host?: string
|
||||
method?: string
|
||||
url: URL
|
||||
/** @default "GET" */
|
||||
method: string
|
||||
cookies?: Partial<Record<string, string>>
|
||||
headers?: Record<string, any>
|
||||
query?: Record<string, any>
|
||||
@@ -23,22 +25,20 @@ export interface RequestInternal {
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface NextAuthHeader {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
// TODO: Rename to `ResponseInternal`
|
||||
/** @internal */
|
||||
export interface ResponseInternal<
|
||||
Body extends string | Record<string, any> | any[] = any
|
||||
> {
|
||||
status?: number
|
||||
headers?: NextAuthHeader[]
|
||||
headers?: Record<string, string>
|
||||
body?: Body
|
||||
redirect?: string
|
||||
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: {
|
||||
@@ -47,10 +47,9 @@ async function AuthHandlerInternal<
|
||||
/** REVIEW: Is this the best way to skip parsing the body in Node.js? */
|
||||
parsedBody?: any
|
||||
}): Promise<ResponseInternal<Body>> {
|
||||
const { options: userOptions, req } = params
|
||||
setLogger(userOptions.logger, userOptions.debug)
|
||||
const { options: authOptions, req } = params
|
||||
|
||||
const assertionResult = assertConfig({ options: userOptions, req })
|
||||
const assertionResult = assertConfig({ options: authOptions, req })
|
||||
|
||||
if (Array.isArray(assertionResult)) {
|
||||
assertionResult.forEach(logger.warn)
|
||||
@@ -60,14 +59,13 @@ 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: [{ key: "Content-Type", value: "application/json" }],
|
||||
body: { message } as any,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: { message: configErrorMessage } as any,
|
||||
}
|
||||
}
|
||||
const { pages, theme } = userOptions
|
||||
const { pages, theme } = authOptions
|
||||
|
||||
const authOnErrorPage =
|
||||
pages?.error && req.query?.callbackUrl?.startsWith(pages.error)
|
||||
@@ -90,13 +88,13 @@ async function AuthHandlerInternal<
|
||||
}
|
||||
}
|
||||
|
||||
const { action, providerId, error, method = "GET" } = req
|
||||
const { action, providerId, error, method } = req
|
||||
|
||||
const { options, cookies } = await init({
|
||||
userOptions,
|
||||
authOptions,
|
||||
action,
|
||||
providerId,
|
||||
host: req.host,
|
||||
url: req.url,
|
||||
callbackUrl: req.body?.callbackUrl ?? req.query?.callbackUrl,
|
||||
csrfToken: req.body?.csrfToken,
|
||||
cookies: req.cookies,
|
||||
@@ -123,7 +121,7 @@ async function AuthHandlerInternal<
|
||||
}
|
||||
case "csrf":
|
||||
return {
|
||||
headers: [{ key: "Content-Type", value: "application/json" }],
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: { csrfToken: options.csrfToken } as any,
|
||||
cookies,
|
||||
}
|
||||
@@ -240,7 +238,7 @@ async function AuthHandlerInternal<
|
||||
}
|
||||
break
|
||||
case "_log":
|
||||
if (userOptions.logger) {
|
||||
if (authOptions.logger) {
|
||||
try {
|
||||
const { code, level, ...metadata } = req.body ?? {}
|
||||
logger[level](code, metadata)
|
||||
@@ -269,7 +267,41 @@ 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)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { randomBytes, randomUUID } from "crypto"
|
||||
import { AuthOptions } from ".."
|
||||
import logger from "../utils/logger"
|
||||
import parseUrl from "../utils/parse-url"
|
||||
import { adapterErrorHandler, eventsErrorHandler } from "./errors"
|
||||
import parseProviders from "./lib/providers"
|
||||
import { createSecret } from "./lib/utils"
|
||||
@@ -13,10 +12,11 @@ import { createCallbackUrl } from "./lib/callback-url"
|
||||
import { RequestInternal } from "."
|
||||
|
||||
import type { InternalOptions } from "./types"
|
||||
import parseUrl from "../utils/parse-url"
|
||||
|
||||
interface InitParams {
|
||||
host?: string
|
||||
userOptions: AuthOptions
|
||||
url: URL
|
||||
authOptions: AuthOptions
|
||||
providerId?: string
|
||||
action: InternalOptions["action"]
|
||||
/** Callback URL value extracted from the incoming request. */
|
||||
@@ -30,10 +30,10 @@ interface InitParams {
|
||||
|
||||
/** Initialize all internal options and cookies. */
|
||||
export async function init({
|
||||
userOptions,
|
||||
authOptions,
|
||||
providerId,
|
||||
action,
|
||||
host,
|
||||
url: reqUrl,
|
||||
cookies: reqCookies,
|
||||
callbackUrl: reqCallbackUrl,
|
||||
csrfToken: reqCsrfToken,
|
||||
@@ -42,12 +42,17 @@ export async function init({
|
||||
options: InternalOptions
|
||||
cookies: cookie.Cookie[]
|
||||
}> {
|
||||
const url = parseUrl(host)
|
||||
// TODO: move this to web.ts
|
||||
const parsed = parseUrl(
|
||||
reqUrl.origin +
|
||||
reqUrl.pathname.replace(`/${action}`, "").replace(`/${providerId}`, "")
|
||||
)
|
||||
const url = new URL(parsed.toString())
|
||||
|
||||
const secret = createSecret({ userOptions, url })
|
||||
const secret = createSecret({ authOptions, url })
|
||||
|
||||
const { providers, provider } = parseProviders({
|
||||
providers: userOptions.providers,
|
||||
providers: authOptions.providers,
|
||||
url,
|
||||
providerId,
|
||||
})
|
||||
@@ -66,7 +71,7 @@ export async function init({
|
||||
buttonText: "",
|
||||
},
|
||||
// Custom options override defaults
|
||||
...userOptions,
|
||||
...authOptions,
|
||||
// These computed settings can have values in userOptions but we override them
|
||||
// and are request-specific.
|
||||
url,
|
||||
@@ -75,24 +80,24 @@ export async function init({
|
||||
provider,
|
||||
cookies: {
|
||||
...cookie.defaultCookies(
|
||||
userOptions.useSecureCookies ?? url.base.startsWith("https://")
|
||||
authOptions.useSecureCookies ?? url.protocol === "https:"
|
||||
),
|
||||
// Allow user cookie options to override any cookie settings above
|
||||
...userOptions.cookies,
|
||||
...authOptions.cookies,
|
||||
},
|
||||
secret,
|
||||
providers,
|
||||
// Session options
|
||||
session: {
|
||||
// If no adapter specified, force use of JSON Web Tokens (stateless)
|
||||
strategy: userOptions.adapter ? "database" : "jwt",
|
||||
strategy: authOptions.adapter ? "database" : "jwt",
|
||||
maxAge,
|
||||
updateAge: 24 * 60 * 60,
|
||||
generateSessionToken: () => {
|
||||
// Use `randomUUID` if available. (Node 15.6+)
|
||||
return randomUUID?.() ?? randomBytes(32).toString("hex")
|
||||
},
|
||||
...userOptions.session,
|
||||
...authOptions.session,
|
||||
},
|
||||
// JWT options
|
||||
jwt: {
|
||||
@@ -100,13 +105,13 @@ export async function init({
|
||||
maxAge, // same as session maxAge,
|
||||
encode: jwt.encode,
|
||||
decode: jwt.decode,
|
||||
...userOptions.jwt,
|
||||
...authOptions.jwt,
|
||||
},
|
||||
// Event messages
|
||||
events: eventsErrorHandler(userOptions.events ?? {}, logger),
|
||||
adapter: adapterErrorHandler(userOptions.adapter, logger),
|
||||
events: eventsErrorHandler(authOptions.events ?? {}, logger),
|
||||
adapter: adapterErrorHandler(authOptions.adapter, logger),
|
||||
// Callback functions
|
||||
callbacks: { ...defaultCallbacks, ...userOptions.callbacks },
|
||||
callbacks: { ...defaultCallbacks, ...authOptions.callbacks },
|
||||
logger,
|
||||
callbackUrl: url.origin,
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
InvalidCallbackUrl,
|
||||
MissingAdapterMethods,
|
||||
} from "../errors"
|
||||
import parseUrl from "../../utils/parse-url"
|
||||
import { defaultCookies } from "./cookie"
|
||||
|
||||
import type { RequestInternal } from ".."
|
||||
@@ -44,11 +43,11 @@ export function assertConfig(params: {
|
||||
req: RequestInternal
|
||||
}): ConfigError | WarningCode[] {
|
||||
const { options, req } = params
|
||||
|
||||
const { url } = req
|
||||
const warnings: WarningCode[] = []
|
||||
|
||||
if (!warned) {
|
||||
if (!req.host) warnings.push("NEXTAUTH_URL")
|
||||
if (!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")
|
||||
@@ -70,21 +69,19 @@ export function assertConfig(params: {
|
||||
|
||||
const callbackUrlParam = req.query?.callbackUrl as string | undefined
|
||||
|
||||
const url = parseUrl(req.host)
|
||||
|
||||
if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, url.base)) {
|
||||
if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, url.origin)) {
|
||||
return new InvalidCallbackUrl(
|
||||
`Invalid callback URL. Received: ${callbackUrlParam}`
|
||||
)
|
||||
}
|
||||
|
||||
const { callbackUrl: defaultCallbackUrl } = defaultCookies(
|
||||
options.useSecureCookies ?? url.base.startsWith("https://")
|
||||
options.useSecureCookies ?? url.protocol === "https://"
|
||||
)
|
||||
const callbackUrlCookie =
|
||||
req.cookies?.[options.cookies?.callbackUrl?.name ?? defaultCallbackUrl.name]
|
||||
|
||||
if (callbackUrlCookie && !isValidHttpUrl(callbackUrlCookie, url.base)) {
|
||||
if (callbackUrlCookie && !isValidHttpUrl(callbackUrlCookie, url.origin)) {
|
||||
return new InvalidCallbackUrl(
|
||||
`Invalid callback URL. Received: ${callbackUrlCookie}`
|
||||
)
|
||||
|
||||
@@ -6,7 +6,6 @@ import type {
|
||||
OAuthConfig,
|
||||
Provider,
|
||||
} from "../../providers"
|
||||
import type { InternalUrl } from "../../utils/parse-url"
|
||||
|
||||
/**
|
||||
* Adds `signinUrl` and `callbackUrl` to each provider
|
||||
@@ -14,7 +13,7 @@ import type { InternalUrl } from "../../utils/parse-url"
|
||||
*/
|
||||
export default function parseProviders(params: {
|
||||
providers: Provider[]
|
||||
url: InternalUrl
|
||||
url: URL
|
||||
providerId?: string
|
||||
}): {
|
||||
providers: InternalProvider[]
|
||||
|
||||
@@ -2,7 +2,6 @@ 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.
|
||||
@@ -28,17 +27,14 @@ 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: {
|
||||
userOptions: AuthOptions
|
||||
url: InternalUrl
|
||||
}) {
|
||||
const { userOptions, url } = params
|
||||
export function createSecret(params: { authOptions: AuthOptions; url: URL }) {
|
||||
const { authOptions, url } = params
|
||||
|
||||
return (
|
||||
userOptions.secret ??
|
||||
authOptions.secret ??
|
||||
// TODO: Remove falling back to default secret, and error in dev if one isn't provided
|
||||
createHash("sha256")
|
||||
.update(JSON.stringify({ ...url, ...userOptions }))
|
||||
.update(JSON.stringify({ ...url, ...authOptions }))
|
||||
.digest("hex")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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.
|
||||
@@ -12,7 +11,7 @@ export type ErrorType =
|
||||
| "verification"
|
||||
|
||||
export interface ErrorProps {
|
||||
url?: InternalUrl
|
||||
url?: URL
|
||||
theme?: Theme
|
||||
error?: ErrorType
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function renderPage(params: RenderPageParams) {
|
||||
return {
|
||||
cookies,
|
||||
status,
|
||||
headers: [{ key: "Content-Type", value: "text/html" }],
|
||||
headers: { "Content-Type": "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>`,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Theme } from "../.."
|
||||
import { InternalUrl } from "../../utils/parse-url"
|
||||
|
||||
export interface SignoutProps {
|
||||
url: InternalUrl
|
||||
url: URL
|
||||
csrfToken: string
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function providers(
|
||||
providers: InternalProvider[]
|
||||
): ResponseInternal<Record<string, PublicProvider>> {
|
||||
return {
|
||||
headers: [{ key: "Content-Type", value: "application/json" }],
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: providers.reduce<Record<string, PublicProvider>>(
|
||||
(acc, { id, name, type, signinUrl, callbackUrl }) => {
|
||||
acc[id] = { id, name, type, signinUrl, callbackUrl }
|
||||
|
||||
@@ -31,7 +31,7 @@ export default async function session(
|
||||
|
||||
const response: ResponseInternal<Session | {}> = {
|
||||
body: {},
|
||||
headers: [{ key: "Content-Type", value: "application/json" }],
|
||||
headers: { "Content-Type": "application/json" },
|
||||
cookies: [],
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ import type { CookieSerializeOptions } from "cookie"
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
import type { InternalUrl } from "../utils/parse-url"
|
||||
|
||||
export type Awaitable<T> = T | PromiseLike<T>
|
||||
|
||||
export type { LoggerInstance }
|
||||
@@ -210,7 +208,7 @@ export interface AuthOptions {
|
||||
* - ⚠ **This is an advanced option.** Advanced options are passed the same way as basic options,
|
||||
* but **may have complex implications** or side effects.
|
||||
* You should **try to avoid using advanced options** unless you are very comfortable using them.
|
||||
* @default Boolean(process.env.AUTH_TRUST_HOST ?? process.env.VERCEL)
|
||||
* @default Boolean(process.env.NEXTAUTH_URL ?? process.env.AUTH_TRUST_HOST ?? process.env.VERCEL)
|
||||
*/
|
||||
trustHost?: boolean
|
||||
}
|
||||
@@ -528,11 +526,7 @@ export interface InternalOptions<
|
||||
WithVerificationToken = TProviderType extends "email" ? true : false
|
||||
> {
|
||||
providers: InternalProvider[]
|
||||
/**
|
||||
* Parsed from `NEXTAUTH_URL` or `x-forwarded-host` on Vercel.
|
||||
* @default "http://localhost:3000/api/auth"
|
||||
*/
|
||||
url: InternalUrl
|
||||
url: URL
|
||||
action: AuthAction
|
||||
provider: InternalProvider<TProviderType>
|
||||
csrfToken?: string
|
||||
|
||||
@@ -94,7 +94,7 @@ export async function getToken<R extends boolean = false>(
|
||||
const authorizationHeader =
|
||||
req.headers instanceof Headers
|
||||
? req.headers.get("authorization")
|
||||
: req.headers.authorization
|
||||
: req.headers?.authorization
|
||||
|
||||
if (!token && authorizationHeader?.split(" ")[0] === "Bearer") {
|
||||
const urlEncodedToken = authorizationHeader.split(" ")[1]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AuthHandler } from "../core"
|
||||
import { getURL, getBody, setHeaders } from "../utils/node"
|
||||
import { getBody, getURL, setHeaders } from "../utils/node"
|
||||
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
@@ -18,35 +18,36 @@ async function NextAuthHandler(
|
||||
res: NextApiResponse,
|
||||
options: AuthOptions
|
||||
) {
|
||||
const url = getURL(
|
||||
req.url,
|
||||
options.trustHost,
|
||||
req.headers["x-forwarded-host"] ?? req.headers.host
|
||||
)
|
||||
|
||||
if (url instanceof Error) return res.status(400).end()
|
||||
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(400)
|
||||
return res.json({
|
||||
message:
|
||||
"There is a problem with the server configuration. Check the server logs for more information.",
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
res.status(status)
|
||||
|
||||
setHeaders(headers, res)
|
||||
|
||||
// If the request expects a return URL, send it as JSON
|
||||
// instead of doing an actual redirect.
|
||||
const redirect = headers.get("Location")
|
||||
|
||||
if (req.body?.json === "true" && redirect) {
|
||||
res.removeHeader("Location")
|
||||
return res.json({ url: redirect })
|
||||
}
|
||||
res.status(response.status)
|
||||
setHeaders(response.headers, res)
|
||||
|
||||
return res.send(await response.text())
|
||||
}
|
||||
@@ -139,19 +140,23 @@ export async function unstable_getServerSession<
|
||||
options = Object.assign({}, args[2], { providers: [] })
|
||||
}
|
||||
|
||||
const urlOrError = getURL(
|
||||
"/api/auth/session",
|
||||
options.trustHost,
|
||||
req.headers["x-forwarded-host"] ?? req.headers.host
|
||||
)
|
||||
const url = getURL("/api/auth/session", new Headers(req.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(400)
|
||||
return res.json({
|
||||
message:
|
||||
"There is a problem with the server configuration. Check the server logs for more information.",
|
||||
})
|
||||
}
|
||||
|
||||
if (urlOrError instanceof Error) throw urlOrError
|
||||
const request = new Request(url, { headers: new Headers(req.headers) })
|
||||
|
||||
options.secret ??= process.env.NEXTAUTH_SECRET
|
||||
const response = await AuthHandler(
|
||||
new Request(urlOrError, { headers: req.headers }),
|
||||
options
|
||||
)
|
||||
options.trustHost = true
|
||||
const response = await AuthHandler(request, options)
|
||||
|
||||
const { status = 200, headers } = response
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { NextResponse, NextRequest } from "next/server"
|
||||
|
||||
import { getToken } from "../jwt"
|
||||
import parseUrl from "../utils/parse-url"
|
||||
import { getURL } from "../utils/node"
|
||||
import { detectHost } from "../utils/web"
|
||||
|
||||
type AuthorizedCallback = (params: {
|
||||
token: JWT | null
|
||||
@@ -113,18 +113,19 @@ async function handleMiddleware(
|
||||
const signInPage = options?.pages?.signIn ?? "/api/auth/signin"
|
||||
const errorPage = options?.pages?.error ?? "/api/auth/error"
|
||||
|
||||
options.trustHost = Boolean(
|
||||
options.trustHost ?? process.env.VERCEL ?? process.env.AUTH_TRUST_HOST
|
||||
options.trustHost ??= !!(
|
||||
process.env.NEXTAUTH_URL ??
|
||||
process.env.VERCEL ??
|
||||
process.env.AUTH_TRUST_HOST
|
||||
)
|
||||
|
||||
let authPath
|
||||
const url = getURL(
|
||||
null,
|
||||
const host = detectHost(
|
||||
options.trustHost,
|
||||
req.headers.get("x-forwarded-host") ?? req.headers.get("host")
|
||||
req.headers?.get("x-forwarded-host"),
|
||||
process.env.NEXTAUTH_URL ??
|
||||
(process.env.NODE_ENV !== "production" && "http://localhost:3000")
|
||||
)
|
||||
if (url instanceof URL) authPath = parseUrl(url).path
|
||||
else authPath = "/api/auth"
|
||||
const authPath = parseUrl(host).path
|
||||
|
||||
const publicPaths = ["/_next", "/favicon.ico"]
|
||||
|
||||
|
||||
@@ -241,13 +241,13 @@ export async function signIn<
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"X-Auth-Return-Redirect": "1",
|
||||
},
|
||||
// @ts-expect-error
|
||||
body: new URLSearchParams({
|
||||
...options,
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -291,12 +291,11 @@ export async function signOut<R extends boolean = true>(
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"X-Auth-Return-Redirect": "1",
|
||||
},
|
||||
// @ts-expect-error
|
||||
body: new URLSearchParams({
|
||||
csrfToken: await getCsrfToken(),
|
||||
csrfToken: (await getCsrfToken()) ?? "",
|
||||
callbackUrl,
|
||||
json: true,
|
||||
}),
|
||||
}
|
||||
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
|
||||
|
||||
@@ -25,30 +25,37 @@ export function getBody(
|
||||
return { body: JSON.stringify(req.body) }
|
||||
}
|
||||
|
||||
/** Extract the host from the environment */
|
||||
export function getURL(
|
||||
url: string | undefined | null,
|
||||
trusted: boolean | undefined = !!(
|
||||
process.env.AUTH_TRUST_HOST ?? process.env.VERCEL
|
||||
),
|
||||
forwardedValue: string | string[] | undefined | null
|
||||
): URL | Error {
|
||||
/**
|
||||
* Extract the full request URL from the environment.
|
||||
* NOTE: It does not verify if the host should be trusted.
|
||||
*/
|
||||
export function getURL(url: string | undefined, headers: Headers): URL | Error {
|
||||
try {
|
||||
let host =
|
||||
process.env.NEXTAUTH_URL ??
|
||||
(process.env.NODE_ENV !== "production" && "http://localhost:3000")
|
||||
if (!url) throw new Error("Missing url")
|
||||
if (process.env.NEXTAUTH_URL) {
|
||||
const base = new URL(process.env.NEXTAUTH_URL)
|
||||
if (!["http:", "https:"].includes(base.protocol)) {
|
||||
throw new Error("Invalid protocol")
|
||||
}
|
||||
const hasCustomPath = base.pathname !== "/"
|
||||
|
||||
if (trusted && forwardedValue) {
|
||||
host = Array.isArray(forwardedValue) ? forwardedValue[0] : forwardedValue
|
||||
if (hasCustomPath) {
|
||||
const apiAuthRe = /\/api\/auth\/?$/
|
||||
const basePathname = base.pathname.match(apiAuthRe)
|
||||
? base.pathname.replace(apiAuthRe, "")
|
||||
: base.pathname
|
||||
return new URL(basePathname.replace(/\/$/, "") + url, base.origin)
|
||||
}
|
||||
return new URL(url, base)
|
||||
}
|
||||
|
||||
if (!host) throw new TypeError("Invalid host")
|
||||
if (!url) throw new TypeError("Invalid URL, cannot determine action")
|
||||
|
||||
if (host.startsWith("http://") || host.startsWith("https://")) {
|
||||
return new URL(`${host}${url}`)
|
||||
}
|
||||
return new URL(`https://${host}${url}`)
|
||||
const proto =
|
||||
headers.get("x-forwarded-proto") ??
|
||||
(process.env.NODE_ENV !== "production" ? "http" : "https")
|
||||
const host = headers.get("x-forwarded-host") ?? headers.get("host")
|
||||
if (!["http", "https"].includes(proto)) throw new Error("Invalid protocol")
|
||||
const origin = `${proto}://${host}`
|
||||
if (!host) throw new Error("Missing host")
|
||||
return new URL(url, origin)
|
||||
} catch (error) {
|
||||
return error as Error
|
||||
}
|
||||
|
||||
@@ -11,7 +11,10 @@ export interface InternalUrl {
|
||||
toString: () => string
|
||||
}
|
||||
|
||||
/** Returns an `URL` like object to make requests/redirects from server-side */
|
||||
/**
|
||||
* TODO: Can we remove this?
|
||||
* Returns an `URL` like object to make requests/redirects from server-side
|
||||
*/
|
||||
export default function parseUrl(url?: string | URL): InternalUrl {
|
||||
const defaultUrl = new URL("http://localhost:3000/api/auth")
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -41,40 +42,56 @@ async function readJSONBody(
|
||||
}
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
const actions: AuthAction[] = [ "providers", "session", "csrf", "signin", "signout", "callback", "verify-request", "error", "_log" ]
|
||||
|
||||
export async function toInternalRequest(
|
||||
req: Request
|
||||
): Promise<RequestInternal> {
|
||||
const url = new URL(req.url)
|
||||
const nextauth = url.pathname.split("/").slice(3)
|
||||
const headers = Object.fromEntries(req.headers)
|
||||
const query: Record<string, any> = Object.fromEntries(url.searchParams)
|
||||
): Promise<RequestInternal | Error> {
|
||||
try {
|
||||
// TODO: .toString() should not inclide action and providerId
|
||||
// see init.ts
|
||||
const url = new URL(req.url.replace(/\/$/, ""))
|
||||
const { pathname } = url
|
||||
|
||||
const cookieHeader = req.headers.get("cookie") ?? ""
|
||||
const cookies =
|
||||
parseCookie(
|
||||
Array.isArray(cookieHeader) ? cookieHeader.join(";") : cookieHeader
|
||||
) ?? {}
|
||||
const action = actions.find((a) => pathname.includes(a))
|
||||
if (!action) {
|
||||
throw new UnknownAction("Cannot detect action.")
|
||||
}
|
||||
|
||||
return {
|
||||
action: nextauth[0] as AuthAction,
|
||||
method: req.method,
|
||||
headers,
|
||||
body: req.body ? await readJSONBody(req.body) : undefined,
|
||||
cookies: cookies,
|
||||
providerId: nextauth[1],
|
||||
error: url.searchParams.get("error") ?? undefined,
|
||||
host: new URL(req.url).origin,
|
||||
query,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
export function toResponse(res: ResponseInternal): Response {
|
||||
const headers = new Headers(
|
||||
res.headers?.reduce((acc, { key, value }) => {
|
||||
acc[key] = value
|
||||
return acc
|
||||
}, {})
|
||||
)
|
||||
const headers = new Headers(res.headers)
|
||||
|
||||
res.cookies?.forEach((cookie) => {
|
||||
const { name, value, options } = cookie
|
||||
@@ -102,3 +119,17 @@ export function toResponse(res: ResponseInternal): Response {
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// TODO: Remove
|
||||
/** Extract the host from the environment */
|
||||
export function detectHost(
|
||||
trusted: boolean,
|
||||
forwardedValue: string | string[] | undefined | null,
|
||||
defaultValue: string | false
|
||||
): string | undefined {
|
||||
if (trusted && forwardedValue) {
|
||||
return Array.isArray(forwardedValue) ? forwardedValue[0] : forwardedValue
|
||||
}
|
||||
|
||||
return defaultValue || undefined
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import EmailProvider from "../src/providers/email"
|
||||
|
||||
it("Show error page if secret is not defined", async () => {
|
||||
const { res, log } = await handler(
|
||||
{ providers: [], secret: undefined },
|
||||
{ providers: [], secret: undefined, trustHost: true },
|
||||
{ prod: true }
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ it("Show error page if adapter is missing functions when using with email", asyn
|
||||
adapter: missingFunctionAdapter,
|
||||
providers: [EmailProvider({ sendVerificationRequest })],
|
||||
secret: "secret",
|
||||
trustHost: true,
|
||||
},
|
||||
{ prod: true }
|
||||
)
|
||||
@@ -48,6 +49,7 @@ it("Show error page if adapter is not configured when using with email", async (
|
||||
{
|
||||
providers: [EmailProvider({ sendVerificationRequest })],
|
||||
secret: "secret",
|
||||
trustHost: true,
|
||||
},
|
||||
{ prod: true }
|
||||
)
|
||||
@@ -64,7 +66,7 @@ it("Show error page if adapter is not configured when using with email", async (
|
||||
|
||||
it("Should show configuration error page on invalid `callbackUrl`", async () => {
|
||||
const { res, log } = await handler(
|
||||
{ providers: [] },
|
||||
{ providers: [], trustHost: true },
|
||||
{ prod: true, params: { callbackUrl: "invalid-callback" } }
|
||||
)
|
||||
|
||||
@@ -80,7 +82,7 @@ it("Should show configuration error page on invalid `callbackUrl`", async () =>
|
||||
|
||||
it("Allow relative `callbackUrl`", async () => {
|
||||
const { res, log } = await handler(
|
||||
{ providers: [] },
|
||||
{ providers: [], trustHost: true },
|
||||
{ prod: true, params: { callbackUrl: "/callback" } }
|
||||
)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ it("Send e-mail to the only address correctly", async () => {
|
||||
providers: [EmailProvider({ sendVerificationRequest })],
|
||||
callbacks: { signIn },
|
||||
secret,
|
||||
trustHost: true,
|
||||
},
|
||||
{
|
||||
path: "signin/email",
|
||||
@@ -54,6 +55,7 @@ it("Send e-mail to first address only", async () => {
|
||||
providers: [EmailProvider({ sendVerificationRequest })],
|
||||
callbacks: { signIn },
|
||||
secret,
|
||||
trustHost: true,
|
||||
},
|
||||
{
|
||||
path: "signin/email",
|
||||
@@ -94,6 +96,7 @@ it("Send e-mail to address with first domain", async () => {
|
||||
providers: [EmailProvider({ sendVerificationRequest })],
|
||||
callbacks: { signIn },
|
||||
secret,
|
||||
trustHost: true,
|
||||
},
|
||||
{
|
||||
path: "signin/email",
|
||||
@@ -140,6 +143,7 @@ it("Redirect to error page if multiple addresses aren't allowed", async () => {
|
||||
}),
|
||||
],
|
||||
secret,
|
||||
trustHost: true,
|
||||
},
|
||||
{
|
||||
path: "signin/email",
|
||||
|
||||
138
packages/next-auth/tests/getURL.test.ts
Normal file
138
packages/next-auth/tests/getURL.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { getURL as getURLOriginal } from "../src/utils/node"
|
||||
|
||||
it("Should return error when missing url", () => {
|
||||
expect(getURL(undefined, {})).toEqual(new Error("Missing url"))
|
||||
})
|
||||
|
||||
it("Should return error when missing host", () => {
|
||||
expect(getURL("/", {})).toEqual(new Error("Missing host"))
|
||||
})
|
||||
|
||||
it("Should return error when invalid protocol", () => {
|
||||
expect(
|
||||
getURL("/", { host: "localhost", "x-forwarded-proto": "file" })
|
||||
).toEqual(new Error("Invalid protocol"))
|
||||
})
|
||||
|
||||
it("Should return error when invalid host", () => {
|
||||
expect(getURL("/", { host: "/" })).toEqual(
|
||||
new TypeError("Invalid base URL: http:///")
|
||||
)
|
||||
})
|
||||
|
||||
it("Should read host headers", () => {
|
||||
expect(getURL("/api/auth/session", { host: "localhost" })).toBeURL(
|
||||
"http://localhost/api/auth/session"
|
||||
)
|
||||
|
||||
expect(
|
||||
getURL("/custom/api/auth/session", { "x-forwarded-host": "localhost:3000" })
|
||||
).toBeURL("http://localhost:3000/custom/api/auth/session")
|
||||
|
||||
// Prefer x-forwarded-host over host
|
||||
expect(
|
||||
getURL("/", { host: "localhost", "x-forwarded-host": "localhost:3000" })
|
||||
).toBeURL("http://localhost:3000/")
|
||||
})
|
||||
|
||||
it("Should read protocol headers", () => {
|
||||
expect(
|
||||
getURL("/", { host: "localhost", "x-forwarded-proto": "http" })
|
||||
).toBeURL("http://localhost/")
|
||||
})
|
||||
|
||||
describe("process.env.NEXTAUTH_URL", () => {
|
||||
afterEach(() => delete process.env.NEXTAUTH_URL)
|
||||
|
||||
it("Should prefer over headers if present", () => {
|
||||
process.env.NEXTAUTH_URL = "http://localhost:3000"
|
||||
expect(getURL("/api/auth/session", { host: "localhost" })).toBeURL(
|
||||
"http://localhost:3000/api/auth/session"
|
||||
)
|
||||
})
|
||||
|
||||
it("catch errors", () => {
|
||||
process.env.NEXTAUTH_URL = "invald-url"
|
||||
expect(getURL("/api/auth/session", {})).toEqual(
|
||||
new TypeError("Invalid URL: invald-url")
|
||||
)
|
||||
|
||||
process.env.NEXTAUTH_URL = "file://localhost"
|
||||
expect(getURL("/api/auth/session", {})).toEqual(
|
||||
new TypeError("Invalid protocol")
|
||||
)
|
||||
})
|
||||
|
||||
it("Supports custom base path", () => {
|
||||
process.env.NEXTAUTH_URL = "http://localhost:3000/custom/api/auth"
|
||||
expect(getURL("/api/auth/session", {})).toBeURL(
|
||||
"http://localhost:3000/custom/api/auth/session"
|
||||
)
|
||||
|
||||
// With trailing slash
|
||||
process.env.NEXTAUTH_URL = "http://localhost:3000/custom/api/auth/"
|
||||
expect(getURL("/api/auth/session", {})).toBeURL(
|
||||
"http://localhost:3000/custom/api/auth/session"
|
||||
)
|
||||
|
||||
// Multiple custom segments
|
||||
process.env.NEXTAUTH_URL = "http://localhost:3000/custom/path/api/auth"
|
||||
expect(getURL("/api/auth/session", {})).toBeURL(
|
||||
"http://localhost:3000/custom/path/api/auth/session"
|
||||
)
|
||||
|
||||
process.env.NEXTAUTH_URL = "http://localhost:3000/custom/path/api/auth/"
|
||||
expect(getURL("/api/auth/session", {})).toBeURL(
|
||||
"http://localhost:3000/custom/path/api/auth/session"
|
||||
)
|
||||
|
||||
// No /api/auth
|
||||
process.env.NEXTAUTH_URL = "http://localhost:3000/custom/nextauth"
|
||||
expect(getURL("/session", {})).toBeURL(
|
||||
"http://localhost:3000/custom/nextauth/session"
|
||||
)
|
||||
|
||||
// No /api/auth, with trailing slash
|
||||
process.env.NEXTAUTH_URL = "http://localhost:3000/custom/nextauth/"
|
||||
expect(getURL("/session", {})).toBeURL(
|
||||
"http://localhost:3000/custom/nextauth/session"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// Utils
|
||||
|
||||
function getURL(
|
||||
url: Parameters<typeof getURLOriginal>[0],
|
||||
headers: HeadersInit
|
||||
) {
|
||||
return getURLOriginal(url, new Headers(headers))
|
||||
}
|
||||
|
||||
expect.extend({
|
||||
toBeURL(rec, exp) {
|
||||
const r = rec.toString()
|
||||
const e = exp.toString()
|
||||
const printR = this.utils.printReceived
|
||||
const printE = this.utils.printExpected
|
||||
if (r === e) {
|
||||
return {
|
||||
message: () => `expected ${printE(e)} not to be ${printR(r)}`,
|
||||
pass: true,
|
||||
}
|
||||
}
|
||||
return {
|
||||
message: () => `expected ${printE(e)}, got ${printR(r)}`,
|
||||
pass: false,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace jest {
|
||||
interface Matchers<R> {
|
||||
toBeURL: (expected: string) => R
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,91 +6,64 @@ it("should not match pages as public paths", async () => {
|
||||
pages: { signIn: "/", error: "/" },
|
||||
secret: "secret",
|
||||
}
|
||||
|
||||
const req = new NextRequest("http://127.0.0.1/protected/pathA", {
|
||||
headers: { authorization: "" },
|
||||
})
|
||||
|
||||
const handleMiddleware = withAuth(options) as NextMiddleware
|
||||
const res = await handleMiddleware(req, null as any)
|
||||
expect(res).toBeDefined()
|
||||
expect(res?.status).toBe(307)
|
||||
|
||||
const response = await handleMiddleware(
|
||||
new NextRequest("http://127.0.0.1/protected/pathA"),
|
||||
null as any
|
||||
)
|
||||
|
||||
expect(response?.status).toBe(307)
|
||||
expect(response?.headers.get("location")).toBe(
|
||||
"http://localhost/?callbackUrl=%2Fprotected%2FpathA"
|
||||
)
|
||||
})
|
||||
|
||||
it("should not redirect on public paths", async () => {
|
||||
const options: NextAuthMiddlewareOptions = { secret: "secret" }
|
||||
|
||||
const req = new NextRequest("http://127.0.0.1/_next/foo", {
|
||||
headers: { authorization: "" },
|
||||
})
|
||||
const req = new NextRequest("http://127.0.0.1/_next/foo")
|
||||
|
||||
const handleMiddleware = withAuth(options) as NextMiddleware
|
||||
const res = await handleMiddleware(req, null as any)
|
||||
expect(res).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should redirect according to nextUrl basePath", async () => {
|
||||
it("should respect NextURL#basePath when redirecting", async () => {
|
||||
const options: NextAuthMiddlewareOptions = { secret: "secret" }
|
||||
|
||||
const req = {
|
||||
nextUrl: {
|
||||
pathname: "/protected/pathA",
|
||||
search: "",
|
||||
origin: "http://127.0.0.1",
|
||||
basePath: "/custom-base-path",
|
||||
},
|
||||
headers: new Headers({ authorization: "" }),
|
||||
}
|
||||
|
||||
const handleMiddleware = withAuth(options) as NextMiddleware
|
||||
const res = await handleMiddleware(req as NextRequest, null as any)
|
||||
expect(res).toBeDefined()
|
||||
expect(res?.status).toEqual(307)
|
||||
expect(res?.headers.get("location")).toContain(
|
||||
"http://127.0.0.1/custom-base-path/api/auth/signin?callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA"
|
||||
|
||||
const response1 = await handleMiddleware(
|
||||
{
|
||||
nextUrl: {
|
||||
pathname: "/protected/pathA",
|
||||
search: "",
|
||||
origin: "http://127.0.0.1",
|
||||
basePath: "/custom-base-path",
|
||||
},
|
||||
} as unknown as NextRequest,
|
||||
null as any
|
||||
)
|
||||
})
|
||||
|
||||
it("should redirect according to nextUrl basePath", async () => {
|
||||
// given
|
||||
const options: NextAuthMiddlewareOptions = { secret: "secret" }
|
||||
|
||||
const handleMiddleware = withAuth(options) as NextMiddleware
|
||||
|
||||
const req1 = {
|
||||
nextUrl: {
|
||||
pathname: "/protected/pathA",
|
||||
search: "",
|
||||
origin: "http://127.0.0.1",
|
||||
basePath: "/custom-base-path",
|
||||
},
|
||||
headers: new Headers({ authorization: "" }),
|
||||
}
|
||||
// when
|
||||
const res = await handleMiddleware(req1 as NextRequest, null as any)
|
||||
|
||||
// then
|
||||
expect(res).toBeDefined()
|
||||
expect(res?.status).toEqual(307)
|
||||
expect(res?.headers.get("location")).toContain(
|
||||
expect(response1?.status).toEqual(307)
|
||||
expect(response1?.headers.get("location")).toBe(
|
||||
"http://127.0.0.1/custom-base-path/api/auth/signin?callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA"
|
||||
)
|
||||
|
||||
const req2 = {
|
||||
nextUrl: {
|
||||
pathname: "/api/auth/signin",
|
||||
search: "callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA",
|
||||
origin: "http://127.0.0.1",
|
||||
basePath: "/custom-base-path",
|
||||
},
|
||||
headers: new Headers({ authorization: "" }),
|
||||
}
|
||||
// and when follow redirect
|
||||
const resFromRedirectedUrl = await handleMiddleware(
|
||||
req2 as NextRequest,
|
||||
// Should not redirect when invoked on sign in page
|
||||
|
||||
const response2 = await handleMiddleware(
|
||||
{
|
||||
nextUrl: {
|
||||
pathname: "/api/auth/signin",
|
||||
searchParams: new URLSearchParams({
|
||||
callbackUrl: "/custom-base-path/protected/pathA",
|
||||
}),
|
||||
origin: "http://127.0.0.1",
|
||||
basePath: "/custom-base-path",
|
||||
},
|
||||
} as unknown as NextRequest,
|
||||
null as any
|
||||
)
|
||||
|
||||
// then return sign in page
|
||||
expect(resFromRedirectedUrl).toBeUndefined()
|
||||
expect(response2).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -1,29 +1,47 @@
|
||||
// import { MissingAPIRoute } from "../src/core/errors"
|
||||
import { nodeHandler } from "./utils"
|
||||
|
||||
it("Missing req.url throws MISSING_NEXTAUTH_API_ROUTE_ERROR", async () => {
|
||||
const { res } = await nodeHandler()
|
||||
|
||||
expect(res.status).toBeCalledWith(400)
|
||||
// Moved to host detection in getUrl
|
||||
// expect(logger.error).toBeCalledTimes(1)
|
||||
// expect(logger.error).toBeCalledWith(
|
||||
// "MISSING_NEXTAUTH_API_ROUTE_ERROR",
|
||||
// expect.any(MissingAPIRoute)
|
||||
// )
|
||||
// expect(res.setHeader).toBeCalledWith("content-type", "application/json")
|
||||
// const body = res.send.mock.calls[0][0]
|
||||
// expect(JSON.parse(body)).toEqual({
|
||||
// message:
|
||||
// "There is a problem with the server configuration. Check the server logs for more information.",
|
||||
// })
|
||||
it("Missing req.url throws in dev", async () => {
|
||||
await expect(nodeHandler).rejects.toThrow(new Error("Missing url"))
|
||||
})
|
||||
|
||||
it("Missing host throws 400 in production", async () => {
|
||||
const configErrorMessage =
|
||||
"There is a problem with the server configuration. Check the server logs for more information."
|
||||
|
||||
it("Missing req.url returns config error in prod", async () => {
|
||||
// @ts-expect-error
|
||||
process.env.NODE_ENV = "production"
|
||||
const { res } = await nodeHandler()
|
||||
const { res, logger } = await nodeHandler()
|
||||
|
||||
expect(logger.error).toBeCalledTimes(1)
|
||||
const error = new Error("Missing url")
|
||||
expect(logger.error).toBeCalledWith("INVALID_URL", error)
|
||||
|
||||
expect(res.status).toBeCalledWith(400)
|
||||
expect(res.json).toBeCalledWith({ message: configErrorMessage })
|
||||
|
||||
// @ts-expect-error
|
||||
process.env.NODE_ENV = "test"
|
||||
})
|
||||
|
||||
it("Missing host throws in dev", async () => {
|
||||
await expect(
|
||||
async () =>
|
||||
await nodeHandler({
|
||||
req: { query: { nextauth: ["session"] } },
|
||||
})
|
||||
).rejects.toThrow(Error)
|
||||
})
|
||||
|
||||
it("Missing host config error in prod", async () => {
|
||||
// @ts-expect-error
|
||||
process.env.NODE_ENV = "production"
|
||||
const { res, logger } = await nodeHandler({
|
||||
req: { query: { nextauth: ["session"] } },
|
||||
})
|
||||
expect(res.status).toBeCalledWith(400)
|
||||
expect(res.json).toBeCalledWith({ message: configErrorMessage })
|
||||
|
||||
expect(logger.error).toBeCalledWith("INVALID_URL", new Error("Missing url"))
|
||||
// @ts-expect-error
|
||||
process.env.NODE_ENV = "test"
|
||||
})
|
||||
@@ -82,12 +100,43 @@ it("Redirects if necessary", async () => {
|
||||
req: {
|
||||
method: "post",
|
||||
url: "/api/auth/signin/github",
|
||||
body: { json: "true" },
|
||||
},
|
||||
})
|
||||
expect(res.status).toBeCalledWith(302)
|
||||
expect(res.removeHeader).toBeCalledWith("Location")
|
||||
expect(res.json).toBeCalledWith({
|
||||
url: "http://localhost/api/auth/signin?csrf=true",
|
||||
})
|
||||
expect(res.setHeader).toBeCalledWith("set-cookie", [
|
||||
expect.stringMatching(
|
||||
/next-auth.csrf-token=.*; Path=\/; HttpOnly; SameSite=Lax/
|
||||
),
|
||||
`next-auth.callback-url=${encodeURIComponent(
|
||||
process.env.NEXTAUTH_URL
|
||||
)}; Path=/; HttpOnly; SameSite=Lax`,
|
||||
])
|
||||
expect(res.setHeader).toBeCalledTimes(2)
|
||||
expect(res.send).toBeCalledWith("")
|
||||
})
|
||||
|
||||
it("Returns redirect if `X-Auth-Return-Redirect` header is present", async () => {
|
||||
process.env.NEXTAUTH_URL = "http://localhost"
|
||||
const { res } = await nodeHandler({
|
||||
req: {
|
||||
method: "post",
|
||||
url: "/api/auth/signin/github",
|
||||
headers: { "X-Auth-Return-Redirect": "1" },
|
||||
},
|
||||
})
|
||||
|
||||
expect(res.status).toBeCalledWith(200)
|
||||
expect(res.setHeader).toBeCalledWith("content-type", "application/json")
|
||||
expect(res.setHeader).toBeCalledWith("set-cookie", [
|
||||
expect.stringMatching(
|
||||
/next-auth.csrf-token=.*; Path=\/; HttpOnly; SameSite=Lax/
|
||||
),
|
||||
`next-auth.callback-url=${encodeURIComponent(
|
||||
process.env.NEXTAUTH_URL
|
||||
)}; Path=/; HttpOnly; SameSite=Lax`,
|
||||
])
|
||||
expect(res.setHeader).toBeCalledTimes(2)
|
||||
expect(res.send).toBeCalledWith(
|
||||
JSON.stringify({ url: "http://localhost/api/auth/signin?csrf=true" })
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user