Skip to content

Commit ff4a1d1

Browse files
authored
🐛 Fix infinite loop when trying to migrate legacy cookie (#1561)
1 parent cb27ae9 commit ff4a1d1

14 files changed

+261
-140
lines changed

.env.development

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ SECRET_PASSWORD=abcdef1234567890abcdef1234567890
55
# Example: https://example.com
66
NEXT_PUBLIC_BASE_URL=http://localhost:3000
77

8+
# AUTH_URL should be the same as NEXT_PUBLIC_BASE_URL
9+
AUTH_URL=http://localhost:3000
10+
811
# A connection string to your Postgres database
912
DATABASE_URL="postgres://postgres:postgres@localhost:5450/rallly"
1013

apps/web/.env.test

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
PORT=3002
22
NEXT_PUBLIC_BASE_URL=http://localhost:3002
3+
AUTH_URL=$NEXT_PUBLIC_BASE_URL
34
SECRET_PASSWORD=abcdef1234567890abcdef1234567890
45
DATABASE_URL=postgres://postgres:postgres@localhost:5450/rallly
56
SUPPORT_EMAIL=[email protected]

apps/web/src/auth/edge/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./with-auth";

apps/web/src/auth/edge/with-auth.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { NextResponse } from "next/server";
2+
import type { NextAuthRequest, Session } from "next-auth";
3+
import NextAuth from "next-auth";
4+
5+
import { nextAuthConfig } from "@/next-auth.config";
6+
7+
import {
8+
getLegacySession,
9+
migrateLegacyJWT,
10+
} from "../legacy/next-auth-cookie-migration";
11+
12+
const { auth } = NextAuth(nextAuthConfig);
13+
14+
export const withAuth = (
15+
middleware: (request: NextAuthRequest) => Promise<NextResponse>,
16+
) => {
17+
return async (request: NextAuthRequest) => {
18+
let legacySession: Session | null = null;
19+
20+
try {
21+
legacySession = await getLegacySession();
22+
} catch (e) {
23+
console.error(e);
24+
}
25+
26+
let session = legacySession;
27+
28+
if (!session) {
29+
try {
30+
session = await auth();
31+
} catch (e) {
32+
console.error(e);
33+
}
34+
}
35+
36+
try {
37+
const res = await nextAuthConfig.callbacks.authorized({
38+
request,
39+
auth: session,
40+
});
41+
42+
if (res !== true) {
43+
return res;
44+
}
45+
} catch (e) {
46+
console.error(e);
47+
}
48+
49+
request.auth = session;
50+
51+
const middlewareRes = await middleware(request);
52+
53+
if (legacySession) {
54+
try {
55+
await migrateLegacyJWT(middlewareRes);
56+
} catch (e) {
57+
console.error(e);
58+
}
59+
}
60+
61+
return middlewareRes;
62+
};
63+
};

apps/web/src/auth/legacy/helpers/jwt.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import hkdf from "@panva/hkdf";
22
import { jwtDecrypt } from "jose";
3-
4-
import type { JWT } from "./types";
3+
import type { JWT } from "next-auth/jwt";
54

65
/** Decodes a NextAuth.js issued JWT. */
76
export async function decodeLegacyJWT(token: string): Promise<JWT | null> {

apps/web/src/auth/legacy/helpers/types.ts

-46
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,70 @@
1-
import type { NextRequest } from "next/server";
2-
import { NextResponse } from "next/server";
1+
import { absoluteUrl } from "@rallly/utils/absolute-url";
2+
import { cookies } from "next/headers";
3+
import type { NextResponse } from "next/server";
4+
import type { Session } from "next-auth";
35
import { encode } from "next-auth/jwt";
46

57
import { decodeLegacyJWT } from "./helpers/jwt";
68

7-
const isSecureCookie =
8-
process.env.NEXT_PUBLIC_BASE_URL?.startsWith("https://") ?? false;
9+
const isSecureCookie = absoluteUrl().startsWith("https://");
910

1011
const prefix = isSecureCookie ? "__Secure-" : "";
1112

1213
const oldCookieName = prefix + "next-auth.session-token";
1314
const newCookieName = prefix + "authjs.session-token";
1415

15-
/**
16-
* Migrates the next-auth cookies to the new authjs cookie names
17-
* This is needed for next-auth v5 which renamed the cookie prefix from 'next-auth' to 'authjs'
18-
*/
19-
export function withAuthMigration(
20-
middleware: (req: NextRequest) => void | Response | Promise<void | Response>,
21-
) {
22-
return async (req: NextRequest) => {
23-
const oldCookie = req.cookies.get(oldCookieName);
16+
export async function getLegacySession(): Promise<Session | null> {
17+
const cookieStore = cookies();
18+
const legacySessionCookie = cookieStore.get(oldCookieName);
19+
if (legacySessionCookie) {
20+
const decodedCookie = await decodeLegacyJWT(legacySessionCookie.value);
2421

25-
// If the old cookie doesn't exist, return the middleware
26-
if (!oldCookie) {
27-
return middleware(req);
22+
if (decodedCookie?.sub) {
23+
const { sub: id, ...rest } = decodedCookie;
24+
return {
25+
user: { id, ...rest },
26+
expires: decodedCookie.exp
27+
? new Date(decodedCookie.exp * 1000).toISOString()
28+
: new Date(Date.now() + 30 * 60 * 60 * 1000).toISOString(),
29+
};
2830
}
31+
}
2932

30-
const response = NextResponse.redirect(req.url);
31-
response.cookies.delete(oldCookieName);
33+
return null;
34+
}
3235

33-
// If the new cookie exists, delete the old cookie first and rerun middleware
34-
if (req.cookies.get(newCookieName)) {
35-
return response;
36+
async function getLegacyJWT() {
37+
const cookieStore = cookies();
38+
const legacySessionCookie = cookieStore.get(oldCookieName);
39+
if (legacySessionCookie) {
40+
const decodedCookie = await decodeLegacyJWT(legacySessionCookie.value);
41+
if (decodedCookie) {
42+
return decodedCookie;
3643
}
44+
}
45+
return null;
46+
}
3747

38-
const decodedCookie = await decodeLegacyJWT(oldCookie.value);
39-
40-
// If old cookie is invalid, delete the old cookie first and rerun middleware
41-
if (!decodedCookie) {
42-
return response;
43-
}
48+
/**
49+
* Replace the old legacy cookie with the new one
50+
*/
51+
export async function migrateLegacyJWT(res: NextResponse) {
52+
const legacyJWT = await getLegacyJWT();
4453

45-
// Set the new cookie
46-
const encodedCookie = await encode({
47-
token: decodedCookie,
54+
if (legacyJWT) {
55+
const newJWT = await encode({
56+
token: legacyJWT,
4857
secret: process.env.SECRET_PASSWORD,
4958
salt: newCookieName,
5059
});
5160

52-
// Set the new cookie with the same value and attributes
53-
response.cookies.set(newCookieName, encodedCookie, {
54-
path: "/",
61+
res.cookies.set(newCookieName, newJWT, {
62+
httpOnly: true,
5563
secure: isSecureCookie,
64+
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
5665
sameSite: "lax",
57-
httpOnly: true,
66+
path: "/",
5867
});
59-
60-
// Delete the old cookie
61-
response.cookies.delete(oldCookieName);
62-
63-
return response;
64-
};
68+
res.cookies.delete(oldCookieName);
69+
}
6570
}

apps/web/src/auth/middleware.ts

-15
This file was deleted.

apps/web/src/middleware.ts

+32-35
Original file line numberDiff line numberDiff line change
@@ -3,44 +3,41 @@ import { withPostHog } from "@rallly/posthog/next/middleware";
33
import { NextResponse } from "next/server";
44

55
import { getLocaleFromHeader } from "@/app/guest";
6-
import { withAuthMigration } from "@/auth/legacy/next-auth-cookie-migration";
7-
import { withAuth } from "@/auth/middleware";
6+
import { withAuth } from "@/auth/edge";
87

98
const supportedLocales = Object.keys(languages);
109

11-
export const middleware = withAuthMigration(
12-
withAuth(async (req) => {
13-
const { nextUrl } = req;
14-
const newUrl = nextUrl.clone();
15-
16-
const isLoggedIn = req.auth?.user?.email;
17-
18-
// if the user is already logged in, don't let them access the login page
19-
if (/^\/(login)/.test(newUrl.pathname) && isLoggedIn) {
20-
newUrl.pathname = "/";
21-
return NextResponse.redirect(newUrl);
22-
}
23-
24-
// Check if locale is specified in cookie
25-
let locale = req.auth?.user?.locale;
26-
if (locale && supportedLocales.includes(locale)) {
27-
newUrl.pathname = `/${locale}${newUrl.pathname}`;
28-
} else {
29-
// Check if locale is specified in header
30-
locale = await getLocaleFromHeader(req);
31-
newUrl.pathname = `/${locale}${newUrl.pathname}`;
32-
}
33-
34-
const res = NextResponse.rewrite(newUrl);
35-
res.headers.set("x-pathname", newUrl.pathname);
36-
37-
if (req.auth?.user?.id) {
38-
await withPostHog(res, { distinctID: req.auth.user.id });
39-
}
40-
41-
return res;
42-
}),
43-
);
10+
export const middleware = withAuth(async (req) => {
11+
const { nextUrl } = req;
12+
const newUrl = nextUrl.clone();
13+
14+
const isLoggedIn = req.auth?.user?.email;
15+
16+
// if the user is already logged in, don't let them access the login page
17+
if (/^\/(login)/.test(newUrl.pathname) && isLoggedIn) {
18+
newUrl.pathname = "/";
19+
return NextResponse.redirect(newUrl);
20+
}
21+
22+
// Check if locale is specified in cookie
23+
let locale = req.auth?.user?.locale;
24+
if (locale && supportedLocales.includes(locale)) {
25+
newUrl.pathname = `/${locale}${newUrl.pathname}`;
26+
} else {
27+
// Check if locale is specified in header
28+
locale = await getLocaleFromHeader(req);
29+
newUrl.pathname = `/${locale}${newUrl.pathname}`;
30+
}
31+
32+
const res = NextResponse.rewrite(newUrl);
33+
res.headers.set("x-pathname", newUrl.pathname);
34+
35+
if (req.auth?.user?.id) {
36+
await withPostHog(res, { distinctID: req.auth.user.id });
37+
}
38+
39+
return res;
40+
});
4441

4542
export const config = {
4643
matcher: ["/((?!api|_next/static|_next/image|static|.*\\.).*)"],

apps/web/src/next-auth.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import z from "zod";
77
import { CustomPrismaAdapter } from "./auth/adapters/prisma";
88
import { isEmailBlocked } from "./auth/helpers/is-email-blocked";
99
import { mergeGuestsIntoUser } from "./auth/helpers/merge-user";
10+
import { getLegacySession } from "./auth/legacy/next-auth-cookie-migration";
1011
import { EmailProvider } from "./auth/providers/email";
1112
import { GoogleProvider } from "./auth/providers/google";
1213
import { GuestProvider } from "./auth/providers/guest";
@@ -22,7 +23,12 @@ const sessionUpdateSchema = z.object({
2223
weekStart: z.number().nullish(),
2324
});
2425

25-
export const { auth, handlers, signIn, signOut } = NextAuth({
26+
const {
27+
auth: originalAuth,
28+
handlers,
29+
signIn,
30+
signOut,
31+
} = NextAuth({
2632
...nextAuthConfig,
2733
adapter: CustomPrismaAdapter({
2834
migrateData: async (userId) => {
@@ -169,3 +175,14 @@ export const { auth, handlers, signIn, signOut } = NextAuth({
169175
},
170176
},
171177
});
178+
179+
const auth = async () => {
180+
const session = await getLegacySession();
181+
if (session) {
182+
return session;
183+
}
184+
185+
return originalAuth();
186+
};
187+
188+
export { auth, handlers, signIn, signOut };
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import { absoluteUrl } from "@rallly/utils/absolute-url";
2+
13
export const sessionConfig = {
24
password: process.env.SECRET_PASSWORD ?? "",
35
cookieName: "rallly-session",
46
cookieOptions: {
5-
secure: process.env.NEXT_PUBLIC_BASE_URL?.startsWith("https://") ?? false,
7+
secure: absoluteUrl().startsWith("https://") ?? false,
68
},
79
ttl: 60 * 60 * 24 * 30, // 30 days
810
};

0 commit comments

Comments
 (0)