メインコンテンツへスキップ
Think You Lab
ブログログイン無料で始める
トップ/ブログ/how-to
Next.jsSupabase認証middlewareApp Router

Next.js App Router の middleware で Supabase 認証ガードを作る — 実装コード全公開

Next.js App Router の middleware を使った Supabase 認証ガードの実装を解説。THINK YOU LAB 本番稼働中の proxy.ts コードを全公開し、getUser() を使うべき理由・Cookie の双方向同期・よくある詰まりポイントを説明します。

2026-04-15·約12分

LINE 公式アカウント

AI副業スタートガイドをLINEで無料配布中

目次
  • middleware / proxy とは何か
  • App Router における middleware の役割
  • `proxy.ts` の配置場所(THINK YOU LAB の現行実装)
  • `matcher` で「どのリクエストに適用するか」を制御する
  • Supabase 認証を middleware で扱う際の注意点
  • `getSession()` ではなく `getUser()` を使う理由
  • Cookie の更新を middleware で行う理由
  • 実装コード(THINK YOU LAB 実例)
  • `proxy.ts` の全コード解説
  • 認証ガードのロジック
  • `next` クエリパラメータを付けてリダイレクトする
  • Cookie の双方向設定(`setAll` の中身)
  • `/api/webhooks` を matcher から除外する理由
  • よくある詰まりポイント
  • middleware が動かない(`matcher` の正規表現ミス)
  • リダイレクトループが起きる
  • Cookie が引き継がれない
  • まとめ

目次

  • middleware / proxy とは何か
  • App Router における middleware の役割
  • `proxy.ts` の配置場所(THINK YOU LAB の現行実装)
  • `matcher` で「どのリクエストに適用するか」を制御する
  • Supabase 認証を middleware で扱う際の注意点
  • `getSession()` ではなく `getUser()` を使う理由
  • Cookie の更新を middleware で行う理由
  • 実装コード(THINK YOU LAB 実例)
  • `proxy.ts` の全コード解説
  • 認証ガードのロジック
  • `next` クエリパラメータを付けてリダイレクトする
  • Cookie の双方向設定(`setAll` の中身)
  • `/api/webhooks` を matcher から除外する理由
  • よくある詰まりポイント
  • middleware が動かない(`matcher` の正規表現ミス)
  • リダイレクトループが起きる
  • Cookie が引き継がれない
  • まとめ

「ログインしていないユーザーが /member/ 配下にアクセスしたら /login に飛ばしたい」

この要件は Next.js の middleware / proxy レイヤーで実現します。ページコンポーネント側に認証チェックを書いても動きますが、複数ページに同じコードが散らばり、抜け漏れが起きやすくなります。認証ガードはリクエストの入口で一元管理するのがベストプラクティスです。

この記事では THINK YOU LAB 本番環境で稼働している middleware のコード(proxy.ts)を丸ごと解説します。


middleware / proxy とは何か

App Router における middleware の役割

Next.js の middleware は、リクエストがサーバーに届いてからページコンポーネントが実行されるより前に動く処理です。すべてのリクエストに対してフィルタリング・リダイレクト・レスポンス加工を行えます。

認証ガードの文脈では「Cookie を見てユーザーが認証済みかどうかを判定し、未認証なら /login に転送する」役割を担います。

proxy.ts の配置場所(THINK YOU LAB の現行実装)

THINK YOU LAB の現行実装では、認証ガードはプロジェクトルートの proxy.ts に置いています。

lms/
  app/
  proxy.ts        ← 認証ガード本体

この proxy.ts に proxy 関数と config.matcher を定義することで、どのパスに認証ガードを適用するかを制御できます。

matcher で「どのリクエストに適用するか」を制御する

export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|api/webhooks).*)",
  ],
};

この正規表現は「以下のパスを除くすべてのリクエストに middleware を適用する」という意味です。

| 除外パス | 理由 | |----------|------| | _next/static | 静的ファイル(JS・CSS)。認証不要 | | _next/image | Next.js の画像最適化エンドポイント。認証不要 | | favicon.ico | ファビコン。認証不要 | | api/webhooks | Stripe Webhook など。自前の署名検証で保護(後述) |


