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
- Handle Supabase Auth Errors in Next.js Middleware
- Fix Supabase Auth Session Not Persisting After Refresh
- Supabase Authentication & Authorization Patterns
- Building SaaS with Next.js and Supabase
- Deploying Next.js + Supabase to Production
- Next.js App Router 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.
Originally published at https://www.iloveblogs.blog
Top comments (0)