I am working on a project using Next.js (App Router) and AWS Amplify Auth. I have implemented middleware to restrict access and redirect users based on a custom Cognito attribute (custom:onboardingStatus: "COMPLETED","PENDING","VERIFIED","REJECTED" ). So the idea is that when a user signs up, that check is added as a custom attribute in cognito. i set this up so that new users are supposed to add more information to successfully activate their account instead of having everything clamped on the sign up screen. So the onboarding screen is a different page
Everything works perfectly in my local environment. However, once deployed to AWS Amplify Hosting, I run into a race condition or cookie propagation issue.
The Issue
A user logs in for the first time on a new browser.
The login function succeeds, and Amplify writes the cookies.
The Problem: The middleware does not trigger the redirect. The user stays stuck on the login screen. when i check the browser logs it just says
UserAlreadyAuthenticatedExceptionwithout the redirect.
The Workaround: If I refresh the page then enter some random dummy data for the email and password, the middleware finally redirects the user to the correct page based on their
custom:onboardingStatuscheck from Aws-cognito.
My Setup
Framework: Next.js 16 (App Router)
Auth: AWS Amplify (Cognito)
Hosting: AWS Amplify Hosting
Middleware Code(.ts)
import { type NextRequest, NextResponse } from "next/server"; import { fetchAuthSession } from "aws-amplify/auth/server"; import { runWithAmplifyServerContext } from "./app/utils/amplifyServerUtils"; const AUTH_ROUTES = ["/auth/login", "/auth/signup"]; const PROTECTED_ROUTES = ["/", "/onboarding", "/dashboard", "/identity_check"]; export default async function middleware(request: NextRequest) { const { pathname, search, origin } = request.nextUrl; const response = NextResponse.next(); const isProtectedRoute = PROTECTED_ROUTES.some( (route) => pathname === route || pathname.startsWith(route + "/") ); const isAuthRoute = AUTH_ROUTES.some( (route) => pathname === route || pathname.startsWith(route + "/") ); const session = await runWithAmplifyServerContext({ nextServerContext: { request, response }, operation: async (contextSpec) => { try { return await fetchAuthSession(contextSpec); } catch (err) { console.error("Middleware Auth Error:", err); return null; } }, }); const isAuthenticated = !!session?.tokens?.idToken; const customRedirect = (url: string) => { const redirectResponse = NextResponse.redirect(new URL(url, origin)); // Attempting to preserve cookies response.cookies.getAll().forEach((cookie) => { redirectResponse.cookies.set(cookie.name, cookie.value); }); return redirectResponse; }; if (isProtectedRoute && !isAuthenticated) { const loginUrl = new URL("/auth/login", origin); loginUrl.searchParams.set("callbackUrl", pathname + search); return customRedirect(loginUrl.toString()); } if (isAuthenticated) { let onboardingStatus: string | undefined; if (session?.tokens?.idToken) { try { const payload = session.tokens.idToken.payload; onboardingStatus = payload["custom:onboardingStatus"]?.toString(); } catch (err) { console.error("Error reading token payload:", err); } } if (isAuthRoute) { return customRedirect("/"); } if (!onboardingStatus || onboardingStatus === "PENDING") { if (pathname !== "/onboarding") { return customRedirect("/onboarding"); } } else if (onboardingStatus === "SUBMITTED" || onboardingStatus === "REJECTED") { if (pathname !== "/identity_check") { return customRedirect("/identity_check"); } } else if (onboardingStatus === "VERIFIED") { if (["/onboarding", "/identity_check"].includes(pathname)) { return customRedirect("/"); } } else { if (pathname !== "/identity_check") { return customRedirect("/identity_check"); } } } return response; } export const config = { matcher: [ "/((?!api|_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", ], }; What I have tried
Manually copying cookies from the initial NextResponse to the RedirectResponse (as seen in the code).
Ensuring runWithAmplifyServerContext is used
"use client"; import { Amplify } from "aws-amplify"; import { parseAmplifyConfig } from "aws-amplify/utils"; import outputs from "@/amplify_outputs.json"; Amplify.configure(parseAmplifyConfig(outputs), { ssr: true }); export default function ConfigureAmplify() { return null; } My Questions
Is there a specific pattern for handling post-login redirects in Next.js Middleware with Amplify so that cookies are guaranteed to be present before the redirect logic runs?
Is there a best way to do this?