言語を切り替える
テーマを切り替える

Next.js ルート保護と権限管理:Middleware と多層防御の完全ガイド

先月、クライアントのセキュリティ監査を行った際、私が書いた権限管理コードを見て監査員が眉をひそめました。「これだけ? Middleware でブロックしているだけですか?」

当時は納得がいきませんでした。Middleware こそがそのためのものではないのか? 未ログインユーザーをログインページにリダイレクトし、ログイン済みなら通す。完璧に見えました。

監査員はブラウザの開発者ツールを開き、フロントエンドのルーティングをバイパスして、Postman でバックエンド API を直接叩いて見せました。保護されるべきデータがいとも簡単に取得されてしまったのです。

その瞬間、頭が真っ白になりました。

その後いろいろ調べてようやく理解しました。Next.js の権限管理は、Middleware だけで完結するものではないということを。完全な多層防御アーキテクチャが必要なのです。

この記事では以下について話します:Middleware はどう使うべきか? getServerSession との関係は? 管理画面の RBAC(ロールベースアクセス制御)体系はどう設計すべきか? そして、すぐに使えるコードテンプレートも紹介します。

もしあなたが Next.js の権限管理を構築中だったり、Middleware と getServerSession の連携に疑問を持っているなら、この記事が役立つはずです。

なぜ権限管理は Middleware だけではダメなのか

Middleware の役割とは

まず Middleware の立ち位置をはっきりさせましょう。

これは Edge Runtime 上で動作し、ユーザーリクエストが最初に行き着く層です。ここで「粗いフィルタリング」を行えます。ユーザーがログインしているか、管理者か一般ユーザーか、通すべきかリダイレクトすべきか、といったことです。

処理が速く、位置も早い。権限管理に最適だと思えますよね?

確かに適しています。しかし問題は、それだけに頼ると不十分だということです。

実際の脆弱性事例

今年1月、セキュリティ研究者が CVE-2025-29927 の脆弱性を公表しました。簡単に言うと、攻撃者がリクエストヘッダーに特殊な x-middleware-subrequest フィールドを追加することで、Middleware のチェックを直接バイパスできるというものです。

Middleware に書いたロジックは、この攻撃の前では無力です。

これは特殊な例ではありません。Middleware は最も外側の防衛線であるため、それ自体が回避されたり、設定ミスがあったり、エッジ環境の制限で複雑な権限判断ができなかったりする可能性があります。

Next.js の公式ドキュメントでも特に強調されています。「Middleware は初期チェックには有用だが、唯一の防衛線にすべきではない(While Middleware can be useful for initial checks, it should not be your only line of defense.)」

要するに、Middleware に全てを賭けるなということです。

フロントエンドで防いでも、バックエンドは?

以前、管理画面プロジェクトを担当した時のことです。Middleware でログインチェックを書き、未ログインユーザーが /admin ルートにアクセスするとログインページにリダイレクトされるようにしました。

見た目は安全です。ユーザーがメニューをクリックし、ルート遷移すれば、Middleware のチェックを通ります。

何が問題だったのか? API を防いでいなかったのです。

好奇心の強いテスト担当の同僚が Network パネルを見て、ユーザー削除のインターフェースが POST /api/users/delete であることを見つけました。彼はカール(curl)でその API を直接叩き、パラメータを適当に入れました。

削除成功。

API Route に権限チェックが全くなかったからです。私は Middleware でフロントエンドのルートを塞いだだけで、誰かが直接 API を叩く可能性を完全に無視していました。

これが Middleware だけに依存する問題点です。フロントエンドのドアは閉められても、バックエンドの窓は開いたままなのです。

Next.js 公式推奨の手法

公式ドキュメントでは「近接性の原則(Proximity Principle)」が言及されています。権限チェックはデータのなるべく近くで行うべきだという原則です。

どういうことか?

データはデータベースにあります。データを守るなら、データベースにアクセスする直前に権限チェックを行うべきです。ルート層でもページ層でもなく、データ層で。

