Reputation: 1
I'm facing a weird issue with my Next.js App Router (v15) middleware for JWT-based route protection. I'm using jose for token verification because it's Edge-compatible.
The issue is that everything works fine locally, but fails in production. Middleware keeps redirecting users unnecessarily as if the JWT verification fails. I suspect it's either a problem with TextEncoder, environment variables, or Edge runtime behavior.
My setup:
Here’s my middleware file:
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const isProtectedRoute = protectedPaths.includes(pathname);
const isAuthRoute = authPaths.includes(pathname);
// Get tokens
const accessToken = request.cookies.get(ACCESS_TOKEN_NAME)?.value;
const refreshToken = request.cookies.get(REFRESH_TOKEN_NAME)?.value;
// If the route is protected, check if the user is authenticated
if (isProtectedRoute) {
const redirectUrl = new URL("/sign-in", request.url);
redirectUrl.searchParams.set("redirect", pathname + request.nextUrl.search);
if (!accessToken) {
return NextResponse.redirect(redirectUrl);
}
try {
// Verify access token
const { payload: accessPayload } = await jose.jwtVerify(
accessToken,
new TextEncoder().encode(process.env.JWT_ACCESS_SECRET || "")
);
// If access token is valid, continue
if (accessPayload && typeof accessPayload.userId === "number") {
return NextResponse.next();
}
} catch (error) {
// Access token is invalid, try refresh token
console.error("Access token verification error:", error);
if (!refreshToken) {
return NextResponse.redirect(redirectUrl);
}
try {
// Verify refresh token
const { payload: refreshPayload } = await jose.jwtVerify(
refreshToken,
new TextEncoder().encode(process.env.JWT_REFRESH_SECRET || "")
);
if (!refreshPayload || typeof refreshPayload.userId !== "number") {
return NextResponse.redirect(redirectUrl);
}
// Generate new access token
const userId = refreshPayload.userId as number;
const newAccessToken = await generateToken(userId, "ACCESS");
const response = NextResponse.next();
// Set the new access token as a cookie in the response
response.cookies.set(ACCESS_TOKEN_NAME, newAccessToken, {
...COOKIE_CONFIG,
maxAge: ACCESS_TOKEN_MAX_AGE,
});
return response;
} catch (error) {
// Refresh token is invalid, redirect to login
console.error("Refresh token verification error:", error);
return NextResponse.redirect(redirectUrl);
}
}
}
if (isAuthRoute && accessToken) {
try {
// Verify the access token is valid before redirecting
await jose.jwtVerify(
accessToken,
new TextEncoder().encode(process.env.JWT_ACCESS_SECRET || "")
);
// redirect where it came from
const redirectUrl = request.nextUrl.searchParams.get("redirect");
if (redirectUrl) {
return NextResponse.redirect(new URL(redirectUrl, request.url));
}
return NextResponse.redirect(new URL("/", request.url));
} catch (error) {
// Token is invalid, let them stay on the auth route
console.error("Auth route token verification error:", error);
return NextResponse.next();
}
}
return NextResponse.next();
}
What I've checked so far:
Possible causes I'm considering:
Upvotes: 0
Views: 33