← 記事一覧に戻る

Supabase Auth + Next.js App Router でセッションが取れない・ログインが動かない時の5つの原因と対策【@supabase/ssr / middleware】

はじめに

Supabase Auth と Next.js App Router を組み合わせると、ローカルではログインできるのに本番で動かない・セッションが取れない・ログイン後もリダイレクトされないといった問題が頻発します。

よく見かけるエラーや症状の例:

AuthSessionMissingError: Auth session missing!
const { data: { session } } = await supabase.auth.getSession()
// session が null になる(ログイン後なのに)
Error: supabase-auth-helpers-nextjs is deprecated

原因の多くは 古いパッケージの使用・middleware の設定漏れ・Cookie の扱い方 に起因します。この記事では5つの原因に絞って解説します。


原因1: 旧パッケージ @supabase/auth-helpers-nextjs を使い続けている(最多)

2024年以降、Supabase は Next.js App Router 向けのパッケージを刷新しました。

旧(非推奨) 新(推奨)
@supabase/auth-helpers-nextjs @supabase/ssr

旧パッケージはPages Router時代の設計で、App Router のServer ComponentやServer Actionsとの相性が悪く、セッションが正しく引き継がれません。

解決策: @supabase/ssr に移行する

npm uninstall @supabase/auth-helpers-nextjs
npm install @supabase/ssr @supabase/supabase-js

クライアント作成の書き方が変わります。

// ❌ 旧: @supabase/auth-helpers-nextjs
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
const supabase = createClientComponentClient()

// ✅ 新: @supabase/ssr(クライアントコンポーネント用)
import { createBrowserClient } from '@supabase/ssr'
const supabase = createBrowserClient(
  ({}).NEXT_PUBLIC_SUPABASE_URL!,
  ({}).NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
// ✅ 新: @supabase/ssr(Server Component / Route Handler 用)
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
    ({}).NEXT_PUBLIC_SUPABASE_URL!,
    ({}).NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // Server Component から呼ばれた場合は Cookie の書き込みができないため無視
          }
        },
      },
    }
  )
}

原因2: middleware.ts が設定されていない・パスが間違っている

App Router でセッションを維持するには middleware でCookieを自動更新 する必要があります。これがないと、ログインしても次のリクエストでセッションが失われます。

// ❌ middleware なしだとセッションが引き継がれない
// (ログインしても /dashboard にアクセスすると session = null)

解決策: middleware.ts を追加する

プロジェクトルート(src/ を使っている場合は src/middleware.ts)に以下を追加します。

// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  let supabaseResponse = NextResponse.next({
    request,
  })

  const supabase = createServerClient(
    ({}).NEXT_PUBLIC_SUPABASE_URL!,
    ({}).NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value }) =>
            request.cookies.set(name, value)
          )
          supabaseResponse = NextResponse.next({
            request,
          })
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  // セッションをリフレッシュ(重要: getUser() を呼ぶこと)
  const {
    data: { user },
  } = await supabase.auth.getUser()

  // 未認証ユーザーを /login にリダイレクト
  if (
    !user &&
    !request.nextUrl.pathname.startsWith('/login') &&
    !request.nextUrl.pathname.startsWith('/auth')
  ) {
    const url = request.nextUrl.clone()
    url.pathname = '/login'
    return NextResponse.redirect(url)
  }

  return supabaseResponse
}