もちろん、Middleware が無駄というわけではありません。Middleware は第一層の拦截(インターセプト)を行えます。未ログインの重定向(リダイレクト)、明らかに権限のないロールの拒否などです。しかしこの層だけでなく、以下も必要です:

  • Server Component 内でのチェック:ページレンダリング前に、ユーザーがそのページにアクセスする権限があるか検証する
  • API Route と Server Action 内でのチェック:各データ操作の前に、再度権限を検証する
  • データベースクエリ層でのチェック:Row-Level Security やクエリフィルタリングを通じて、ユーザーが権限のあるデータにしかアクセスできないようにする

多層防御です。一層が破られても、次の層があります。

正直、最初は面倒だと思いました。しかし痛い目を見て知りました。セキュリティにおいて、あなたが面倒だと思う場所こそ、攻撃者が興味を持つ場所なのです。

Middleware と getServerSession の正しい連携

なぜ Middleware で getServerSession が使えないのか

NextAuth を使い始めた頃、私もこの問題に悩まされました。

ドキュメントを見ると、Server Component でセッションを取得するには getServerSession(authOptions) を使うとあります。自然と、Middleware でもそう使おうとしました。

そしてエラーになりました。

調べてようやく分かりました。Middleware は Edge Runtime で動作し、getServerSession は Node.js Runtime を必要とします。両者は互換性がありません。

Edge Runtime は Vercel が開発した軽量な実行環境で、Node.js の完全な API はありませんが、高速でグローバルに分散されています。Middleware はパフォーマンスのために Edge Runtime を選択しましたが、その代償として Node.js の機能が全て使えるわけではありません。

では、Middleware でどうやってセッションを取得するのか?

正しい方法:getToken または withAuth を使う

NextAuth は Middleware 専用の2つの API を提供しています。

方法1:getToken を使用

// middleware.ts
import { getToken } from "next-auth/jwt"
import { NextResponse } from "next/server"

export async function middleware(req) {
  const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET })
  
  if (!token) {
    return NextResponse.redirect(new URL('/login', req.url))
  }
  
  // ロールチェック
  if (req.nextUrl.pathname.startsWith('/admin') && token.role !== 'admin') {
    return NextResponse.redirect(new URL('/403', req.url))
  }
  
  return NextResponse.next()
}

export const config = {
  matcher: ['/admin/:path*', '/dashboard/:path*']
}

getToken はリクエストのクッキーから JWT トークンを解析し、ユーザー情報を取得します。注意点として、これは JWT セッション戦略のみサポートしており、データベースセッションを使用している場合は使えません。

方法2:withAuth 高階関数を使用

// middleware.ts
import { withAuth } from "next-auth/middleware"

export default withAuth({
  callbacks: {
    authorized: ({ token, req }) => {
      // 未ログイン
      if (!token) return false
      
      // admin ルートは admin ロールのみアクセス許可
      if (req.nextUrl.pathname.startsWith('/admin')) {
        return token.role === 'admin'
      }
      
      return true
    }
  }
})

export const config = {
  matcher: ['/admin/:path*', '/dashboard/:path*']
}

withAuth はラッパー関数で、リダイレクトロジックを処理してくれます。authorized コールバックで truefalse を返すだけで、自動的にログインページへリダイレクトします。

個人的には withAuth の方がコードが簡潔で好みです。

では getServerSession はどこで使う?

getServerSession は Server Component、API Route、Server Action で使用します。

Server Component 内:

// app/admin/page.tsx
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { redirect } from "next/navigation"

export default async function AdminPage() {
  const session = await getServerSession(authOptions)
  
  if (!session) {
    redirect('/login')
  }
  
  if (session.user.role !== 'admin') {
    redirect('/403')
  }
  
  // ページレンダリング
  return <UsersList />
}

この層で、ユーザーがこのページにアクセスする権限があるかチェックします。権限がなければ見せません。

API Route 内:

// app/api/users/route.ts
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { NextResponse } from "next/server"

export async function DELETE(req: Request) {
  const session = await getServerSession(authOptions)
  
  if (!session || session.user.role !== 'admin') {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
  }
  
  // 削除実行
  // ...
  
  return NextResponse.json({ success: true })
}

Server Action 内:

// app/actions.ts
'use server'

import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"

