Next.js: 認証 & 認可

最終更新:2026-08-26

フルスタックアプリケーションに認証を追加することは、オフィスビルに入退室管理システムを設置するようなものです — 誰が入室できるか、どのフロアにアクセスできるかを正確に制御する必要があります。

1. 学習目標



2. あるフルスタック開発者の実話

(1) 課題: ローンチ直前に認証がないことに気づいた

Alice は15人の SaaS スタートアップで働いています。彼女は Next.js 16 を使用してフル機能のチームコラボレーションプラットフォーム「TaskFlow」を開発しました。しかし、ローンチ前のセキュリティ監査で、テクニカルリードの Bob が指摘しました:

「君の /dashboard ページは誰でもアクセスでき、API エンドポイントにはトークン認証がなく、ユーザーデータが平文で露出している。」

監査レポートには以下の問題がリストされています:

問題 範囲 リスクレベル
ログインページなし すべてのルート 🔴 高
API ルート: 認証なし /api/projects/* 🔴 高
ロールの区別なし すべてのユーザーが管理者パネルを閲覧可能 🟡 中
セッションが期限切れしない 一度ログインすると永続的にログイン状態 🔴 高

(2) Auth.js + Clerk による解決策

Auth.js v5 を使用して標準的な認証フローを実装し、Clerk をゼロ設定の代替として使用します。

TS
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
import Google from 'next-auth/providers/google'
import Credentials from 'next-auth/providers/credentials'

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    GitHub,
    Google,
    Credentials({
      credentials: { email: {}, password: {} },
      async authorize(credentials) {
        const user = { id: '1', name: 'Alice', email: 'alice@taskflow.io', role: 'admin' }
        return credentials.email === 'alice@taskflow.io' && credentials.password === 'pass123' ? user : null
      }
    })
  ],
  callbacks: { session: ({ session, token }) => ({ ...session, user: { ...session.user, role: token.role } }) }
})

(3) 成果

指標 実装前 実装後
/dashboard に未ログインでアクセス アクセス可能 ログインページにリダイレクト
API エンドポイントのセキュリティ 検証なし getToken() JWT 検証
ロール制御 なし admin、editor、viewer の3段階
セッション有効期限 永続 30日で自動期限切れ
統合時間 Auth.js: 2時間 / Clerk: 30分


3. Auth.js v5 (NextAuth) プロバイダー統合

(1) プロバイダーアーキテクチャ

Auth.js v5 の Provider 抽象化レイヤーにより、統一されたインターフェースで複数の認証ソースと統合できます:

100%
graph TB
    A[リクエスト認証] --> B{NextAuth ルートハンドラ}
    B --> C[Credentials<br/>Email+パスワード]
    B --> D[OAuth<br/>GitHub / Google]
    B --> E[その他の Provider<br/>Auth0 / Azure AD]
    C --> F[JWT コールバック<br/>token + session]
    D --> F
    E --> F
    F --> G[セッションをクライアントに返す]
    G --> H[ミドルウェア検証]
    H --> I[保護されたページ]
    H --> J[公開ページ]

    style B fill:#cce5ff
    style F fill:#d4edda
プロバイダータイプ 実装の複雑さ ユーザー体験 適用シナリオ
Credentials 中 (カスタムログインページが必要) 標準フォーム 独自アカウントシステム
GitHub OAuth ワンクリックログイン 開発者ツール
Google OAuth ワンクリックログイン 一般ユーザー向け
OIDC / SAML エンタープライズ SSO 企業イントラネット

(2) インストールと初期化

BASH
npm install next-auth@beta
npx auth secret    # AUTH_SECRET を生成
TS
// auth.ts — 集中認証設定
import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
import Google from 'next-auth/providers/google'

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    GitHub({ clientId: process.env.GITHUB_ID!, clientSecret: process.env.GITHUB_SECRET! }),
    Google({ clientId: process.env.GOOGLE_ID!, clientSecret: process.env.GOOGLE_SECRET! })
  ]
})

▶ サンプル: Credentials プロバイダーの完全な設定

💻 出力:

TEXT 📖 参照専用
TypeScript コードが正常に実行されました。
TS
// auth.ts — Credentials + OAuth 混合
import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import GitHub from 'next-auth/providers/github'

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Credentials({
      name: 'credentials',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' }
      },
      async authorize(credentials) {
        const { email, password } = credentials as { email: string; password: string }
        // 実際のプロジェクトではデータベースにクエリ
        const user = { id: '1', name: 'Alice', email, role: 'admin' }
        if (email === 'admin@taskflow.io' && password === 'admin123') return user
        return null
      }
    }),
    GitHub
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) token.role = (user as any).role
      return token
    },
    async session({ session, token }) {
      session.user.role = token.role as string
      return session
    }
  }
})
💻 出力:

TEXT 📖 参照専用
TypeScript 設定が正常に読み込まれました。
💡 ヒント: authorize 関数が null を返すと、ログイン失敗を示し、Auth.js は自動的に 401 レスポンスを返します。



4. セッション管理: JWT vs データベース

(1) 2つの戦略の比較

Auth.js v5 は2つのセッション保存戦略をサポートします:

指標 JWT (デフォルト) データベース
保存場所 Cookie 内の暗号化 JWT データベース Session テーブル
クエリオーバーヘッド ゼロ (DB クエリなし) リクエストごとに1回の DB クエリ
即時無効化 JWT 有効期限に依存 即時に無効化可能
スケーラビリティ データベース不要 Prisma などの ORM が必要
適した規模 中小規模アプリケーション 大規模エンタープライズアプリケーション
100%
graph LR
    subgraph JWT パターン
        A1[ログイン] --> B1[JWT を生成<br/>ユーザー + ロール付き]
        B1 --> C1[Cookie に書き込み]
        C1 --> D1[リクエスト → ミドルウェア<br/>JWT 復号 → 検証]
    end
    subgraph データベースパターン
        A2[ログイン] --> B2[セッションを作成<br/>DB に書き込み]
        B2 --> C2[セッション ID → Cookie]
        C2 --> D2[リクエスト → ミドルウェア<br/>DB 検索 → 検証]
    end

    style A1 fill:#d4edda
    style A2 fill:#cce5ff

(2) データベースセッション設定

TS
// auth.ts — データベースセッション + Prisma
import NextAuth from 'next-auth'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { prisma } from '@/lib/prisma'

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  session: { strategy: 'database' },
  providers: [GitHub, Google]
})

▶ サンプル: セッションの取得とユーザー情報の表示

💻 出力:

TEXT 📖 参照専用
TypeScript モジュールが正常に実行されました。
TSX
// app/dashboard/page.tsx — サーバーサイドでセッションを取得
import { auth } from '@/auth'

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

  if (!session?.user) return <p>まずログインしてください</p>

  return (
    <div>
      <h1>おかえりなさい、{session.user.name} さん</h1>
      <p>メール: {session.user.email}</p>
      <p>ロール: {session.user.role}</p>
      <img src={session.user.image!} alt="avatar" width={48} height={48} />
    </div>
  )
}
💻 出力:

TEXT 📖 参照専用
<h1>おかえりなさい、Alice さん</h1>
<p>メール: alice@taskflow.io</p>
<p>ロール: admin</p>


5. ミドルウェアルート保護

(1) Matcher 設定モード

middleware.ts はページに到達する前にリクエストを傍受し、セッションの有効性をチェックします:

100%
graph TB
    A[ユーザーが /dashboard/* をリクエスト] --> B{middleware.ts}
    B -->|有効なセッション| C[通過 → page.tsx]
    B -->|セッションなし| D[/login にリダイレクト]
    D --> E{公開パス?}
    E -->|/api/auth/*| F[通過]
    E -->|/_next/*| F
    E -->|/favicon.ico| F

    style B fill:#fff3cd
    style C fill:#d4edda
    style D fill:#f8d7da
Matcher パターン マッチングパス 説明
/dashboard/:path* /dashboard/* ダッシュボードを保護
/api/projects/:path* /api/projects/* API を保護
`/((?!auth _next favicon).*)`

(2) 完全なミドルウェア実装

TS
// middleware.ts
import { auth } from '@/auth'
import { NextResponse } from 'next/server'

export default auth((req) => {
  const { pathname } = req.nextUrl
  const isLoggedIn = !!req.auth
  const isPublicPath = pathname.startsWith('/login') ||
    pathname.startsWith('/register') ||
    pathname.startsWith('/api/auth')

  if (!isLoggedIn && !isPublicPath) {
    return NextResponse.redirect(new URL('/login', req.url))
  }

  // RBAC: 非 admin が /admin にアクセスするのを防止
  if (pathname.startsWith('/admin') && req.auth?.user?.role !== 'admin') {
    return NextResponse.redirect(new URL('/dashboard', req.url))
  }

  return NextResponse.next()
})

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|images).*)']
}

▶ サンプル: API ルートに認証保護を追加

💻 出力:

TEXT 📖 参照専用
ミドルウェアがリクエストを傍受し、条件に基づいてリダイレクトします。
TS
// app/api/projects/route.ts — 保護された API
import { getToken } from 'next-auth/jwt'
import { NextRequest, NextResponse } from 'next/server'

export async function GET(req: NextRequest) {
  const token = await getToken({ req })

  if (!token) {
    return NextResponse.json({ error: 'ログインしていません' }, { status: 401 })
  }

  // token.role は JWT コールバックから取得
  if (token.role !== 'admin' && token.role !== 'editor') {
    return NextResponse.json({ error: '権限が不足しています' }, { status: 403 })
  }

  return NextResponse.json({ projects: [{ id: 1, name: 'TaskFlow' }] })
}
💻 出力:

TEXT 📖 参照専用
GET app/api/projects/route.ts → 認証トークンを検証し、保護されたデータを JSON で返します。
🔥 よくあるミス: getToken() には AUTH_SECRET 環境変数が設定されている必要があります。設定されていない場合は null を返します。



6. Clerk サードパーティ認証

(1) Clerk の機能比較

機能 Auth.js Clerk
導入の複雑さ 中 (Provider 設定が必要) 低 (npm + 環境変数)
UI コンポーネント カスタムログインページ <SignIn /> / <SignUp /> すぐに使用可能
多要素認証 手動統合が必要 組み込みサポート
無料枠の制限 なし 5,000 MAU (無料)
カスタム UI 完全に自由 多くの制限あり

(2) Clerk の統合手順

BASH
npm install @clerk/nextjs
# .env.local に以下を追加:
# NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=...
# CLERK_SECRET_KEY=...
TSX
// app/layout.tsx — ClerkProvider でルートレイアウトをラップ
import { ClerkProvider } from '@clerk/nextjs'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <ClerkProvider>
      <html lang="ja">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  )
}

▶ サンプル: Clerk ログインページ + 保護されたページ

💻 出力:

TEXT 📖 参照専用
RootLayout コンポーネントの UI をレンダリングします。
TSX
// app/page.tsx — ログインページ
import { SignIn, SignedIn, SignedOut, UserButton } from '@clerk/nextjs'

export default function HomePage() {
  return (
    <div>
      <SignedOut>
        <SignIn routing="hash" />
      </SignedOut>
      <SignedIn>
        <div>
          <UserButton afterSignOutUrl="/" />
          <h1>TaskFlow へようこそ</h1>
        </div>
      </SignedIn>
    </div>
  )
}
💻 出力:

TEXT 📖 参照専用
レンダリング: Welcome to TaskFlow
表示テキスト: Welcome to TaskFlow
TSX
// app/dashboard/page.tsx — Clerk auth() 保護
import { auth } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'

export default async function DashboardPage() {
  const { userId } = auth()

  if (!userId) redirect('/sign-in')

  return <div>Dashboard — UserID: {userId}</div>
}
💡 ヒント: Clerk の auth() はサーバーサイド関数です。サーバーコンポーネントで使用する際に 'use client' は不要です。

(3) Clerk ミドルウェア

TS
// middleware.ts
import { clerkMiddleware } from '@clerk/nextjs/server'

export default clerkMiddleware()

export const config = {
  matcher: ['/((?!_next|sign-in|sign-up|favicon.ico).*)']
}


7. RBAC ロールベースアクセス制御

(1) ロールモデル設計

100%
graph TB
    A[ユーザー] --> B{ロール}
    B --> C[admin<br/>全アクセス]
    B --> D[editor<br/>プロジェクトの読み書き]
    B --> E[viewer<br/>読み取り専用]

    C --> F[アイテムの作成/削除]
    C --> G[チーム管理]
    C --> H[設定変更]
    D --> I[タスク編集]
    D --> J[コメント追加]
    E --> K[ダッシュボード閲覧]
    E --> L[ドキュメント読み取り]

    style C fill:#d4edda
    style D fill:#cce5ff
    style E fill:#f8d7da
ロール 権限レベル アクセス可能ページ
admin 100 すべて (/admin を含む)
editor 50 /dashboard/projects (編集可能)
viewer 20 /dashboard (読み取り専用)

▶ サンプル: RBAC 権限チェックコンポーネント

💻 出力:

TEXT 📖 参照専用
図: User; admin 全アクセス; editor プロジェクトの読み書き; viewer 読み取り専用; アイテムの作成/削除; チーム管理。
TSX
// components/PermissionGuard.tsx
import { auth } from '@/auth'
import { redirect } from 'next/navigation'

type Role = 'admin' | 'editor' | 'viewer'

const roleHierarchy: Record<Role, number> = { admin: 100, editor: 50, viewer: 20 }

export async function PermissionGuard({
  children,
  minRole
}: {
  children: React.ReactNode
  minRole: Role
}) {
  const session = await auth()
  const userRole = (session?.user?.role as Role) || 'viewer'

  if (roleHierarchy[userRole] < roleHierarchy[minRole]) {
    redirect('/dashboard')
  }

  return <>{children}</>
}
💻 出力:

TEXT 📖 参照専用
PermissionGuard コンポーネントの UI をレンダリングします。
TSX
// app/admin/page.tsx — PermissionGuard の使用
import { PermissionGuard } from '@/components/PermissionGuard'

export default function AdminPage() {
  return (
    <PermissionGuard minRole="admin">
      <h1>管理コンソール</h1>
      <p>このページは admin のみ閲覧可能です。</p>
    </PermissionGuard>
  )
}


8. 完全な例: マルチプロバイダー認証 + RBAC の総合実装

TSX
// app/dashboard/layout.tsx — 保護されたレイアウト + RBAC
import { auth } from '@/auth'
import { redirect } from 'next/navigation'
import { PermissionGuard } from '@/components/PermissionGuard'

export default async function DashboardLayout({
  children,
  analytics,
  team
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
}) {
  const session = await auth()

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

  const user = session.user!

  return (
    <div>
      <header>
        <h1>TaskFlow</h1>
        <p>ようこそ、{user.name} さん ({user.role})</p>
        <nav>
          <a href="/dashboard">概要</a>
          {user.role === 'admin' && <a href="/admin">管理</a>}
          <a href="/api/auth/signout">ログアウト</a>
        </nav>
      </header>
      <div style={{ display: 'flex', gap: '2rem' }}>
        <main>{children}</main>
        <aside>
          {analytics}
          <PermissionGuard minRole="editor">{team}</PermissionGuard>
        </aside>
      </div>
    </div>
  )
}
TS
// app/api/projects/[id]/route.ts — 完全な API 保護
import { getToken } from 'next-auth/jwt'
import { NextRequest, NextResponse } from 'next/server'

const roles = { admin: 100, editor: 50, viewer: 20 }

export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  const token = await getToken({ req })
  const { id } = await params

  if (!token) return NextResponse.json({ error: 'unauthorized' }, { status: 401 })
  if (roles[token.role as keyof typeof roles] < 50) {
    return NextResponse.json({ error: 'forbidden' }, { status: 403 })
  }

  // 実際にアイテムを削除
  return NextResponse.json({ id, deleted: true })
}
💻 出力 (ログインなしでの DELETE リクエスト):

JSON
{ "error": "unauthorized" }
💻 出力 (viewer ロールでの DELETE リクエスト):

JSON
{ "error": "forbidden" }

❓ よくある質問

Q Auth.js v4 (NextAuth) と v5 の違いは何ですか?
A v5 は App Router のルートハンドラ (app/api/auth/[...nextauth]/route.ts) を使用し、サーバーコンポーネントで auth() 経由でセッションを直接取得できるようになり、getSession()SessionProvider が不要になりました。
Q JWT セッションとデータベースセッションのどちらを選ぶべきですか?
A 即時セッション無効化が不要な小規模アプリケーションには JWT (DB クエリゼロ) を選択します。即時のユーザーログアウトやマルチデバイス管理が必要なエンタープライズアプリケーションにはデータベースセッションを選択します。JWT モードでは、セッション無効化は JWT の有効期限 (デフォルト30日) まで待つ必要があります。
Q いつ Clerk を選び、いつ Auth.js を選ぶべきですか?
A 迅速にローンチしたい、UI を自作したくない場合 → Clerk (30分で統合)。完全にカスタマイズ可能な UI が必要、独自アカウントシステムを構築したい場合 → Auth.js。Clerk の無料枠は 5,000 MAU までで、それを超えると €25/月です。
Q ミドルウェアでの auth()getToken() の違いは何ですか?
A auth() は Auth.js v5 のラッパー関数で、完全な Session オブジェクトを返します。getToken()next-auth/jwt から派生し、JWT トークンのみを解析するため、より優れたパフォーマンスを提供します。ミドルウェアでは auth() のシンプルさを推奨し、API ルートでは getToken() の軽量さを推奨します。
Q 静的エクスポートされたページを保護するには?
A output: 'export' で構築された静的サイトはミドルウェアを使用できません (Node.js ランタイムが必要)。この場合、クライアントコンポーネントで useSession() フックを使用してログイン状態をチェックするか、Clerk の <SignedIn> / <SignedOut> 条件付きレンダリングを使用します。

📖 まとめ


📝 練習問題

  1. 基本問題 (⭐): 新しい Next.js プロジェクトを作成し、Auth.js v5 の GitHub Provider を統合して、/dashboard ページにユーザーのアバターとメールアドレスを表示します。

  2. 応用問題 (⭐⭐): ミドルウェアに RBAC ロジックを追加します: admin/admin/* にアクセス可能、editor/projects/* の編集画面にアクセス可能。その他のロールがこれらのリソースにアクセスしようとした場合は 403 ページを返します。

  3. 発展問題 (⭐⭐⭐): Auth.js (Credentials Provider) と Prisma を組み合わせます: ユーザーがログインページでメールとパスワードを入力 → authorize がデータベースにクエリして検証 → roleorgId を JWT に書き込み → ミドルウェアが orgId に基づいて対応する組織のワークスペースにルーティングします。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%