mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
10 Commits
next-auth@
...
next-auth@
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2913fbac3b | ||
|
|
2875b49f11 | ||
|
|
5259d247a2 | ||
|
|
d1d93fd75e | ||
|
|
62f672ae30 | ||
|
|
2c669b32fc | ||
|
|
2dea8919e5 | ||
|
|
6fdb0da6eb | ||
|
|
5c4a9a697d | ||
|
|
eddd8fd7f9 |
@@ -46,6 +46,10 @@ title: Tutorials and Explainers
|
||||
- Learn how to use Sign-In With Ethereum to authenticate your users with their existing Ethereum wallets - identifiers they personally control.
|
||||
- Example application: [spruceid/siwe-next-auth-example](https://github.com/spruceid/siwe-next-auth-example)
|
||||
|
||||
#### [Next.js Authentication with Okta and NextAuth.js 4.0](https://thetombomb.com/posts/nextjs-nextauth-okta) <svg xmlns="http://www.w3.org/2000/svg" style={{ marginLeft: '5px', marginBottom:'-6px'}} height="20" width="20" fill="none" viewBox="0 0 24 24" stroke="currentColor"><title>External</title> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> </svg>
|
||||
|
||||
- Learn how to perform authentication with an OIDC Application in Okta and NextAuth.js.
|
||||
|
||||
## Fullstack
|
||||
|
||||
#### [Build a FullStack App with Next.js, NextAuth.js, Supabase & Prisma](https://themodern.dev/courses/build-a-fullstack-app-with-nextjs-supabase-and-prisma-322389284337222224) <svg xmlns="http://www.w3.org/2000/svg" style={{ marginLeft: '5px', marginBottom:'-6px'}} height="20" width="20" fill="none" viewBox="0 0 24 24" stroke="currentColor"><title>External</title> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> </svg>
|
||||
|
||||
@@ -14,6 +14,10 @@ If you did not find a guide or tutorial covering your use case, please [open an
|
||||
- How to restrict access to pages and API routes.
|
||||
- [Usage with class components](/tutorials/usage-with-class-components)
|
||||
- How to use `useSession()` hook with class components.
|
||||
- [Next.js Authentication with Okta and NextAuth.js 4.0](https://thetombomb.com/posts/nextjs-nextauth-okta)
|
||||
- Learn how to perform authentication with an OIDC Application in Okta and NextAuth.js.
|
||||
|
||||
|
||||
|
||||
### Advanced
|
||||
|
||||
|
||||
@@ -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.3",
|
||||
"version": "4.18.6",
|
||||
"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 } from "../utils/node"
|
||||
import { getBody, getURL, setHeaders } from "../utils/node"
|
||||
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
@@ -18,38 +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)
|
||||
|
||||
for (const [key, val] of headers.entries()) {
|
||||
const value = key === "set-cookie" ? val.split(",") : val
|
||||
res.setHeader(key, value)
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
@@ -142,26 +140,31 @@ 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
|
||||
|
||||
for (const [key, val] of headers.entries()) {
|
||||
const value = key === "set-cookie" ? val.split(",") : val
|
||||
res.setHeader(key, value)
|
||||
}
|
||||
setHeaders(headers, res)
|
||||
|
||||
// This would otherwise break rendering
|
||||
// with `getServerSideProps` that needs to always return HTML
|
||||
res.removeHeader?.("Content-Type")
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
|
||||
@@ -6,7 +6,17 @@ import { NextResponse, NextRequest } from "next/server"
|
||||
|
||||
import { getToken } from "../jwt"
|
||||
import parseUrl from "../utils/parse-url"
|
||||
import { getURL } from "../utils/node"
|
||||
|
||||
// // TODO: Remove
|
||||
/** Extract the host from the environment */
|
||||
export function detectHost(
|
||||
trusted: boolean,
|
||||
forwardedValue: string | null,
|
||||
defaultValue: string | false
|
||||
): string | undefined {
|
||||
if (trusted && forwardedValue) return forwardedValue
|
||||
return defaultValue || undefined
|
||||
}
|
||||
|
||||
type AuthorizedCallback = (params: {
|
||||
token: JWT | null
|
||||
@@ -113,18 +123,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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IncomingMessage } from "http"
|
||||
import type { IncomingMessage, ServerResponse } from "http"
|
||||
import type { GetServerSidePropsContext, NextApiRequest } from "next"
|
||||
|
||||
export function setCookie(res, value: string) {
|
||||
@@ -25,35 +25,135 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set-Cookie header field-values are sometimes comma joined in one string. This splits them without choking on commas
|
||||
* that are within a single set-cookie field-value, such as in the Expires portion.
|
||||
* This is uncommon, but explicitly allowed - see https://tools.ietf.org/html/rfc2616#section-4.2
|
||||
* Node.js does this for every header *except* set-cookie - see https://github.com/nodejs/node/blob/d5e363b77ebaf1caf67cd7528224b651c86815c1/lib/_http_incoming.js#L128
|
||||
* Based on: https://github.com/google/j2objc/commit/16820fdbc8f76ca0c33472810ce0cb03d20efe25
|
||||
* Credits to: https://github.com/tomball for original and https://github.com/chrusart for JavaScript implementation
|
||||
* @source https://github.com/nfriedly/set-cookie-parser/blob/3eab8b7d5d12c8ed87832532861c1a35520cf5b3/lib/set-cookie.js#L144
|
||||
*/
|
||||
function getSetCookies(cookiesString: string) {
|
||||
if (typeof cookiesString !== "string") {
|
||||
return []
|
||||
}
|
||||
|
||||
const cookiesStrings: string[] = []
|
||||
let pos = 0
|
||||
let start
|
||||
let ch
|
||||
let lastComma: number
|
||||
let nextStart
|
||||
let cookiesSeparatorFound
|
||||
|
||||
function skipWhitespace() {
|
||||
while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) {
|
||||
pos += 1
|
||||
}
|
||||
return pos < cookiesString.length
|
||||
}
|
||||
|
||||
function notSpecialChar() {
|
||||
ch = cookiesString.charAt(pos)
|
||||
|
||||
return ch !== "=" && ch !== ";" && ch !== ","
|
||||
}
|
||||
|
||||
while (pos < cookiesString.length) {
|
||||
start = pos
|
||||
cookiesSeparatorFound = false
|
||||
|
||||
while (skipWhitespace()) {
|
||||
ch = cookiesString.charAt(pos)
|
||||
if (ch === ",") {
|
||||
// ',' is a cookie separator if we have later first '=', not ';' or ','
|
||||
lastComma = pos
|
||||
pos += 1
|
||||
|
||||
skipWhitespace()
|
||||
nextStart = pos
|
||||
|
||||
while (pos < cookiesString.length && notSpecialChar()) {
|
||||
pos += 1
|
||||
}
|
||||
|
||||
// currently special character
|
||||
if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") {
|
||||
// we found cookies separator
|
||||
cookiesSeparatorFound = true
|
||||
// pos is inside the next cookie, so back up and return it.
|
||||
pos = nextStart
|
||||
cookiesStrings.push(cookiesString.substring(start, lastComma))
|
||||
start = pos
|
||||
} else {
|
||||
// in param ',' or param separator ';',
|
||||
// we continue from that comma
|
||||
pos = lastComma + 1
|
||||
}
|
||||
} else {
|
||||
pos += 1
|
||||
}
|
||||
}
|
||||
|
||||
if (!cookiesSeparatorFound || pos >= cookiesString.length) {
|
||||
cookiesStrings.push(cookiesString.substring(start, cookiesString.length))
|
||||
}
|
||||
}
|
||||
|
||||
return cookiesStrings
|
||||
}
|
||||
|
||||
export function setHeaders(headers: Headers, res: ServerResponse) {
|
||||
for (const [key, val] of headers.entries()) {
|
||||
let value: string | string[] = val
|
||||
// See: https://github.com/whatwg/fetch/issues/973
|
||||
if (key === "set-cookie") {
|
||||
const cookies = getSetCookies(value)
|
||||
let original = res.getHeader("set-cookie") as string[] | string
|
||||
original = Array.isArray(original) ? original : [original]
|
||||
value = original.concat(cookies).filter(Boolean)
|
||||
}
|
||||
res.setHeader(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace NodeJS {
|
||||
|
||||
@@ -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,51 @@ 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: url.toString() should not include 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
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
action,
|
||||
providerId,
|
||||
method: req.method ?? "GET",
|
||||
headers: Object.fromEntries(req.headers),
|
||||
body: req.body ? await readJSONBody(req.body) : undefined,
|
||||
cookies: parseCookie(req.headers.get("cookie") ?? "") ?? {},
|
||||
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
|
||||
|
||||
@@ -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"
|
||||
import { mockReqRes, nextHandler } 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(nextHandler).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 nextHandler()
|
||||
|
||||
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 nextHandler({
|
||||
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 nextHandler({
|
||||
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"
|
||||
})
|
||||
@@ -31,7 +49,7 @@ it("Missing host throws 400 in production", async () => {
|
||||
it("Defined host throws 400 in production if not trusted", async () => {
|
||||
// @ts-expect-error
|
||||
process.env.NODE_ENV = "production"
|
||||
const { res } = await nodeHandler({
|
||||
const { res } = await nextHandler({
|
||||
req: { headers: { host: "http://localhost" } },
|
||||
})
|
||||
expect(res.status).toBeCalledWith(400)
|
||||
@@ -42,7 +60,7 @@ it("Defined host throws 400 in production if not trusted", async () => {
|
||||
it("Defined host throws 400 in production if trusted but invalid URL", async () => {
|
||||
// @ts-expect-error
|
||||
process.env.NODE_ENV = "production"
|
||||
const { res } = await nodeHandler({
|
||||
const { res } = await nextHandler({
|
||||
req: { headers: { host: "localhost" } },
|
||||
options: { trustHost: true },
|
||||
})
|
||||
@@ -54,7 +72,7 @@ it("Defined host throws 400 in production if trusted but invalid URL", async ()
|
||||
it("Defined host does not throw in production if trusted and valid URL", async () => {
|
||||
// @ts-expect-error
|
||||
process.env.NODE_ENV = "production"
|
||||
const { res } = await nodeHandler({
|
||||
const { res } = await nextHandler({
|
||||
req: {
|
||||
url: "/api/auth/session",
|
||||
headers: { host: "http://localhost" },
|
||||
@@ -62,6 +80,7 @@ it("Defined host does not throw in production if trusted and valid URL", async (
|
||||
options: { trustHost: true },
|
||||
})
|
||||
expect(res.status).toBeCalledWith(200)
|
||||
// @ts-expect-error
|
||||
expect(JSON.parse(res.send.mock.calls[0][0])).toEqual({})
|
||||
// @ts-expect-error
|
||||
process.env.NODE_ENV = "test"
|
||||
@@ -69,25 +88,92 @@ it("Defined host does not throw in production if trusted and valid URL", async (
|
||||
|
||||
it("Use process.env.NEXTAUTH_URL for host if present", async () => {
|
||||
process.env.NEXTAUTH_URL = "http://localhost"
|
||||
const { res } = await nodeHandler({
|
||||
const { res } = await nextHandler({
|
||||
req: { url: "/api/auth/session" },
|
||||
})
|
||||
expect(res.status).toBeCalledWith(200)
|
||||
// @ts-expect-error
|
||||
expect(JSON.parse(res.send.mock.calls[0][0])).toEqual({})
|
||||
})
|
||||
|
||||
it("Redirects if necessary", async () => {
|
||||
process.env.NEXTAUTH_URL = "http://localhost"
|
||||
const { res } = await nodeHandler({
|
||||
const { res } = await nextHandler({
|
||||
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.getHeaders()).toEqual({
|
||||
location: "http://localhost/api/auth/signin?csrf=true",
|
||||
"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.send).toBeCalledWith("")
|
||||
})
|
||||
|
||||
it("Returns redirect if `X-Auth-Return-Redirect` header is present", async () => {
|
||||
process.env.NEXTAUTH_URL = "http://localhost"
|
||||
const { res } = await nextHandler({
|
||||
req: {
|
||||
method: "post",
|
||||
url: "/api/auth/signin/github",
|
||||
headers: { "X-Auth-Return-Redirect": "1" },
|
||||
},
|
||||
})
|
||||
|
||||
expect(res.status).toBeCalledWith(200)
|
||||
|
||||
expect(res.getHeaders()).toEqual({
|
||||
"content-type": "application/json",
|
||||
"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.send).toBeCalledWith(
|
||||
JSON.stringify({ url: "http://localhost/api/auth/signin?csrf=true" })
|
||||
)
|
||||
})
|
||||
|
||||
it("Should preserve user's `set-cookie` headers", async () => {
|
||||
const { req, res } = mockReqRes({
|
||||
method: "post",
|
||||
url: "/api/auth/signin/credentials",
|
||||
headers: { host: "localhost", "X-Auth-Return-Redirect": "1" },
|
||||
})
|
||||
res.setHeader("set-cookie", ["foo=bar", "bar=baz"])
|
||||
|
||||
await nextHandler({ req, res })
|
||||
|
||||
expect(res.getHeaders()).toEqual({
|
||||
"content-type": "application/json",
|
||||
"set-cookie": [
|
||||
"foo=bar",
|
||||
"bar=baz",
|
||||
expect.stringMatching(
|
||||
/next-auth.csrf-token=.*; Path=\/; HttpOnly; SameSite=Lax/
|
||||
),
|
||||
`next-auth.callback-url=${encodeURIComponent(
|
||||
"http://localhost"
|
||||
)}; Path=/; HttpOnly; SameSite=Lax`,
|
||||
],
|
||||
})
|
||||
|
||||
expect(res.send).toBeCalledWith(
|
||||
JSON.stringify({ url: "http://localhost/api/auth/signin?csrf=true" })
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { createHash } from "crypto"
|
||||
import { AuthHandler } from "../src/core"
|
||||
import type { LoggerInstance, AuthOptions } from "../src"
|
||||
import { createHash } from "node:crypto"
|
||||
import { IncomingMessage, ServerResponse } from "node:http"
|
||||
import { Socket } from "node:net"
|
||||
import type { AuthOptions, LoggerInstance } from "../src"
|
||||
import type { Adapter } from "../src/adapters"
|
||||
import { AuthHandler } from "../src/core"
|
||||
|
||||
import NextAuth from "../src/next"
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from "next"
|
||||
import { Stream } from "node:stream"
|
||||
|
||||
export function mockLogger(): Record<keyof LoggerInstance, jest.Mock> {
|
||||
return {
|
||||
@@ -79,38 +82,143 @@ export function mockAdapter(): Adapter {
|
||||
return adapter
|
||||
}
|
||||
|
||||
export async function nodeHandler(
|
||||
export async function nextHandler(
|
||||
params: {
|
||||
req?: Partial<NextApiRequest>
|
||||
res?: Partial<NextApiResponse>
|
||||
options?: Partial<AuthOptions>
|
||||
} = {}
|
||||
) {
|
||||
const req = {
|
||||
body: {},
|
||||
cookies: {},
|
||||
headers: {},
|
||||
method: "GET",
|
||||
...params.req,
|
||||
}
|
||||
|
||||
const res = {
|
||||
...params.res,
|
||||
end: jest.fn(),
|
||||
json: jest.fn(),
|
||||
status: jest.fn().mockReturnValue({ end: jest.fn() }),
|
||||
setHeader: jest.fn(),
|
||||
removeHeader: jest.fn(),
|
||||
send: jest.fn(),
|
||||
let req = params.req
|
||||
// @ts-expect-error
|
||||
let res: NextApiResponse = params.res
|
||||
if (!params.res) {
|
||||
;({ req, res } = mockReqRes(params.req))
|
||||
}
|
||||
|
||||
const logger = mockLogger()
|
||||
|
||||
await NextAuth(req as any, res as any, {
|
||||
// @ts-expect-error
|
||||
await NextAuth(req, res, {
|
||||
providers: [],
|
||||
secret: "secret",
|
||||
logger,
|
||||
...params.options,
|
||||
})
|
||||
|
||||
return { req, res, logger }
|
||||
}
|
||||
|
||||
export function mockReqRes(req?: Partial<NextApiRequest>): {
|
||||
req: NextApiRequest
|
||||
res: NextApiResponse
|
||||
} {
|
||||
const request = new IncomingMessage(new Socket())
|
||||
request.headers = req?.headers ?? {}
|
||||
request.method = req?.method
|
||||
request.url = req?.url
|
||||
|
||||
const response = new ServerResponse(request)
|
||||
// @ts-expect-error
|
||||
response.status = (code) => (response.statusCode = code)
|
||||
// @ts-expect-error
|
||||
response.send = (data) => sendData(request, response, data)
|
||||
// @ts-expect-error
|
||||
response.json = (data) => sendJson(response, data)
|
||||
|
||||
const res: NextApiResponse = {
|
||||
...response,
|
||||
// @ts-expect-error
|
||||
setHeader: jest.spyOn(response, "setHeader"),
|
||||
// @ts-expect-error
|
||||
getHeader: jest.spyOn(response, "getHeader"),
|
||||
// @ts-expect-error
|
||||
removeHeader: jest.spyOn(response, "removeHeader"),
|
||||
// @ts-expect-error
|
||||
status: jest.spyOn(response, "status"),
|
||||
// @ts-expect-error
|
||||
send: jest.spyOn(response, "send"),
|
||||
// @ts-expect-error
|
||||
json: jest.spyOn(response, "json"),
|
||||
// @ts-expect-error
|
||||
end: jest.spyOn(response, "end"),
|
||||
// @ts-expect-error
|
||||
getHeaders: jest.spyOn(response, "getHeaders"),
|
||||
}
|
||||
|
||||
return { req: request as any, res }
|
||||
}
|
||||
|
||||
// Code below is copied from Next.js
|
||||
// https://github.com/vercel/next.js/tree/canary/packages/next/server/api-utils
|
||||
// TODO: Remove
|
||||
|
||||
/**
|
||||
* Send `any` body to response
|
||||
* @param req request object
|
||||
* @param res response object
|
||||
* @param body of response
|
||||
*/
|
||||
function sendData(req: NextApiRequest, res: NextApiResponse, body: any): void {
|
||||
if (body === null || body === undefined) {
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
// strip irrelevant headers/body
|
||||
if (res.statusCode === 204 || res.statusCode === 304) {
|
||||
res.removeHeader("Content-Type")
|
||||
res.removeHeader("Content-Length")
|
||||
res.removeHeader("Transfer-Encoding")
|
||||
|
||||
if (process.env.NODE_ENV === "development" && body) {
|
||||
console.warn(
|
||||
`A body was attempted to be set with a 204 statusCode for ${req.url}, this is invalid and the body was ignored.\n` +
|
||||
`See more info here https://nextjs.org/docs/messages/invalid-api-status-body`
|
||||
)
|
||||
}
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
const contentType = res.getHeader("Content-Type")
|
||||
|
||||
if (body instanceof Stream) {
|
||||
if (!contentType) {
|
||||
res.setHeader("Content-Type", "application/octet-stream")
|
||||
}
|
||||
body.pipe(res)
|
||||
return
|
||||
}
|
||||
|
||||
const isJSONLike = ["object", "number", "boolean"].includes(typeof body)
|
||||
const stringifiedBody = isJSONLike ? JSON.stringify(body) : body
|
||||
|
||||
if (Buffer.isBuffer(body)) {
|
||||
if (!contentType) {
|
||||
res.setHeader("Content-Type", "application/octet-stream")
|
||||
}
|
||||
res.setHeader("Content-Length", body.length)
|
||||
res.end(body)
|
||||
return
|
||||
}
|
||||
|
||||
if (isJSONLike) {
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8")
|
||||
}
|
||||
|
||||
res.setHeader("Content-Length", Buffer.byteLength(stringifiedBody))
|
||||
res.end(stringifiedBody)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send `JSON` object
|
||||
* @param res response object
|
||||
* @param jsonBody of data
|
||||
*/
|
||||
function sendJson(res: NextApiResponse, jsonBody: any): void {
|
||||
// Set header to application/json
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
// Use send to handle request
|
||||
res.send(JSON.stringify(jsonBody))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user