export async function deleteUser(userId: string) {
  const session = await getServerSession(authOptions)
  
  if (!session || session.user.role !== 'admin') {
    throw new Error('Unauthorized')
  }
  
  // 削除実行
  // ...
}

両者の役割分担

整理すると:

  • Middleware(getTokenを使用):粗い粒度のルート遮断。未ログインの一括リダイレクト、基本的なロールチェックなど。
  • getServerSession:細かい粒度の権限管理。実際にデータを操作する前の最後の検証。

例えるなら、Middleware はマンション入り口の警備員で、明らかに住人でない人を止めます。getServerSession は自宅のドアの鍵で、警備員が通しても、鍵を持っていなければ家には入れません。

多層防御とは、こういうことです。

管理画面の RBAC 完全設計

Middleware と getServerSession の連携について話しましたが、これらはまだ断片的です。管理画面を構築するには、完全な RBAC(ロールベースアクセス制御)システムが必要です。

まず RBAC モデルを整理する

RBAC の核心的な考え方は:ユーザー → ロール → 権限 です。

  • ユーザー(User):田中、佐藤
  • ロール(Role):管理者、編集者、閲覧者
  • 権限(Permission):ユーザー一覧閲覧、記事編集、コメント削除

1人のユーザーは複数のロールを持つことができ、1つのロールは複数の権限を含みます。例えば田中は管理者ですべての権限を持ち、佐藤は編集者で記事編集とユーザー一覧閲覧のみ可能です。

