Web

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.

1 answer• 1 view

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

ts
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)

ts
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)

ts
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

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:

ts
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 /:

tsx
"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:

ts
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:

ts
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:

ts
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.


Query params fail (e.g., disabled JS)? Store in a cookie before redirect:

In middleware:

ts
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.signIn matches middleware awareness, per Next.js config docs.
  • redirect: false breaks it? Stack Overflow confirms—use server redirects or drop it.
  • Logs show / redirects? Check NEXTAUTH_URL env 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

  1. Callbacks | NextAuth.js
  2. Client API | NextAuth.js
  3. Securing pages and API routes | NextAuth.js
  4. Protecting | Auth.js
  5. Protect any route through middleware ¡ GitHub Discussion
  6. NextAuth - Redirect to original page ¡ Stack Overflow
  7. 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.

Authors
Verified by moderation
Moderation
NextAuth: Redirect Back to Original URL After Login