【Next.js 14】Supabase AuthとMiddlewareの連携でハマった話(セッション管理の罠)

Nextjs

はじめに

現在、フリーランス活動に向けたポートフォリオとして、Next.js (App Router)Supabase を使ったWebアプリ(予約管理システム)を開発しています。

普段はPythonや業務システムのバックエンドを触ることが多いのですが、今回は「モダンな構成でフルスタック開発をしたい」と思い、Vercelへのデプロイまでを目標に構築を始めました。

しかし、開発初期の「ユーザー認証(ログイン機能)」の実装で、想定以上に時間を溶かしてしまいました。 今回は、備忘録も兼ねて「Next.jsのMiddlewareにおけるSupabaseセッション管理のハマりポイントと解決策」を共有します。

開発環境

  • Framework: Next.js 14 (App Router)
  • BaaS: Supabase
  • Library: @supabase/ssr (以前の auth-helpers ではなく推奨の最新版を使用)
  • Language: TypeScript

ぶつかった壁:リロードするとログアウトしてしまう

Supabaseの公式ドキュメント通りに実装し、ログイン画面からGoogle認証などでサインインすることには成功しました。 しかし、トップページに戻ったり、ブラウザをリロードしたりすると、なぜか勝手にログアウト状態に戻ってしまう(セッションが維持されない) という現象が発生しました。

コンソールを見ても派手なエラーは出ておらず、原因の特定に苦労しました。

原因:MiddlewareでのCookie更新処理が漏れていた

結論から言うと、原因は middleware.ts におけるセッション(Cookie)の更新処理の記述不足 でした。

Next.jsのApp Router(Server Components)環境では、サーバー側とクライアント側で正しくCookieをやり取りするために、リクエストごとにセッション情報をリフレッシュする必要があります。

私は最初、クライアントサイドのコードばかり気にしていましたが、重要なのはサーバーへの入り口である「ミドルウェア」の設定でした。

解決策:updateSessionの実装

公式ドキュメント(Server-Side Auth)を再度読み込み、middleware.ts を以下のように修正することで解決しました。

実際の修正コード(middleware.ts)

import { type NextRequest } from 'next/server'
import { updateSession } from '@/utils/supabase/middleware'

export async function middleware(request: NextRequest) {
  // ここでセッションを更新しないと、Server Component側でユーザー情報を取得できない
  return await updateSession(request)
}

export const config = {
  matcher: [
    /*
     * 以下のパスを除外して、それ以外ですべてミドルウェアを適用
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

そして、呼び出している utils/supabase/middleware.ts 側で、リクエストとレスポンスのCookieを操作する処理を記述します。

// utils/supabase/middleware.ts の一部抜粋
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function updateSession(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: any) {
          request.cookies.set({ name, value, ...options })
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          })
          response.cookies.set({ name, value, ...options })
        },
        remove(name: string, options: any) {
          // ...省略(削除処理)
        },
      },
    }
  )

  await supabase.auth.getUser()
  return response
}

この処理を追加したことで、リロードしても、別ページに遷移しても、正しくログイン状態(User Session)が維持されるようになりました。

学び:App Router時代の認証は「サーバー視点」が重要

React(SPA)だけの開発に慣れていると、どうしても「ブラウザのローカルストレージにトークンがあればOK」と考えがちです。 しかし、Next.js App RouterのようなSSR(サーバーサイドレンダリング)主体のフレームワークでは、「サーバーがリクエストを受け取った瞬間に、そのユーザーが誰かを知る必要がある」という点を強く意識する必要があると痛感しました。

今後の展望

認証周りの基盤が整ったので、現在は以下の機能実装を進めています。

  1. データベース設計: SupabaseのRow Level Security (RLS) を活用した、堅牢なデータアクセスの構築。
  2. UI実装: Tailwind CSSを使ったレスポンシブな管理画面の作成。

特にRLS(行レベルセキュリティ)は、バックエンドエンジニアとしての腕の見せ所だと感じているので、また設定で工夫した点があれば記事にまとめようと思います。

コメント

タイトルとURLをコピーしました