権限の粒度は3つに分けられます:

  • ページレベル:特定のページにアクセスできるか(例:/admin/users
  • 機能レベル:特定のボタンをクリックできるか(例:「削除」ボタン)
  • データレベル:特定のデータを見られるか(例:自分が作成した記事のみ閲覧可能)

データベース設計(Prisma Schema)は概ね以下のようになります:

model User {
  id    String @id @default(cuid())
  email String @unique
  roles Role[]
}

model Role {
  id          String       @id @default(cuid())
  name        String       @unique
  permissions Permission[]
  users       User[]
}

model Permission {
  id    String @id @default(cuid())
  name  String @unique // 例: "user:view", "user:edit"
  roles Role[]
}

実際のプロジェクトでは、これほど複雑な DB 設計は不要かもしれません。ロールが多くない(管理者、編集者、閲覧者の3つだけなど)場合は、コード内で定義するだけで十分です。

四層防御アーキテクチャ

次に、権限チェックを各層にどう実装するか解説します。

第1層:Middleware - 粗い粒度のフィルタリング

// middleware.ts
import { withAuth } from "next-auth/middleware"

export default withAuth({
  callbacks: {
    authorized: ({ token, req }) => {
      if (!token) return false
      
      const path = req.nextUrl.pathname
      
      // admin パスは admin ロールのみ
      if (path.startsWith('/admin')) {
        return token.role === 'admin'
      }
      
      // dashboard パスはログインしていればOK
      if (path.startsWith('/dashboard')) {
        return true
      }
      
      return false
    }
  }
})

export const config = {
  matcher: ['/admin/:path*', '/dashboard/:path*']
}

この層では基本的なロール判断のみ行い、具体的な機能権限には触れません。

第2層:Server Component - ページレベル権限チェック

// app/admin/users/page.tsx
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { checkPermission } from "@/lib/permissions"
import { redirect } from "next/navigation"

export default async function UsersPage() {
  const session = await getServerSession(authOptions)
  
  if (!session) {
    redirect('/login')
  }
  
  // ユーザー一覧閲覧権限があるかチェック
  const hasPermission = await checkPermission(session.user.id, 'user:view')
  
  if (!hasPermission) {
    redirect('/403')
  }
  
  // ページレンダリング
  return <UsersList />
}

この層では、ユーザーがそのページにアクセスする権限があるか確認します。なければ見せません。

第3層:UI 条件付きレンダリング - 機能レベル権限

// components/UsersList.tsx
'use client'

import { useSession } from "next-auth/react"
import { hasPermission } from "@/lib/permissions-client"

export function UsersList() {
  const { data: session } = useSession()
  
  const canEdit = hasPermission(session, 'user:edit')
  const canDelete = hasPermission(session, 'user:delete')
  
  return (
    <div>
      {users.map(user => (
        <div key={user.id}>
          <span>{user.name}</span>
          {canEdit && <button>編集</button>}
          {canDelete && <button>削除</button>}
        </div>
      ))}
    </div>
  )
}

この層では権限に基づいてボタンを隠したり表示したりします。ユーザーに見えないボタンは、当然クリックされません。

ただし注意してください。これは UX 最適化であり、セキュリティ対策ではありません。知識のある人はブラウザコンソールでボタンを表示させることができます。真のセキュリティチェックは次の層にあります。

第4層:Server Action / API - データ操作前検証

// app/actions.ts
'use server'

import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { checkPermission } from "@/lib/permissions"

export async function deleteUser(userId: string) {
  const session = await getServerSession(authOptions)
  
  if (!session) {
    throw new Error('Unauthorized')
  }
  
  // 削除権限が必須
  const hasPermission = await checkPermission(session.user.id, 'user:delete')
  
  if (!hasPermission) {
    throw new Error('Forbidden')
  }
  
  // 削除実行
  await db.user.delete({ where: { id: userId } })
  
  return { success: true }
}

これが最後の防衛線です。前の層でチェックしたかどうかに関わらず、データ操作の段階で必ず再検証します。

権限設定の集中管理

あちこちに権限チェックを書くのは面倒です。私のやり方は、権限定義とチェックロジックを1つのファイルにまとめることです。

// lib/permissions.ts
export const PERMISSIONS = {
  USER_VIEW: 'user:view',
  USER_EDIT: 'user:edit',
  USER_DELETE: 'user:delete',
  POST_VIEW: 'post:view',
  POST_EDIT: 'post:edit',
  POST_DELETE: 'post:delete',
} as const

export const ROLES = {
  ADMIN: {
    name: 'admin',
    permissions: Object.values(PERMISSIONS) // 管理者は全権限
  },
  EDITOR: {
    name: 'editor',
    permissions: [
      PERMISSIONS.USER_VIEW,
      PERMISSIONS.POST_VIEW,
      PERMISSIONS.POST_EDIT,
    ]
  },
  VIEWER: {
    name: 'viewer',
    permissions: [
      PERMISSIONS.USER_VIEW,
      PERMISSIONS.POST_VIEW,
    ]
  }
} as const

// サーバーサイド権限チェック
export async function checkPermission(userId: string, permission: string) {
  const user = await db.user.findUnique({
    where: { id: userId },
    include: { roles: true }
  })
  
  if (!user) return false
  
  // ユーザーロールが必要な権限を含んでいるかチェック
  const userRole = ROLES[user.role as keyof typeof ROLES]
  return userRole?.permissions.includes(permission) ?? false
}
// lib/permissions-client.ts (クライアント版)
import { Session } from "next-auth"

export function hasPermission(session: Session | null, permission: string) {
  if (!session?.user) return false
  
  const userRole = ROLES[session.user.role as keyof typeof ROLES]
  return userRole?.permissions.includes(permission) ?? false
}

こうすれば、権限の追加・削除・変更が1ファイルで済み、コードを探し回る必要がありません。

このアーキテクチャのメリット

何層もコードを書く価値はあるのか?

あります。

  • 安全性:一層が破られても、他が守る
  • 保守性:権限設定が集中管理され、変更が容易
  • UX:隠すべきボタンを隠し、「クリックしてから権限がないと言われる」事態を防ぐ
  • 監査対応:各層に明確なチェックがあり、セキュリティ監査をパスしやすい

正直、最初は構築に時間がかかります。しかし新機能追加や権限ルール変更の際、圧倒的に楽になります。

実戦テンプレート

原理とアーキテクチャを説明しました。ここからは、すぐに使えるコードテンプレートとよくある問題のトラブルシューティングです。

完全な Middleware 設定テンプレート

これは、多ロール、動的ルートマッチングをサポートする完全な Middleware 設定です:

// middleware.ts
import { withAuth } from "next-auth/middleware"
import { NextResponse } from "next/server"

export default withAuth(
  function middleware(req) {
    const token = req.nextauth.token
    const path = req.nextUrl.pathname
    
    // パスとロールに基づき細かく制御
    if (path.startsWith('/admin') && token?.role !== 'admin') {
      return NextResponse.redirect(new URL('/403', req.url))
    }
    
    if (path.startsWith('/editor') && !['admin', 'editor'].includes(token?.role as string)) {
      return NextResponse.redirect(new URL('/403', req.url))
    }
    
    return NextResponse.next()
  },
  {
    callbacks: {
      authorized: ({ token }) => !!token
    }
  }
)

export const config = {
  matcher: [
    '/admin/:path*',
    '/editor/:path*',
    '/dashboard/:path*',
    '/api/admin/:path*',
    '/api/editor/:path*'
  ]
}

ポイント

  1. matcher で保護対象パスを定義、ワイルドカード :path* 対応
  2. authorized コールバックで基本的なログインチェック
  3. middleware 関数内でより細かいロール判断

動的メニューレンダリング

ユーザー権限に応じたメニュー生成は管理画面の定番要件です。シンプルで実用的な実装例です:

// components/Sidebar.tsx
'use client'

import { useSession } from "next-auth/react"
import Link from "next/link"
import { hasPermission } from "@/lib/permissions-client"

const menuItems = [
  {
    label: 'ユーザー管理',
    href: '/admin/users',
    permission: 'user:view'
  },
  {
    label: '記事管理',
    href: '/admin/posts',
    permission: 'post:view'
  },
  {
    label: 'システム設定',
    href: '/admin/settings',
    permission: 'setting:manage'
  }
]

export function Sidebar() {
  const { data: session } = useSession()
  
  // 権限に基づいてメニュー項目をフィルタリング
  const visibleItems = menuItems.filter(item => 
    hasPermission(session, item.permission)
  )
  
  return (
    <nav>
      {visibleItems.map(item => (
        <Link key={item.href} href={item.href}>
          {item.label}
        </Link>
      ))}
    </nav>
  )
}

メニュー設定と権限を紐付ければ一目瞭然です。メニュー追加時は menuItems 配列に足すだけです。

再利用可能な権限チェック Hook

クライアントコンポーネントでより便利に使うための React Hook のカプセル化:

// hooks/usePermission.ts
import { useSession } from "next-auth/react"
import { hasPermission } from "@/lib/permissions-client"

export function usePermission(permission: string) {
  const { data: session, status } = useSession()
  
  const isLoading = status === 'loading'
  const isAllowed = hasPermission(session, permission)
  
  return { isAllowed, isLoading }
}

使い方は非常にシンプルです:

// components/DeleteButton.tsx
'use client'

import { usePermission } from "@/hooks/usePermission"

export function DeleteButton({ userId }: { userId: string }) {
  const { isAllowed, isLoading } = usePermission('user:delete')
  
  if (isLoading) return <div>Loading...</div>
  if (!isAllowed) return null
  
  return (
    <button onClick={() => deleteUser(userId)}>
      削除
    </button>
  )
}

よくある問題のトラブルシューティング

問題1:Middleware の無限リダイレクト

症状:ページアクセス時にブラウザが「リダイレクト回数が多すぎます」とエラーになる。

原因:ログインページ自体も Middleware にインターセプトされ、循環リダイレクトしている。

解決:matcher でログインページや公開ページを除外する。

export const config = {
  matcher: [
    /*
     * すべてのパスにマッチ、ただし以下を除く:
     * - /login (ログインページ)
     * - /api/auth (NextAuth API)
     * - /_next (Next.js 内部)
     * - /favicon.ico, /robots.txt (静的ファイル)
     */
    '/((?!login|api/auth|_next|favicon.ico|robots.txt).*)',
  ]
}

問題2:getServerSession が null を返す

症状:ログイン済みなのに、getServerSession が常に null を返す。

原因:

  1. authOptions の設定ミス、または渡していない
  2. Cookie 設定の問題(クロスドメイン、HTTPS など)
  3. Middleware 内で間違って getServerSession を使っている

解決:

  • authOptions のインポート確認
  • Server Component または API Route で使用しているか確認(Middleware は不可)
  • 開発環境で NEXTAUTH_URL 環境変数が正しいか確認
// 正しい使い方
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/app/api/auth/[...nextauth]/route"

const session = await getServerSession(authOptions)

問題3:権限チェックのパフォーマンス問題

症状:ページ読み込みが遅い。毎回 DB アクセスして権限確認しているため。

原因:権限チェックがキャッシュされず、リクエストごとに DB 問い合わせしている。

解決:

  1. ユーザーロールと権限を JWT トークンに含め、DB アクセスを回避する
// app/api/auth/[...nextauth]/route.ts
export const authOptions: NextAuthOptions = {
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.role = user.role
        token.permissions = user.permissions
      }
      return token
    },
    async session({ session, token }) {
      session.user.role = token.role
      session.user.permissions = token.permissions
      return session
    }
  }
}
  1. React Query や SWR を使い、クライアント側で権限クエリ結果をキャッシュする
