Supabase Auth Redirect Not Working Next.js App Router
Auth redirects failing in Next.js App Router? Learn the exact cause and fix it with this complete guide including OAuth and magic link redirects.
Supabase Auth Redirect Not Working Next.js App Router#
Supabase Auth Redirect Not Working Next.js App Router Solution#
Authentication redirects failing in Next.js App Router is a common issue that leaves users stuck on the login page even after successful authentication. You implement Supabase auth, users sign in successfully, but the redirect to the dashboard never happens—or worse, they get redirected to the wrong page. As covered in our Supabase Authentication & Authorization Patterns, proper redirect handling is essential for smooth user experience.
In this guide, I'll show you exactly why redirects fail in Next.js 14 App Router and how to fix them for email/password, OAuth, and magic link authentication.
Why Auth Redirects Fail in Next.js App Router#
The Next.js App Router introduced significant changes to how routing works, and Supabase auth redirects require special handling. Here's what's happening:
The Core Problem#
- Server vs Client Rendering - App Router uses Server Components by default, but auth state changes happen on the client
- Router Cache - Next.js aggressively caches routes, so even after authentication, cached pages may not reflect the new auth state
- Callback URL Mismatch - OAuth and magic link callbacks need explicit handling in App Router
Why Traditional Solutions Don't Work#
// ❌ This doesn't work in App Router
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (!error) {
window.location.href = '/dashboard' // Causes full page reload
}
The issue: Using window.location.href causes a full page reload and loses the React state. Using router.push() alone doesn't refresh Server Components.
Step-by-Step Solution#
Step 1: Create Auth Callback Route#
First, create a callback route handler for OAuth and magic link redirects:
// app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
const requestUrl = new URL(request.url)
const code = requestUrl.searchParams.get('code')
const next = requestUrl.searchParams.get('next') ?? '/dashboard'
if (code) {
const supabase = createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
// Redirect to the intended destination
return NextResponse.redirect(new URL(next, request.url))
}
}
// If there's an error, redirect to login with error message
return NextResponse.redirect(
new URL('/login?error=Could not authenticate user', request.url)
)
}
Step 2: Fix Email/Password Redirects#
For email/password authentication, use both router.push() and router.refresh():
'use client'
import { createClient } from '@/lib/supabase/client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
export default function LoginPage() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const router = useRouter()
const supabase = createClient()
async function handleLogin(e: React.FormEvent) {
e.preventDefault()
setLoading(true)
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) {
alert(error.message)
setLoading(false)
return
}
// ✅ CORRECT: Use both push and refresh
router.push('/dashboard')
router.refresh() // This updates Server Components
}
return (
<form onSubmit={handleLogin}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Signing in...' : 'Sign In'}
</button>
</form>
)
}
Step 3: Fix OAuth Redirects#
For OAuth providers (Google, GitHub, etc.), configure the redirect URL properly:
'use client'
import { createClient } from '@/lib/supabase/client'
export function GoogleSignIn() {
const supabase = createClient()
async function handleGoogleSignIn() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
// ✅ CORRECT: Point to callback route
redirectTo: `${window.location.origin}/auth/callback?next=/dashboard`,
queryParams: {
access_type: 'offline',
prompt: 'consent',
},
},
})
if (error) {
alert(error.message)
}
}
return (
<button onClick={handleGoogleSignIn}>
Sign in with Google
</button>
)
}
Step 4: Fix Magic Link Redirects#
For magic link authentication, configure the email redirect:
'use client'
import { createClient } from '@/lib/supabase/client'
import { useState } from 'react'
export function MagicLinkForm() {
const [email, setEmail] = useState('')
const [loading, setLoading] = useState(false)
const [sent, setSent] = useState(false)
const supabase = createClient()
async function handleMagicLink(e: React.FormEvent) {
e.preventDefault()
setLoading(true)
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
// ✅ CORRECT: Point to callback route with next parameter
emailRedirectTo: `${window.location.origin}/auth/callback?next=/dashboard`,
},
})
if (error) {
alert(error.message)
} else {
setSent(true)
}
setLoading(false)
}
if (sent) {
return <p>Check your email for the magic link!</p>
}
return (
<form onSubmit={handleMagicLink}>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Sending...' : 'Send Magic Link'}
</button>
</form>
)
}
Step 5: Add Middleware Protection#
Ensure your middleware properly handles redirects:
// middleware.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
request.cookies.set({ name, value, ...options })
response = NextResponse.next({ request: { headers: request.headers } })
response.cookies.set({ name, value, ...options })
},
remove(name: string, options: CookieOptions) {
request.cookies.set({ name, value: '', ...options })
response = NextResponse.next({ request: { headers: request.headers } })
response.cookies.set({ name, value: '', ...options })
},
},
}
)
const { data: { user } } = await supabase.auth.getUser()
// Redirect to login if not authenticated
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Redirect to dashboard if already authenticated
if (user && (request.nextUrl.pathname === '/login' || request.nextUrl.pathname === '/signup')) {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
return response
}
export const config = {
matcher: ['/dashboard/:path*', '/login', '/signup'],
}
Working Code Examples#
Complete Login Flow with Redirect#
// app/login/page.tsx
'use client'
import { createClient } from '@/lib/supabase/client'
import { useRouter, useSearchParams } from 'next/navigation'
import { useState, useEffect } from 'react'
export default function LoginPage() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const router = useRouter()
const searchParams = useSearchParams()
const supabase = createClient()
// Get redirect destination from URL or default to dashboard
const next = searchParams.get('next') ?? '/dashboard'
// Show error if present in URL
useEffect(() => {
const error = searchParams.get('error')
if (error) {
alert(error)
}
}, [searchParams])
async function handleLogin(e: React.FormEvent) {
e.preventDefault()
setLoading(true)
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) {
alert(error.message)
setLoading(false)
return
}
// Redirect to intended destination
router.push(next)
router.refresh()
}
return (
<div>
<h1>Sign In</h1>
<form onSubmit={handleLogin}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Signing in...' : 'Sign In'}
</button>
</form>
</div>
)
}
OAuth with Multiple Providers#
// components/OAuthButtons.tsx
'use client'
import { createClient } from '@/lib/supabase/client'
export function OAuthButtons() {
const supabase = createClient()
async function signInWithProvider(provider: 'google' | 'github') {
const { data, error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${window.location.origin}/auth/callback?next=/dashboard`,
},
})
if (error) {
alert(error.message)
}
}
return (
<div>
<button onClick={() => signInWithProvider('google')}>
Sign in with Google
</button>
<button onClick={() => signInWithProvider('github')}>
Sign in with GitHub
</button>
</div>
)
}
Common Mistakes#
-
Mistake #1: Not using router.refresh() - After authentication, you must call
router.refresh()to update Server Components. Without it, protected pages won't recognize the new auth state. -
Mistake #2: Wrong callback URL - OAuth and magic links must redirect to
/auth/callback, not directly to/dashboard. The callback route exchanges the code for a session. -
Mistake #3: Missing next parameter - Always include a
nextparameter in your callback URL to specify where users should go after authentication. -
Mistake #4: Using window.location.href - This causes a full page reload and loses React state. Use Next.js's
router.push()androuter.refresh()instead. -
Mistake #5: Not handling errors in callback - Always check for errors in the callback route and redirect to login with an error message if authentication fails.
FAQ#
Why does my OAuth redirect fail?#
OAuth redirects fail when the callback URL is incorrect or not configured in Supabase. Ensure your callback URL is https://iloveblog.blog/auth/callback and add it to the "Redirect URLs" list in your Supabase project settings.
How do I redirect to a specific page after login?#
Use the next query parameter in your redirect URL. For example: /auth/callback?next=/profile. The callback route will read this parameter and redirect accordingly.
Why do I need both router.push() and router.refresh()?#
router.push() navigates to the new route, but router.refresh() is needed to update Server Components with the new auth state. Without refresh, Server Components will still show the old (unauthenticated) state.
Can I use redirectTo with email/password auth?#
No, redirectTo only works with OAuth and magic link authentication. For email/password, use router.push() and router.refresh() after successful sign-in.
How do I test redirects locally?#
Use http://localhost:3000/auth/callback as your redirect URL during development. Make sure to add this to your Supabase project's allowed redirect URLs.
Related Articles#
- Fix Supabase Auth Session Not Persisting After Refresh Next.js 14
- Handle Supabase Auth Errors in Next.js Middleware
- Build Protected Routes in Next.js 15 with Supabase
- Implement Social Auth with Supabase Next.js Complete Guide
Conclusion#
Fixing auth redirects in Next.js App Router requires understanding the difference between Server and Client Components and properly handling the authentication callback. The key steps are:
- Create a callback route handler for OAuth and magic links
- Use
router.push()+router.refresh()for email/password auth - Always include the
nextparameter to specify redirect destination - Configure middleware to handle protected routes
Test your implementation by signing in with each auth method and verifying users are redirected to the correct page. If redirects still fail, check your Supabase project settings to ensure callback URLs are properly configured.
With these fixes in place, your authentication flow will work smoothly across all auth methods in Next.js 14 App Router.
Frequently Asked Questions
Continue Reading
Fix Supabase Auth Session Not Persisting After Refresh
Supabase auth sessions mysteriously disappearing after page refresh? Learn the exact cause and fix it in 5 minutes with this tested solution.
Handle Supabase Auth Errors in Next.js Middleware
Auth errors crashing your Next.js middleware? Learn how to handle Supabase auth errors gracefully with proper error handling patterns.
Supabase Email Confirmation Not Sending Troubleshooting
Email confirmations not sending from Supabase? Learn the exact causes and fixes for SMTP, template, and configuration issues in 10 minutes.
Browse by Topic
Find stories that matter to you.