Supabase 認証を middleware で扱う際の注意点

getSession() ではなく getUser() を使う理由

Supabase の認証情報を取得するメソッドは2つあります。

| メソッド | 動作 | セキュリティ | |----------|------|------------| | getSession() | Cookie のセッション情報をそのまま返す | Cookie が改ざんされていても検知できない | | getUser() | Supabase サーバーにトークンの有効性を問い合わせる | サーバー側で検証するため、改ざんを検知できる |

middleware での認証判定は必ず getUser() を使うことが Supabase 公式の推奨です。

Cookie の更新を middleware で行う理由

Supabase のアクセストークンは有効期限(デフォルト1時間)があります。getUser() を呼ぶ際にリフレッシュ処理が自動的に実行されます。middleware がすべてのリクエストに対して実行されるため、トークンが切れたタイミングでも自然にリフレッシュされ、ユーザーはログイン状態を維持できます。


実装コード(THINK YOU LAB 実例)

proxy.ts の全コード解説

// lms/proxy.ts
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
 
const MEMBER_PREFIX = "/member";
const ADMIN_PREFIX = "/admin";
 
export async function proxy(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request });
 
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.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)
          );
        },
      },
    }
  );
 
  const {
    data: { user },
  } = await supabase.auth.getUser();
 
  const { pathname } = request.nextUrl;
 
  if (
    !user &&
    (pathname.startsWith(MEMBER_PREFIX) || pathname.startsWith(ADMIN_PREFIX))
  ) {
    const loginUrl = request.nextUrl.clone();
    loginUrl.pathname = "/login";
    loginUrl.searchParams.set("next", pathname);
    return NextResponse.redirect(loginUrl);
  }
 
  if (user && pathname.startsWith(ADMIN_PREFIX)) {
    const { data: profile } = await supabase
      .from("users")
      .select("role")
      .eq("id", user.id)
      .single();
 
    if (profile?.role !== "admin") {
      const url = request.nextUrl.clone();
      url.pathname = "/member/courses";
      return NextResponse.redirect(url);
    }
  }
 
  return supabaseResponse;
}
 
export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|api/webhooks).*)",
  ],
};

認証ガードのロジック

コアのガードロジックはシンプルです。

if (
  !user &&
  (pathname.startsWith(MEMBER_PREFIX) || pathname.startsWith(ADMIN_PREFIX))
) {
  const loginUrl = request.nextUrl.clone();
  loginUrl.pathname = "/login";
  loginUrl.searchParams.set("next", pathname);
  return NextResponse.redirect(loginUrl);
}

条件は2つ:

  1. user が null(未認証)
  2. アクセス先が /member または /admin で始まる

/login や公開ページはこれらのプレフィックスで始まらないため、未認証でも通過します。現行実装では /admin に対してロール判定も入っており、ログイン済みでも role != 'admin' のユーザーは /member/courses へ戻します。

next クエリパラメータを付けてリダイレクトする

loginUrl.searchParams.set("next", pathname);

ログインページが元のアクセス先パスを保持します。ログイン成功後に /auth/callback が next パラメータを受け取って元のページにリダイレクトするため、「ログインしてから元いたページに戻る」という自然な UX が実現します。

Cookie の双方向設定(setAll の中身)

setAll(cookiesToSet) {
  cookiesToSet.forEach(({ name, value }) =>
    request.cookies.set(name, value)       // (1) リクエストを更新
  );
  supabaseResponse = NextResponse.next({ request }); // (2) 新しいレスポンスを作り直す
  cookiesToSet.forEach(({ name, value, options }) =>
    supabaseResponse.cookies.set(name, value, options) // (3) レスポンスに Cookie を設定
  );
},

この3ステップを省略すると Cookie の同期が壊れ、ページ遷移のたびにセッションが失われます。

/api/webhooks を matcher から除外する理由

Stripe が送る Webhook リクエストには Supabase の Cookie が存在しません。/api/webhooks/stripe は Stripe の Webhook 署名(STRIPE_WEBHOOK_SECRET)で保護するため、middleware の対象から意図的に外しています。

Stripe Webhook 実装の詳細 → Supabase + Stripe Webhook で会員ステータスを自動更新する