// hooks/usePermissions.ts
import useSWR from 'swr'

export function usePermissions() {
  const { data: permissions } = useSWR('/api/me/permissions', {
    revalidateOnFocus: false,
    dedupingInterval: 60000 // 1分間は重複リクエストしない
  })
  
  return permissions
}

クイックチェックリスト

リリース前に、権限管理を以下のリストでチェックしてください:

  • Middleware は粗いチェックのみ行っているか?
  • すべての Server Component ページで権限検証しているか?
  • すべての API Route で権限検証しているか?
  • すべての Server Action で権限検証しているか?
  • 権限に基づいて敏感なボタンを隠しているか?
  • 権限設定は集中管理されているか?
  • JWT トークンにロール情報が含まれているか?
  • ログインページや公開ページは Middleware の対象外か?

全てチェックできれば、あなたの権限管理は堅牢です。

結論

最初の問いに戻りましょう:Next.js の権限管理は結局どうすべきか?

答えは「一つに頼るな」です。

Middleware は重要です。第一の防衛線として、未認証アクセスの大部分を阻止します。しかしそれが全てではありません。Server Component でページ権限をチェックし、Server Action と API Route で操作権限を検証し、UI 層で体験を最適化する必要があります。

この多層防御アーキテクチャは一見面倒に見えます。しかしセキュリティに銀の弾丸はありません。一層が破られても他が守る、これが確実なやり方です。

