diff --git a/.changeset/old-ads-push.md b/.changeset/old-ads-push.md
new file mode 100644
index 0000000000..d22559ed5b
--- /dev/null
+++ b/.changeset/old-ads-push.md
@@ -0,0 +1,5 @@
+---
+'@clerk/nextjs': patch
+---
+
+Ensure that cookies set inside Next.js Middleware are correctly passed through while using [`authMiddleware`](https://clerk.com/docs/references/nextjs/auth-middleware).
diff --git a/integration/tests/next-middleware.test.ts b/integration/tests/next-middleware.test.ts
new file mode 100644
index 0000000000..3addbb1a2e
--- /dev/null
+++ b/integration/tests/next-middleware.test.ts
@@ -0,0 +1,110 @@
+import { expect, test } from '@playwright/test';
+
+import type { Application } from '../models/application';
+import { appConfigs } from '../presets';
+import { createTestUtils } from '../testUtils';
+
+test.describe('next middleware @nextjs', () => {
+ test.describe.configure({ mode: 'parallel' });
+ let app: Application;
+
+ test.beforeAll(async () => {
+ app = await appConfigs.next.appRouter
+ .clone()
+ .addFile(
+ 'src/middleware.ts',
+ () => `import { authMiddleware } from '@clerk/nextjs/server';
+import { NextResponse } from "next/server";
+
+export default authMiddleware({
+ publicRoutes: ['/', '/hash/sign-in', '/hash/sign-up'],
+ afterAuth: async (auth, req) => {
+ const response = NextResponse.next();
+ response.cookies.set({
+ name: "first",
+ value: "123456789",
+ path: "/",
+ sameSite: "none",
+ secure: true,
+ });
+ response.cookies.set("second", "987654321", {
+ sameSite: "none",
+ secure: true,
+ });
+ response.cookies.set("third", "foobar", {
+ sameSite: "none",
+ secure: true,
+ });
+ return response;
+ },
+});
+
+export const config = {
+ matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
+};`,
+ )
+ .addFile(
+ 'src/app/provider.tsx',
+ () => `'use client'
+import { ClerkProvider } from "@clerk/nextjs"
+
+export function Provider({ children }: { children: any }) {
+ return (
+
+ {children}
+
+ )
+}`,
+ )
+ .addFile(
+ 'src/app/layout.tsx',
+ () => `import './globals.css';
+import { Inter } from 'next/font/google';
+import { Provider } from './provider';
+
+const inter = Inter({ subsets: ['latin'] });
+
+export const metadata = {
+ title: 'Create Next App',
+ description: 'Generated by create next app',
+};
+
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+ `,
+ )
+ .commit();
+ await app.setup();
+ await app.withEnv(appConfigs.envs.withEmailCodes);
+ await app.dev();
+ });
+
+ test.afterAll(async () => {
+ await app.teardown();
+ });
+
+ test('authMiddleware passes through all cookies', async ({ browser }) => {
+ // See https://playwright.dev/docs/api/class-browsercontext
+ const context = await browser.newContext();
+ const page = await context.newPage();
+ const u = createTestUtils({ app, page });
+
+ await page.goto(app.serverUrl);
+ await u.po.signIn.waitForMounted();
+
+ const cookies = await context.cookies();
+
+ expect(cookies.find(c => c.name == 'first').value).toBe('123456789');
+ expect(cookies.find(c => c.name == 'second').value).toBe('987654321');
+ expect(cookies.find(c => c.name == 'third').value).toBe('foobar');
+
+ await context.close();
+ });
+});
diff --git a/packages/nextjs/src/utils/response.test.ts b/packages/nextjs/src/utils/response.test.ts
index 13f8e5d15d..525b0e01c6 100644
--- a/packages/nextjs/src/utils/response.test.ts
+++ b/packages/nextjs/src/utils/response.test.ts
@@ -29,10 +29,32 @@ describe('mergeResponses', function () {
const response1 = new NextResponse();
const response2 = new NextResponse();
response1.cookies.set('foo', '1');
+ response1.cookies.set('second', '2');
response1.cookies.set('bar', '1');
response2.cookies.set('bar', '2');
const finalResponse = mergeResponses(response1, response2);
expect(finalResponse!.cookies.get('foo')).toEqual(response1.cookies.get('foo'));
+ expect(finalResponse!.cookies.get('second')).toEqual(response1.cookies.get('second'));
+ expect(finalResponse!.cookies.get('bar')).toEqual(response2.cookies.get('bar'));
+ });
+
+ it('should merge the cookies with non-response values', function () {
+ const response2 = NextResponse.next();
+ response2.cookies.set('foo', '1');
+ response2.cookies.set({
+ name: 'second',
+ value: '2',
+ path: '/',
+ sameSite: 'none',
+ secure: true,
+ });
+ response2.cookies.set('bar', '1', {
+ sameSite: 'none',
+ secure: true,
+ });
+ const finalResponse = mergeResponses(null, response2);
+ expect(finalResponse!.cookies.get('foo')).toEqual(response2.cookies.get('foo'));
+ expect(finalResponse!.cookies.get('second')).toEqual(response2.cookies.get('second'));
expect(finalResponse!.cookies.get('bar')).toEqual(response2.cookies.get('bar'));
});
diff --git a/packages/nextjs/src/utils/response.ts b/packages/nextjs/src/utils/response.ts
index 760ea6939d..14b5943977 100644
--- a/packages/nextjs/src/utils/response.ts
+++ b/packages/nextjs/src/utils/response.ts
@@ -8,7 +8,15 @@ import { constants as nextConstants } from '../constants';
* but the cookies and headers of all responses are merged.
*/
export const mergeResponses = (...responses: (NextResponse | Response | null | undefined | void)[]) => {
- const normalisedResponses = responses.filter(Boolean).map(res => new NextResponse(res!.body, res!));
+ const normalisedResponses = responses.filter(Boolean).map(res => {
+ // If the response is a NextResponse, we can just return it
+ if (res instanceof NextResponse) {
+ return res;
+ }
+
+ return new NextResponse(res!.body, res!);
+ });
+
if (normalisedResponses.length === 0) {
return;
}
@@ -22,7 +30,9 @@ export const mergeResponses = (...responses: (NextResponse | Response | null | u
});
response.cookies.getAll().forEach(cookie => {
- finalResponse.cookies.set(cookie.name, cookie.value);
+ const { name, value, ...options } = cookie;
+
+ finalResponse.cookies.set(name, value, options);
});
}