Skip to content

Commit

Permalink
Merge branch 'main' into feat/oauth4webapi-balazs
Browse files Browse the repository at this point in the history
  • Loading branch information
balazsorban44 committed Dec 12, 2022
2 parents 0b8d3fd + 2913fba commit 67c525b
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 76 deletions.
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",
Expand Down
12 changes: 11 additions & 1 deletion packages/next-auth/src/next/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion packages/next-auth/src/utils/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
23 changes: 2 additions & 21 deletions packages/next-auth/src/utils/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
}
Expand Down Expand Up @@ -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 =
Expand All @@ -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")
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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 },
})
Expand All @@ -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",
Expand All @@ -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" })
)
Expand Down
Loading

0 comments on commit 67c525b

Please sign in to comment.