Middleware のみ vs 多層防御

比較項目Middleware のみ多層防御
安全性低、回避可能高、多層バックアップ
開発コスト低、一箇所設定中、複数箇所にチェック追加
保守性悪、権限ロジック分散良、設定集中管理
ユーザー体験普通、クリック後に権限なし判明良、使えない機能は事前非表示
監査対応悪、チェックポイント単一良、各層に記録あり

ご覧の通り、多層防御は手間がかかりますが、メリットは計り知れません。

今すぐ行動を

Next.js プロジェクト進行中なら、今すぐチェックしてください:

  1. プロジェクトは Middleware だけで権限管理していませんか?
  2. API Route と Server Action に独立した権限検証はありますか?
  3. 権限設定が各ファイルに散らばっていませんか?

どれか一つでも「Yes」なら、早急に多層防御アーキテクチャへのリファクタリングをお勧めします。一気にやる必要はありません。まずは最も重要なデータ操作層に権限チェックを追加し、徐々に他を補完していけば良いのです。

セキュリティ問題は、後回しにするほど修正コストが高くなります。

まだ疑問がありますか?

この記事では Next.js 権限管理の核心アーキテクチャと実戦ソリューションを話しましたが、全シナリオは網羅できていません。もし実際のプロジェクトで以下のような問題に直面したら:

  • データレベル権限のやり方(自分が作成したデータのみ閲覧)
  • Prisma の Row-Level Security との組み合わせ
  • マルチテナントシステムの権限分離

ぜひコメントで教えてください、一緒に考えましょう。

権限管理は永遠に古びないトピックです。この記事がいくつかの落とし穴を避ける助けになれば幸いです。

Next.js 多層権限防御 完全構成フロー

Middleware 基礎チェックから getServerSession 詳細検証、データベース権限チェックまでの完全ステップ

