Repository: auth0/nextjs-auth0 (ts)

Issue:
User didn't successfully log in due to the cookie is too big and spilt into two piece, nextjs edge-runtime not correctly set the cookie
### Checklist

- [X] The issue can be reproduced in the [nextjs-auth0 sample app](https://github.com/auth0-samples/auth0-nextjs-samples/tree/main/Sample-01) (or N/A).
- [X] I have looked into the [Readme](https://github.com/auth0/nextjs-auth0#readme), [Examples](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md), and [FAQ](https://github.com/auth0/nextjs-auth0/blob/main/FAQ.md) and have not found a suitable solution or answer.
- [X] I have looked into the [API documentation](https://auth0.github.io/nextjs-auth0/) and have not found a suitable solution or answer.
- [X] I have searched the [issues](https://github.com/auth0/nextjs-auth0/issues) and have not found a suitable solution or answer.
- [X] I have searched the [Auth0 Community](https://community.auth0.com) forums and have not found a suitable solution or answer.
- [X] I agree to the terms within the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md).

### Description

Hi,

We have a problem that user can not successfully login. We recognize the issue to be similar to this one #1045. 

In short, our user has a large profile photo url which makes the cookie exceed 4MB limit. Auth spilt it into two piece but due to Vercel edge-runtime can't successfully handle it https://github.com/vercel/next.js/issues/38302 (It seems like that they close it but the issue persist).

We are solving this issue by not requiring the "profile" scope which is not ideal in our use-case.

Is there any workaround on Auth0 side to make this work?

### Reproduction

### This is our configuration

```ts
import {
  AuthError,
  handleAuth,
  handleCallback,
  handleLogin,
  handleLogout,
} from "@auth0/nextjs-auth0";
import { NextApiRequest } from "next";

const audience = process.env.AUTH0_OAUTH_AUDIENCE;
const scope = process.env.AUTH0_OAUTH_SCOPE;

function getUrls(req: NextApiRequest) {
  const { host } = req.headers;
  const protocol = process.env.VERCEL_URL ? "https" : "http";
  const redirectUri = `${protocol}://${host}/api/auth/callback`;
  const returnTo = `${protocol}://${host}`;
  return {
    redirectUri,
    returnTo,
  };
}

export default handleAuth({
  async login(req, res) {
    try {
      const { redirectUri, returnTo } = getUrls(req);

      await handleLogin(req, res, {
        authorizationParams: {
          prompt: "login",
          audience,
          scope,
          redirect_uri: redirectUri,
        },
        returnTo,
      });
    } catch (error) {
      if (error instanceof AuthError) {
        res.status(error.status || 400).end(error.message);
      }
    }
  },
  async callback(req, res) {
    try {
      const { redirectUri } = getUrls(req);

      await handleCallback(req, res, {
        redirectUri,
        authorizationParams: {
          audience,
          scope,
        },
      });
    } catch (error) {
      if (error instanceof AuthError) {
        res.status(error.status || 500).end(error.message);
      }
    }
  },
  async logout(req, res) {
    const { returnTo } = getUrls(req);
    await handleLogout(req, res, {
      returnTo,
    });
  },
});

```

### We are using this api to get accessToken

```ts
import { getAccessToken, withApiAuthRequired } from "@auth0/nextjs-auth0";
import { NextApiHandler } from "next";

const handler: NextApiHandler = async (req, res) => {
  try {
    const { accessToken } = await getAccessToken(req, res);
    res.status(200).json({ accessToken });
  } catch (err) {
    console.error(err);
    res.redirect(307, "/api/auth/login");
  }
};

export default withApiAuthRequired(handler);
```


I think we are in the similar issue as @jonahallibone . Because the user can't login has two session.

We try to alter the get-access-token to make this work but there has no luck

```ts
import { getAccessToken, getSession } from "@auth0/nextjs-auth0";
import { NextApiHandler } from "next";

const handler: NextApiHandler = async (req, res) => {
  const user = await getSession(req, res);

  if (!user || !user.accessToken) { // <---- can not get the access token
    res.redirect(307, "/api/auth/login");
    return;
  }

  res.status(200).json({ accessToken: user?.accessToken });
};

export default handler;
```

- The user need to use social auth 
- Some of the user's profile's field need to be extra large to mimic the oversize cookie issue

### Additional context

_No response_

### nextjs-auth0 version

2.6.0

### Next.js version

13.4.2

### Node.js version

14.x

Pull request:
Fix for handling chunked cookies in edge runtime
### 📋 Changes

Fixes #1230 

**Before**
<img width="691" alt="image" src="https://github.com/auth0/nextjs-auth0/assets/1299658/15469b10-4669-4a06-881f-50baa5544f95">

**After**
![image](https://github.com/auth0/nextjs-auth0/assets/1299658/3edf29be-aacc-4bcd-a1f6-8bb005648ba4)

The fix doesn't need to go into the Beta, since this uses NextResponse#cookies directly


Patch:
diff --git a/src/utils/middleware-cookies.ts b/src/utils/middleware-cookies.ts
index d593bfdf..fbe23533 100644
--- a/src/utils/middleware-cookies.ts
+++ b/src/utils/middleware-cookies.ts
@@ -4,11 +4,14 @@ import { NextRequest, NextResponse } from 'next/server';
 export default class MiddlewareCookies extends Cookies {
   protected getSetCookieHeader(res: NextResponse): string[] {
     const value = res.headers.get('set-cookie');
-    return value?.split(', ') || [];
+    return splitCookiesString(value as string);
   }
 
   protected setSetCookieHeader(res: NextResponse, cookies: string[]): void {
-    res.headers.set('set-cookie', cookies.join(', '));
+    res.headers.delete('set-cookie');
+    for (const cookie of cookies) {
+      res.headers.append('set-cookie', cookie);
+    }
   }
 
   getAll(req: NextRequest): Record<string, string> {
@@ -24,3 +27,76 @@ export default class MiddlewareCookies extends Cookies {
     }, {});
   }
 }
+
+/* eslint-disable max-len */
+/**
+ * Handle cookies with commas, eg `foo=; Expires=Thu, 01 Jan 1970 00:00:00 GMT`
+ * @source https://github.com/vercel/edge-runtime/blob/90160abc42e6139c41494c5d2e98f09e9a5fa514/packages/cookies/src/response-cookies.ts#L128
+ */
+function splitCookiesString(cookiesString: string) {
+  if (!cookiesString) return [];
+  const cookiesStrings = [];
+  let pos = 0;
+  let start;
+  let ch;
+  let lastComma;
+  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;
+          /* c8 ignore next 5 */
+        } 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;
+}


Test patch:
diff --git a/tests/utils/middleware-cookies.test.ts b/tests/utils/middleware-cookies.test.ts
index 7ee1e01c..e539b615 100644
--- a/tests/utils/middleware-cookies.test.ts
+++ b/tests/utils/middleware-cookies.test.ts
@@ -3,6 +3,7 @@
  */
 import MiddlewareCookies from '../../src/utils/middleware-cookies';
 import { NextRequest, NextResponse } from 'next/server';
+import { serialize } from 'cookie';
 
 const setup = (reqInit?: RequestInit): [NextRequest, NextResponse] => {
   return [new NextRequest(new URL('http://example.com'), reqInit), NextResponse.next()];
@@ -54,6 +55,20 @@ describe('cookie', () => {
     expect(res.headers.get('set-cookie')).toEqual(['foo=bar', 'baz=qux'].join(', '));
   });
 
+  it('should not overwrite existing set cookie with expiry', async () => {
+    const [, res] = setup();
+    res.headers.set('set-cookie', serialize('foo', '', { expires: new Date(0) }));
+    const setter = new MiddlewareCookies();
+    setter.set('baz', 'qux');
+    setter.commit(res);
+    expect(res.headers.get('set-cookie')).toEqual(
+      ['foo=; Expires=Thu, 01 Jan 1970 00:00:00 GMT', 'baz=qux'].join(', ')
+    );
+    if ('getAll' in res.headers) {
+      expect((res.headers.getAll as (header: string) => string[])('set-cookie')).toHaveLength(2);
+    }
+  });
+
   it('should override existing cookies that equal name', async () => {
     const [, res] = setup();
     res.headers.set('set-cookie', ['foo=bar', 'baz=qux'].join(', '));