Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Sync #2

Merged
Merged
Changes from 4 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f329102
fix(core): properly construct url (#5984)
balazsorban44 Dec 8, 2022
f856363
chore(release): bump package version(s) [skip ci]
balazsorban44 Dec 8, 2022
221bc8e
fix(core): add protocol if missing
balazsorban44 Dec 8, 2022
157269e
fix(core): throw error if no action can be determined
balazsorban44 Dec 8, 2022
0a140cd
test(core): fix test
balazsorban44 Dec 8, 2022
b74bfc6
chore(release): bump package version(s) [skip ci]
balazsorban44 Dec 8, 2022
7444ab3
Merge branch 'main' into feat/oauth4webapi-balazs
balazsorban44 Dec 8, 2022
eddd8fd
chore(docs): add new tutorial (#5604)
thomas-desmond Dec 8, 2022
5c4a9a6
fix(core): handle `Request` -> `Response` regressions (#5991)
balazsorban44 Dec 8, 2022
6fdb0da
chore(release): bump package version(s) [skip ci]
balazsorban44 Dec 8, 2022
2dea891
fix(sequelize): increase sequelize `id_token` column length (#5929)
pCyril Dec 10, 2022
2c669b3
fix(core): correct status code when returning redirects (#6004)
balazsorban44 Dec 11, 2022
62f672a
fix(core): host detection/NEXTAUTH_URL (#6007)
balazsorban44 Dec 11, 2022
d1d93fd
chore(release): bump package version(s) [skip ci]
balazsorban44 Dec 11, 2022
7f3a6ca
Merge branch 'main' into feat/oauth4webapi-balazs
balazsorban44 Dec 11, 2022
0b8d3fd
update lock file
balazsorban44 Dec 11, 2022
5259d24
fix(next): correctly bundle next-auth/middleware
balazsorban44 Dec 12, 2022
2875b49
fix(core): preserve incoming set cookies (#6029)
balazsorban44 Dec 12, 2022
2913fba
chore(release): bump package version(s) [skip ci]
balazsorban44 Dec 12, 2022
67c525b
Merge branch 'main' into feat/oauth4webapi-balazs
balazsorban44 Dec 12, 2022
fa864e1
make logos optional
balazsorban44 Dec 12, 2022
92cfb91
sync with `next-auth`
balazsorban44 Dec 12, 2022
0469fc6
clean up `next-auth/edge`
balazsorban44 Dec 12, 2022
73026a4
sync
balazsorban44 Dec 12, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/next-auth/package.json
Original file line number Diff line number Diff line change
@@ -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",
12 changes: 11 additions & 1 deletion packages/next-auth/src/next/middleware.ts
Original file line number Diff line number Diff line change
@@ -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
8 changes: 7 additions & 1 deletion packages/next-auth/src/utils/node.ts
Original file line number Diff line number Diff line change
@@ -147,8 +147,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)
}
}
23 changes: 2 additions & 21 deletions packages/next-auth/src/utils/web.ts
Original file line number Diff line number Diff line change
@@ -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,19 +69,14 @@ export async function toInternalRequest(
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
) ?? {},
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
}
97 changes: 67 additions & 30 deletions packages/next-auth/tests/next.test.ts
Original file line number Diff line number Diff line change
@@ -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,52 +72,57 @@ 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" },
},
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"
})

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" })
)
152 changes: 130 additions & 22 deletions packages/next-auth/tests/utils.ts
Original file line number Diff line number Diff line change
@@ -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))
}