⏱️ Estimated time: 3 hr

  1. 1

    Step1: 第1層:Middleware 基礎チェック

    middleware.ts 作成:
    ```ts
    import { withAuth } from 'next-auth/middleware'

    export default withAuth({
    pages: {
    signIn: '/login'
    }
    })

    export const config = {
    matcher: ['/dashboard/:path*', '/admin/:path*']
    }
    ```

    役割:
    • ユーザーログイン確認
    • 未ログインのリダイレクト
    • 高速、初期段階

    制限:
    • 回避可能(x-middleware-subrequest 脆弱性)
    • 粗いフィルタリングのみ
    • 詳細な権限チェック不可

    ポイント:Middleware は第1層だが、唯一の防御にしてはいけない。
  2. 2

    Step2: 第2層:getServerSession 詳細検証

    ページ内で使用:
    ```tsx
    import { getServerSession } from 'next-auth'
    import { authOptions } from '@/app/api/auth/[...nextauth]/route'

    export default async function DashboardPage() {
    const session = await getServerSession(authOptions)

    if (!session) {
    redirect('/login')
    }

    // ユーザーロールチェック
    if (session.user.role !== 'admin') {
    redirect('/unauthorized')
    }

    return <div>Dashboard</div>
    }
    ```

    役割:
    • ユーザーIDの詳細検証
    • ユーザーロール確認
    • 細粒度の権限管理

    利点:
    • Middleware より安全
    • 詳細な権限チェック可
    • 回避不可

    ポイント:getServerSession は第2層で詳細検証を行う。
  3. 3

    Step3: 第3層:データベース権限チェック

    API Route でチェック:
    ```ts
    import { getServerSession } from 'next-auth'
    import { db } from '@/lib/db'

    export async function GET(request: Request) {
    const session = await getServerSession(authOptions)

    if (!session) {
    return new Response('Unauthorized', { status: 401 })
    }

    // DB 権限チェック
    const user = await db.user.findUnique({
    where: { id: session.user.id },
    include: { role: { include: { permissions: true } } }
    })

    const hasPermission = user.role.permissions.some(
    p => p.resource === 'users' && p.action === 'read'
    )

    if (!hasPermission) {
    return new Response('Forbidden', { status: 403 })
    }

    // データ返却
    return Response.json({ users: [...] })
    }
    ```

    役割:
    • 最終権限検証
    • データベース内の権限確認
    • データ安全確保

    ポイント:DB は第3層、最終検証。
  4. 4

    Step4: RBAC 権限体系の実装

    データベース Schema:
    ```prisma
    model User {
    id String @id @default(cuid())
    email String @unique
    role Role @relation(fields: [roleId], references: [id])
    roleId String
    }

    model Role {
    id String @id @default(cuid())
    name String @unique
    permissions Permission[]
    users User[]
    }

    model Permission {
    id String @id @default(cuid())
    resource String
    action String
    roles Role[]
    }
    ```

    権限チェック関数:
    ```ts
    async function checkPermission(
    userId: string,
    resource: string,
    action: string
    ) {
    const user = await db.user.findUnique({
    where: { id: userId },
    include: { role: { include: { permissions: true } } }
    })

    return user.role.permissions.some(
    p => p.resource === resource && p.action === action
    )
    }
    ```

    ポイント:
    • ユーザーはロールを持つ
    • ロールは権限を持つ
    • 権限がリソースアクセスを制御

FAQ

なぜ権限管理は Middleware だけではダメなのですか?
理由:Middleware は回避可能です。

脆弱性事例:
• CVE-2025-29927:リクエストヘッダーに x-middleware-subrequest を追加することで防御をバイパス可能
• Postman でバックエンド API を直接叩くとデータ取得可能

Middleware の位置づけ:
• Edge Runtime で動作する粗いフィルタ
• 未ログインチェック、ロール確認など
• 高速だが、これだけでは不十分

解決策:多層防御
• 第1層:Middleware(基礎チェック)
• 第2層:getServerSession(詳細検証)
• 第3層:DB権限チェック(最終検証)

