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

fix(core): handle Request -> Response regressions #5991

Merged
merged 3 commits into from
Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Prev Previous commit
Next Next commit
fix(core): handle , while setting set-cookie
  • Loading branch information
balazsorban44 committed Dec 8, 2022
commit 7a390844c8db3c548ca0155f16d6a033693a3fbf
9 changes: 4 additions & 5 deletions packages/next-auth/src/next/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AuthHandler } from "../core"
import { getURL, getBody } from "../utils/node"
import { getURL, getBody, setHeaders } from "../utils/node"

import type {
GetServerSidePropsContext,
Expand Down Expand Up @@ -37,10 +37,7 @@ async function NextAuthHandler(
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)
}
setHeaders(headers, res)

// If the request expects a return URL, send it as JSON
// instead of doing an actual redirect.
Expand Down Expand Up @@ -158,6 +155,8 @@ export async function unstable_getServerSession<

const { status = 200, headers } = response

setHeaders(headers, res)

// This would otherwise break rendering
// with `getServerSideProps` that needs to always return HTML
res.removeHeader?.("Content-Type")
Expand Down
89 changes: 88 additions & 1 deletion packages/next-auth/src/utils/node.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -54,6 +54,93 @@ export function getURL(
}
}

/**
* 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()) {
// See: https://github.com/whatwg/fetch/issues/973
const value = key === "set-cookie" ? getSetCookies(val) : val
res.setHeader(key, value)
}
}

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace NodeJS {
Expand Down