Supabase Auth Redirect Not Working Next.js App Router
technology

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.

2026-02-16
9 min read
Supabase Auth Redirect Not Working Next.js App Router

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#

  1. Server vs Client Rendering - App Router uses Server Components by default, but auth state changes happen on the client
  2. Router Cache - Next.js aggressively caches routes, so even after authentication, cached pages may not reflect the new auth state
  3. 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>
  )
}

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 next parameter 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() and router.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.

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:

  1. Create a callback route handler for OAuth and magic links
  2. Use router.push() + router.refresh() for email/password auth
  3. Always include the next parameter to specify redirect destination
  4. 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

|

Have more questions? Contact us