ポイント:3層の防御で安全を確保し、どの層も省略してはいけません。
Middleware と getServerSession の違いは何ですか?
Middleware:
• Edge Runtime 動作
• 高速、初期段階
• 粗いフィルタ(ログイン状態、簡易ロール)
• 回避可能(脆弱性あり)

getServerSession:
• Node.js Runtime 動作
• 詳細検証可
• ロール、権限の詳細チェック
• 回避不可

併用:
• Middleware で基礎チェック(第1層)
• getServerSession で詳細検証(第2層)
• DB で最終権限チェック(第3層)

コード例:
```ts
// middleware.ts(第1層)
export default withAuth({
pages: { signIn: '/login' }
})

// page.tsx(第2層)
const session = await getServerSession(authOptions)
if (session.user.role !== 'admin') {
redirect('/unauthorized')
}

// api/route.ts(第3層)
const hasPermission = await checkPermission(userId, 'users', 'read')
if (!hasPermission) {
return new Response('Forbidden', { status: 403 })
}
```
RBAC 権限体系はどう実装しますか?
RBAC = Role-Based Access Control(ロールベースアクセス制御)

DB Schema:
```prisma
model User {
id String @id
email String @unique
role Role @relation(fields: [roleId], references: [id])
roleId String
}

model Role {
id String @id
name String @unique
permissions Permission[]
users User[]
}

model Permission {
id String @id
resource String // リソース:users, posts, etc.
action String // アクション:read, write, delete
roles Role[]
}
```

権限チェック:
```ts
async function checkPermission(
userId: string,
resource: string,
action: string
) {
const user = await db.user.findUnique({
where: { id: userId },
include: { role: { include: { permissions: true } } }
})

return user.role.permissions.some(
p => p.resource === resource && p.action === action
)
}
```

ポイント:
• ユーザー → ロール
• ロール → 権限
• 権限 → リソースアクセス
多層防御アーキテクチャの実装方法は?
3層の防御:

第1層:Middleware(基礎チェック)
```ts
// middleware.ts
export default withAuth({
pages: { signIn: '/login' }
})
```

第2層:getServerSession(詳細検証)
```tsx
// page.tsx
const session = await getServerSession(authOptions)
if (session.user.role !== 'admin') {
redirect('/unauthorized')
}
```

第3層:データベース権限チェック(最終検証)
```ts
// api/route.ts
const hasPermission = await checkPermission(userId, 'users', 'read')
if (!hasPermission) {
return new Response('Forbidden', { status: 403 })
}
```

ポイント:
• 3層で安全確保
• どの層も省略不可
• Middlewareは粗、getServerSessionは細、DBは最終
x-middleware-subrequest 脆弱性をどう防ぎますか?
脆弱性:リクエストヘッダーに x-middleware-subrequest を追加して Middleware をバイパスできる。

防御法:
• Middleware だけに頼らない
• API Route でも権限チェックを行う
• getServerSession で検証する
• DB で最終権限チェックを行う

コード例:
```ts
// api/route.ts
export async function GET(request: Request) {
// Middleware がバイパスされてもここでチェック
const session = await getServerSession(authOptions)

if (!session) {
return new Response('Unauthorized', { status: 401 })
}

// DB 権限チェック
const hasPermission = await checkPermission(session.user.id, 'users', 'read')

if (!hasPermission) {
return new Response('Forbidden', { status: 403 })
}

return Response.json({ users: [...] })
}
```

ポイント:
• 多層防御
• API Route 防御必須
• DB 最終検証
• Middleware だけは危険
権限管理のベストプラクティスは?
3層防御:
• Middleware(第1層)
• getServerSession(第2層)
• DB 権限チェック(第3層)

RBAC 体系:
• ユーザー → ロール
• ロール → 権限
• 権限 → リソース

コード構成:
• 権限チェック関数作成
• API Route で統一利用
• 重複コード回避

安全提案:
• Middleware だけに頼らない
• API Route 防御必須
• DB 最終検証
• 定期的な権限設定レビュー

ポイント:
• 多層防御で安全確保
• 全層必須
• 継続的な改善

重要:権限管理は安全の基礎。

9 min read · 公開日: 2025年12月19日 · 更新日: 2026年1月22日

コメント

GitHubアカウントでログインしてコメントできます

関連記事