はじめに
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 も合わせて確認することをおすすめします。