DB レベルのアクセス制御は RLS で → Supabase RLS の書き方入門


よくある詰まりポイント

middleware が動かない(matcher の正規表現ミス)

症状: 保護したはずのページが未認証でアクセスできる

よくある間違い:

// 間違い: / で始まらないパスを書いてしまっている
matcher: ["member/:path*"]
 
// 正しい
matcher: ["/member/:path*"]

また、認証ガードのファイル位置が正しいか確認します。THINK YOU LAB の現行実装では proxy.ts をプロジェクトルート直下(lms/proxy.ts)に置いています。

リダイレクトループが起きる

症状: ログインページにアクセスするたびにリダイレクトが繰り返される

原因: /login ページ自体が MEMBER_PREFIX や ADMIN_PREFIX に含まれていないか確認します。また getUser() が常に null を返す(セッション Cookie が壊れている)ケースも原因になります。

Cookie が引き継がれない

症状: ログイン後に別ページに遷移すると、また未認証状態になる

原因: setAll の実装が不完全。特に supabaseResponse = NextResponse.next({ request }) のリセットが抜けていると、新しい Cookie がレスポンスに含まれません。

ブラウザの開発者ツールで「Application」→「Cookies」を見て、sb-xxx-auth-token のような Cookie が設定されているか確認します。


まとめ

Next.js App Router の middleware を使った認証ガードのポイント:

  • getUser() を使う(getSession() は Cookie 改ざんを検知できないため不可)

  • setAll は request・response の両方に Cookie を設定する(3ステップ)

  • /login などの公開ページは保護対象から外す(MEMBER_PREFIX / ADMIN_PREFIX で管理)

  • next パラメータでログイン後の復帰先を保持する

  • /api/webhooks は matcher から除外し、Stripe 署名検証に委ねる

  • Supabase 認証(magic link)の実装はこちら → Supabase magic link でログイン機能を実装する

  • DB レベルのアクセス制御は RLS で → Supabase RLS の書き方入門


middleware で認証ガードを作ると、ログインしていないユーザーを弾く処理が1ファイルで管理できます。Next.js 認証 middleware テンプレを LINE で無料配布中です。

→ Next.js 認証 middleware テンプレを受け取る(LINE登録・無料)

LINE 公式アカウント

Next.js App Router 実装Tips・Supabase Auth 設定ガイドをLINEで配信中

R

Rikuto (LAB)

非エンジニアが Claude Code × n8n × Supabase で副業システムを作り続ける実験記。 失敗も含めたリアルな一次情報を発信しています。

THINK YOU LAB 運営

関連記事

  • Supabase Auth で magic link ログインを実装する — Next.js App Router 対応

    2026-04-15

  • Stripe Checkout を Next.js Server Action で実装する — Supabase Auth 連携つき

    2026-04-15

  • Supabase + Stripe Webhook で会員ステータスを自動更新する — Next.js App Router 実装

    2026-04-15

← ブログ一覧へ
X でシェアLINE でシェア
← 前の記事Stripe Checkout を Next.js Server Action で実装する — Supabase Auth 連携つき
次の記事 →n8n Webhook の使い方|URL 発行から Slack 通知まで実例付きで解説

目次

  • middleware / proxy とは何か
  • App Router における middleware の役割
  • `proxy.ts` の配置場所(THINK YOU LAB の現行実装)
  • `matcher` で「どのリクエストに適用するか」を制御する
  • Supabase 認証を middleware で扱う際の注意点
  • `getSession()` ではなく `getUser()` を使う理由
  • Cookie の更新を middleware で行う理由
  • 実装コード(THINK YOU LAB 実例)
  • `proxy.ts` の全コード解説
  • 認証ガードのロジック
  • `next` クエリパラメータを付けてリダイレクトする
  • Cookie の双方向設定(`setAll` の中身)
  • `/api/webhooks` を matcher から除外する理由
  • よくある詰まりポイント
  • middleware が動かない(`matcher` の正規表現ミス)
  • リダイレクトループが起きる
  • Cookie が引き継がれない
  • まとめ
Think You Lab
このブログについて料金プランプライバシーポリシーお問い合わせ特定商取引法ログイン

© 2026 Think You Lab