NextAuth: Redirect Back to Original URL After Login
Preserve original URLs and redirect users back after login in Next.js with NextAuth. Middleware plus callbackUrl, redirect callback, or cookie fallback.
How can I preserve the original requested URL and redirect back to it after login with Next.js and NextAuth?
Context
When a user opens a dedicated link like https://mywebsite.com/user/4545454554 I want them to be redirected to that page after authentication. Right now they are always redirected to the home page (/) which should only happen if the user navigates to https://mywebsite.com directly.
Relevant code (login, middleware, and NextAuth config):
Login code
const onSubmit = async (data: { clientId: string; secret: string }) => {
setError("");
const result = await signIn("credentials", {
clientId: data.clientId,
secret: data.secret,
redirect: false,
});
if (result?.error) {
setError("Invalid credentials. Please try again.");
return;
}
if (result?.ok) {
router.push("/reconciliation");
}
};
Middleware (proxy)
export { default } from "next-auth/middleware"
import { NextResponse, NextRequest } from "next/server";
import { getToken } from "next-auth/jwt";
import { hasRole } from "@/app/utils/roleUtils";
export async function proxy(req: NextRequest) {
console.log("đ Middleware Triggered đ");
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
console.log("đ URL :", req.nextUrl.pathname);
console.log("đ Token founded");
if (!token) {
console.log("No token found, redirecting to login page");
return NextResponse.redirect(new URL("/", req.url));
}
// Get the current path
const path = req.nextUrl.pathname;
// Get user roles from token (array of roles)
const userRoles = token.roles as string[] || [];
console.log("User roles:", userRoles);
// Check if user has the required SYS_FINANCE role
if (!hasRole(userRoles, "SYS_FINANCE")) {
console.log("Access denied: User does not have SYS_FINANCE role");
return NextResponse.redirect(new URL("/unauthorized", req.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/reconciliation/:path*, /detail/:path*", "/account/:path*", "/unauthorized/:path*"],
};
NextAuth configuration (authOptions)
import CredentialsProvider from "next-auth/providers/credentials";
import {NextAuthOptions} from "next-auth";
import {JWT} from "next-auth/jwt";
export const authOptions : NextAuthOptions = {
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
clientId: {label: "clientId", type: "text"},
secret: {label: "secret", type: "password"},
},
async authorize(credentials, _req): Promise<any> {
console.log('api url : '+process.env.NEXT_PUBLIC_API_BASE_URL);
console.log('callback url : '+process.env.NEXTAUTH_CALLBACK_URL);
console.log('front url : '+process.env.NEXTAUTH_URL);
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_AUTH_API_BASE_URL}/api/v1/authentication/token/admin`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
clientId: credentials?.clientId,
secret: credentials?.secret,
}),
});
if (!res.ok) {
const errorData = await res.json();
console.log(`â Login failed: ${res.status} - ${errorData.message}`);
throw new Error(`Login failed: ${res.status} - ${errorData.message}`);
}
const data = await res.json();
return {
clientId: data.clientId,
token: data.token,
};
} catch (error) {
console.error("â Error in authentication:", error);
throw new Error(error.message || "Unknown authentication error");
}
},
}),
],
session: {strategy: "jwt", maxAge: 15 * 60},
callbacks: {
async jwt({token, user}: { token: JWT, user: any }) {
if (user) {
(token as any).id = user.id;
(token as any).token = user.token;
(token as any).clientId = user.clientId;
const decodedAccessToken = JSON.parse(Buffer.from(user.token.split(".")[1], "base64").toString())
console.debug("decodedAccessToken :" + Buffer.from(user.token.split(".")[1], "base64"));
// Store all roles as an array
if (decodedAccessToken.roles && Array.isArray(decodedAccessToken.roles)) {
(token as any).roles = decodedAccessToken.roles.map((role: any) => role.authority);
} else if (decodedAccessToken.roles) {
(token as any).roles = [decodedAccessToken.roles.authority];
} else {
(token as any).roles = [];
}
if (decodedAccessToken) {
(token as any).accessTokenExpires = decodedAccessToken["exp"] * 1000
}
if ((token as any).accessTokenExpires && (Date.now() > Number((token as any).accessTokenExpires))) {
console.log("token expired");
return null
}
}
return token;
},
async session({session, token}: { session:any,token: JWT }) {
console.log("session :" + (token as any).clientId);
if (!token) {
return null;
}
session.user.token = (token as any).token;
session.user.clientId = (token as any).clientId;
session.user.roles = (token as any).roles; // Array of all roles
return session;
},
},
pages: {
signIn: "/",
error: "?error=true",
},
secret: process.env.NEXTAUTH_SECRET,
};
Question
What changes should I make to ensure the original requested path (for example, /user/4545454554) is preserved and the user is redirected back to it after successful login? Specifically, should I:
- Change the middleware to redirect to the sign-in page with a callback or returnTo parameter?
- Pass a callbackUrl to signIn and use the returned URL instead of router.push(â/reconciliationâ)?
- Store the original path in a cookie or query parameter before redirecting to sign-in?
Please suggest the recommended approach and any minimal code changes required in the middleware, the signIn call, or NextAuth configuration to implement this behavior.
To preserve the original requested URL like /user/4545454554 and redirect back after login with NextAuth in Next.js, update your middleware to capture the path and redirect to the sign-in endpoint with a callbackUrl query parameter. In your login handler, read this callbackUrl from the URL and pass it to signIn, removing redirect: false or handling the redirect dynamically instead of hardcoding /reconciliation. Finally, add a redirect callback in your NextAuth config to securely validate these URLs, preventing open redirect vulnerabilities.
Contents
- How NextAuth Redirects Work
- Step 1: Update Middleware to Capture Original URL
- Step 2: Modify Login Handler with callbackUrl
- Step 3: Add Redirect Callback in NextAuth Config
- Step 4: Expand Matcher for Protected Routes
- Alternative: Cookie-Based Fallback
- Common Pitfalls and Troubleshooting
- Sources
- Conclusion
How NextAuth Redirects Work
Ever hit a protected page only to land on the homepage after login? Thatâs the default NextAuth behaviorâsecure, but not user-friendly for deep links like /user/4545454554. NextAuth uses a callbackUrl mechanism to fix this. When you redirect unauthenticated users to sign-in, append the original path as ?callbackUrl=/original/path. After successful auth, NextAuth reads it and bounces you back, but only if your config allows it.
The official NextAuth client API docs confirm signIn accepts callbackUrl as a second arg. Middleware intercepts requests first, per the securing routes tutorial. Your current setup skips this: middleware dumps to /, login forces /reconciliation. No wonder deep links vanish.
Why callbackUrl over manual storage? Itâs built-in, handles relative paths, and pairs with a redirect callback for security. Letâs tweak your code minimally.
Step 1: Update Middleware to Capture Original URL
Your proxy middleware already grabs the tokenâperfect starting point. But when no token? Donât redirect to /. Instead, snag the full pathname and pipe it to NextAuthâs sign-in flow.
Replace the if (!token) block:
if (!token) {
console.log("No token found, redirecting to login with callbackUrl");
const callbackUrl = req.nextUrl.pathname + req.nextUrl.search; // Preserve full path + query
const signInUrl = new URL(`/api/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`, req.url);
return NextResponse.redirect(signInUrl);
}
This hits /api/auth/signin?callbackUrl=%2Fuser%2F4545454554, which NextAuth routes to your pages.signIn: "/" with the param intact. Users see the login form at / with the target URL preserved. Genius, right?
A GitHub discussion nails this pattern exactlyâcommunity-tested on real apps.
Step 2: Modify Login Handler with callbackUrl
Your onSubmit ignores any incoming callbackUrl and shoves everyone to /reconciliation. Ditch redirect: false (it skips NextAuthâs redirect magic) and let signIn handle the bounce.
First, read callbackUrl from the page URL. Assuming your login is a client component at /:
"use client";
import { useSearchParams } from "next/navigation"; // Add this
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl") || "/reconciliation"; // Fallback for root visits
const onSubmit = async (data: { clientId: string; secret: string }) => {
setError("");
const result = await signIn("credentials", {
clientId: data.clientId,
secret: data.secret,
callbackUrl, // Pass it here!
// Drop redirect: falseâlet NextAuth redirect naturally
});
if (result?.error) {
setError("Invalid credentials. Please try again.");
return;
}
// No need for manual router.pushâNextAuth handles it
};
Boom. After auth, youâre at /user/4545454554. If someone hits / directly? Falls back to /reconciliation. The NextAuth callbacks docs explain why this works seamlessly with credentials providers.
If you must keep redirect: false (say, for custom UI feedback), do:
if (result?.ok) {
router.push(callbackUrl);
return;
}
Step 3: Add Redirect Callback in NextAuth Config
NextAuth blocks arbitrary callbackUrls by defaultâsame-origin only. Your deep links? Fine, since theyâre relative. But add this callback to whitelist them explicitly.
In authOptions.callbacks:
callbacks: {
// ... your existing jwt/session
async redirect({ url, baseUrl }) {
// Allows relative callback URLs
if (url.startsWith("/")) return `${baseUrl}${url}`;
// Allows callback URLs on same origin
else if (new URL(url).origin === baseUrl) return url;
return baseUrl; // Fallback to home
},
// ...
},
This mirrors the official example. Secure against phishing, yet flexible for /user/*.
Step 4: Expand Matcher for Protected Routes
Your matcher skips /user/:path*âthatâs why /user/4545454554 might slip through unprotected. Add it:
export const config = {
matcher: [
"/reconciliation/:path*",
"/detail/:path*",
"/account/:path*",
"/unauthorized/:path*",
"/user/:path*", // Add this!
],
};
Now middleware catches it, preserves the ID, and redirects smartly. The Auth.js protecting docs back this dynamic matcher approach.
Alternative: Cookie-Based Fallback
Query params fail (e.g., disabled JS)? Store in a cookie before redirect:
In middleware:
if (!token) {
const callbackUrl = req.nextUrl.pathname + req.nextUrl.search;
const response = NextResponse.redirect(new URL("/api/auth/signin", req.url));
response.cookies.set("returnTo", callbackUrl, { path: "/" });
return response;
}
In login: const returnTo = cookies().get("returnTo")?.value || "/reconciliation"; then callbackUrl: returnTo.
Snyk examples demo this legacy trick. But callbackUrl wins for simplicity.
Common Pitfalls and Troubleshooting
- Double callbackUrl? Seen in this GitHub threadâvalidate in redirect callback.
- Stuck at sign-in? Ensure
pages.signInmatches middleware awareness, per Next.js config docs. - redirect: false breaks it? Stack Overflow confirmsâuse server redirects or drop it.
- Logs show
/redirects? CheckNEXTAUTH_URLenv matches your domain. - Test: Hit
/user/fake, watch console, login, verify bounce-back.
These fixes take ~20 lines total. Scales to any Next.js auth flow.
Sources
- Callbacks | NextAuth.js
- Client API | NextAuth.js
- Securing pages and API routes | NextAuth.js
- Protecting | Auth.js
- Protect any route through middleware ¡ GitHub Discussion
- NextAuth - Redirect to original page ¡ Stack Overflow
- Next.js | NextAuth.js
Conclusion
These changesâmiddleware capture, signIn with callbackUrl, and redirect callbackânail NextAuth redirects back to the original URL, ditching hardcoded paths for good. Your /user/4545454554 users stay happy, no more homepage frustration. Test edge cases like root visits (fallback works), and youâre set for production. Quick win for any Next.js auth app.