diff --git a/packages/next-auth/src/next/index.ts b/packages/next-auth/src/next/index.ts index 51cdbb6505..62f390a2f9 100644 --- a/packages/next-auth/src/next/index.ts +++ b/packages/next-auth/src/next/index.ts @@ -1,5 +1,5 @@ import { AuthHandler } from "../core" -import { getURL, getBody } from "../utils/node" +import { getURL, getBody, setHeaders } from "../utils/node" import type { GetServerSidePropsContext, @@ -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. @@ -158,10 +155,11 @@ export async function unstable_getServerSession< 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() diff --git a/packages/next-auth/src/utils/node.ts b/packages/next-auth/src/utils/node.ts index 0512d09e44..341de6caaf 100644 --- a/packages/next-auth/src/utils/node.ts +++ b/packages/next-auth/src/utils/node.ts @@ -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) { @@ -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 {