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 | |
|---|---|---|---|
|
|
2913fbac3b | ||
|
|
2875b49f11 | ||
|
|
5259d247a2 | ||
|
|
d1d93fd75e |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-auth",
|
||||
"version": "4.18.5",
|
||||
"version": "4.18.6",
|
||||
"description": "Authentication for Next.js",
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/next-auth.git",
|
||||
|
||||
@@ -6,7 +6,17 @@ import { NextResponse, NextRequest } from "next/server"
|
||||
|
||||
import { getToken } from "../jwt"
|
||||
import parseUrl from "../utils/parse-url"
|
||||
import { detectHost } from "../utils/web"
|
||||
|
||||
// // 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
|
||||
|
||||
@@ -142,8 +142,14 @@ function getSetCookies(cookiesString: string) {
|
||||
|
||||
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
|
||||
const value = key === "set-cookie" ? getSetCookies(val) : val
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function toInternalRequest(
|
||||
req: Request
|
||||
): Promise<RequestInternal | Error> {
|
||||
try {
|
||||
// TODO: .toString() should not inclide action and providerId
|
||||
// TODO: url.toString() should not include action and providerId
|
||||
// see init.ts
|
||||
const url = new URL(req.url.replace(/\/$/, ""))
|
||||
const { pathname } = url
|
||||
@@ -69,8 +69,6 @@ export async function toInternalRequest(
|
||||
providerId = providerIdOrAction
|
||||
}
|
||||
|
||||
const cookieHeader = req.headers.get("cookie") ?? ""
|
||||
|
||||
return {
|
||||
url,
|
||||
action,
|
||||
@@ -78,10 +76,7 @@ export async function toInternalRequest(
|
||||
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
|
||||
) ?? {},
|
||||
cookies: parseCookie(req.headers.get("cookie") ?? "") ?? {},
|
||||
error: url.searchParams.get("error") ?? undefined,
|
||||
query: Object.fromEntries(url.searchParams),
|
||||
}
|
||||
@@ -119,17 +114,3 @@ 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
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { nodeHandler } from "./utils"
|
||||
import { mockReqRes, nextHandler } from "./utils"
|
||||
|
||||
it("Missing req.url throws in dev", async () => {
|
||||
await expect(nodeHandler).rejects.toThrow(new Error("Missing url"))
|
||||
await expect(nextHandler).rejects.toThrow(new Error("Missing url"))
|
||||
})
|
||||
|
||||
const configErrorMessage =
|
||||
@@ -10,7 +10,7 @@ const configErrorMessage =
|
||||
it("Missing req.url returns config error in prod", async () => {
|
||||
// @ts-expect-error
|
||||
process.env.NODE_ENV = "production"
|
||||
const { res, logger } = await nodeHandler()
|
||||
const { res, logger } = await nextHandler()
|
||||
|
||||
expect(logger.error).toBeCalledTimes(1)
|
||||
const error = new Error("Missing url")
|
||||
@@ -26,7 +26,7 @@ it("Missing req.url returns config error in prod", async () => {
|
||||
it("Missing host throws in dev", async () => {
|
||||
await expect(
|
||||
async () =>
|
||||
await nodeHandler({
|
||||
await nextHandler({
|
||||
req: { query: { nextauth: ["session"] } },
|
||||
})
|
||||
).rejects.toThrow(Error)
|
||||
@@ -35,7 +35,7 @@ it("Missing host throws in dev", async () => {
|
||||
it("Missing host config error in prod", async () => {
|
||||
// @ts-expect-error
|
||||
process.env.NODE_ENV = "production"
|
||||
const { res, logger } = await nodeHandler({
|
||||
const { res, logger } = await nextHandler({
|
||||
req: { query: { nextauth: ["session"] } },
|
||||
})
|
||||
expect(res.status).toBeCalledWith(400)
|
||||
@@ -49,7 +49,7 @@ it("Missing host config error in prod", 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)
|
||||
@@ -60,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 },
|
||||
})
|
||||
@@ -72,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" },
|
||||
@@ -80,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"
|
||||
@@ -87,37 +88,41 @@ 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",
|
||||
},
|
||||
})
|
||||
expect(res.status).toBeCalledWith(302)
|
||||
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.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 nodeHandler({
|
||||
const { res } = await nextHandler({
|
||||
req: {
|
||||
method: "post",
|
||||
url: "/api/auth/signin/github",
|
||||
@@ -126,16 +131,48 @@ it("Returns redirect if `X-Auth-Return-Redirect` header is present", async () =>
|
||||
})
|
||||
|
||||
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.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