export const config = {
  matcher: [
    // 静的ファイルと _next を除外
    '/((?!_next/static|_next/image|favicon.ico|.*\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

getUser() を呼ぶことが重要です。 getSession() ではなく getUser() を使うことで、Supabaseのサーバー側でセッショントークンが検証・更新されます。


原因3: Server Component で getSession() を使っている

セキュリティ上の理由から、App RouterのServer Componentでは getSession() ではなく getUser() を使う必要があります。

// ❌ Server Component での getSession() は信頼できない
const { data: { session } } = await supabase.auth.getSession()
// session が null になることがある(JWTが古い場合など)

// ✅ Server Component では getUser() を使う
const { data: { user }, error } = await supabase.auth.getUser()
if (error || !user) {
  redirect('/login')
}

getSession() はローカルのCookieに保存されたJWTをそのまま返すだけで、サーバー側での検証を行いません。一方 getUser() はSupabaseのサーバーにリクエストを送り、トークンが有効であることを確認します。

ページコンポーネントでの正しい使い方

// app/dashboard/page.tsx
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'

export default async function DashboardPage() {
  const supabase = await createClient()

  const { data: { user }, error } = await supabase.auth.getUser()

  if (error || !user) {
    redirect('/login')
  }

  return (
    <div>
      <h1>ダッシュボード</h1>
      <p>ログイン中: {user.email}</p>
    </div>
  )
}

原因4: next.config.js の設定でCookieがブロックされている

headers の設定で Set-Cookie がブロックされているケースがあります。特に X-Frame-Options や CSP を厳しく設定している場合に起きます。

// next.config.ts
// ❌ Supabase の Cookie を妨害する可能性のある設定
const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
    ]
  },
}

認証に関わるルートに Cache-Control: public を設定すると、CDNがレスポンスをキャッシュしてしまい、Set-Cookie ヘッダーが伝わらなくなります。

解決策: 認証ルートはキャッシュしない

// next.config.ts
const nextConfig = {
  async headers() {
    return [
      {
        // 静的アセットのみキャッシュ
        source: '/static/(.*)',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
      {
        // 認証コールバックは絶対にキャッシュしない
        source: '/auth/(.*)',
        headers: [
          {
            key: 'Cache-Control',
            value: 'no-store',
          },
        ],
      },
    ]
  },
}

export default nextConfig

原因5: メール認証コールバックのルートが実装されていない

Supabase のメール認証(マジックリンク・メール確認)は、ユーザーがリンクをクリックした後に コールバックURL にリダイレクトされ、そこでセッションを確立します。このルートがないと認証が完了しません。

# コールバックURLの例
http://localhost:3000/auth/callback?code=xxxx&next=/dashboard

Supabase ダッシュボードの Authentication → URL Configuration → Redirect URLs に登録したURLと、実装したルートが一致していないとエラーになります。

解決策: コールバックルートを実装する

// app/auth/callback/route.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function GET(request: NextRequest) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get('code')
  const next = searchParams.get('next') ?? '/'

  if (code) {
    const cookieStore = await cookies()
    const supabase = createServerClient(
      ({}).NEXT_PUBLIC_SUPABASE_URL!,
      ({}).NEXT_PUBLIC_SUPABASE_ANON_KEY!,
      {
        cookies: {
          getAll() {
            return cookieStore.getAll()
          },
          setAll(cookiesToSet) {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          },
        },
      }
    )

    const { error } = await supabase.auth.exchangeCodeForSession(code)

    if (!error) {
      const forwardedHost = request.headers.get('x-forwarded-host')
      const isLocalEnv = "production" === 'development'
      if (isLocalEnv) {
        return NextResponse.redirect(`${origin}${next}`)
      } else if (forwardedHost) {
        return NextResponse.redirect(`https://${forwardedHost}${next}`)
      } else {
        return NextResponse.redirect(`${origin}${next}`)
      }
    }
  }

  // エラー時はエラーページへ
  return NextResponse.redirect(`${origin}/auth/auth-code-error`)
}

Supabase ダッシュボードでリダイレクト先URLも忘れずに登録してください。

# Authentication → URL Configuration に追加
http://localhost:3000/auth/callback       # ローカル開発用
https://your-domain.com/auth/callback    # 本番環境用

まとめ

Supabase Auth + Next.js App Router の認証問題と対策を整理します。

症状 原因 対策
セッションが null / Auth session missing 旧パッケージ使用 @supabase/ssr に移行
ログイン後もセッションが消える middleware なし middleware.ts を追加
Server Component でセッション取得失敗 getSession() 使用 getUser() に変更
本番でCookieが保存されない キャッシュ設定の問題 認証ルートに no-store
メール認証後にエラー コールバックルートなし /auth/callback/route.ts を実装

まず @supabase/auth-helpers-nextjs@supabase/ssr に移行し、middleware.ts を追加する だけで大半のケースが解決します。Supabase の公式ドキュメントは頻繁に更新されるため、@supabase/ssr の README も合わせて確認することをおすすめします。