Next.js: 認証 & 認可
最終更新:2026-08-26
フルスタックアプリケーションに認証を追加することは、オフィスビルに入退室管理システムを設置するようなものです — 誰が入室できるか、どのフロアにアクセスできるかを正確に制御する必要があります。
1. 学習目標
- Auth.js v5 (NextAuth) の統合: Credentials / GitHub / Google マルチプロバイダー
- JWT とデータベースセッション戦略の選択と設定
- ミドルウェアルート保護と
matcherによる共通パスの除外 - Clerk サードパーティ認証統合 (
<SignIn />/<SignUp />/auth()ヘルパー) - RBAC: ロールベースアクセス制御 (admin / editor / viewer)
- 保護された API ルートの
getToken()認証
2. あるフルスタック開発者の実話
(1) 課題: ローンチ直前に認証がないことに気づいた
Alice は15人の SaaS スタートアップで働いています。彼女は Next.js 16 を使用してフル機能のチームコラボレーションプラットフォーム「TaskFlow」を開発しました。しかし、ローンチ前のセキュリティ監査で、テクニカルリードの Bob が指摘しました:
「君の
/dashboardページは誰でもアクセスでき、API エンドポイントにはトークン認証がなく、ユーザーデータが平文で露出している。」
監査レポートには以下の問題がリストされています:
| 問題 | 範囲 | リスクレベル |
|---|---|---|
| ログインページなし | すべてのルート | 🔴 高 |
| API ルート: 認証なし | /api/projects/* |
🔴 高 |
| ロールの区別なし | すべてのユーザーが管理者パネルを閲覧可能 | 🟡 中 |
| セッションが期限切れしない | 一度ログインすると永続的にログイン状態 | 🔴 高 |
(2) Auth.js + Clerk による解決策
Auth.js v5 を使用して標準的な認証フローを実装し、Clerk をゼロ設定の代替として使用します。
// 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 抽象化レイヤーにより、統一されたインターフェースで複数の認証ソースと統合できます:
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) インストールと初期化
npm install next-auth@beta
npx auth secret # AUTH_SECRET を生成
// 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 プロバイダーの完全な設定
TypeScript コードが正常に実行されました。
// 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
}
}
})
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 が必要 |
| 適した規模 | 中小規模アプリケーション | 大規模エンタープライズアプリケーション |
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) データベースセッション設定
// 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]
})
▶ サンプル: セッションの取得とユーザー情報の表示
TypeScript モジュールが正常に実行されました。
// 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>
)
}
<h1>おかえりなさい、Alice さん</h1>
<p>メール: alice@taskflow.io</p>
<p>ロール: admin</p>
5. ミドルウェアルート保護
(1) Matcher 設定モード
middleware.ts はページに到達する前にリクエストを傍受し、セッションの有効性をチェックします:
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) 完全なミドルウェア実装
// 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 ルートに認証保護を追加
ミドルウェアがリクエストを傍受し、条件に基づいてリダイレクトします。
// 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' }] })
}
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 の統合手順
npm install @clerk/nextjs
# .env.local に以下を追加:
# NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=...
# CLERK_SECRET_KEY=...
// 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 ログインページ + 保護されたページ
RootLayout コンポーネントの UI をレンダリングします。
// 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>
)
}
レンダリング: Welcome to TaskFlow
表示テキスト: Welcome to TaskFlow
// 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>
}
auth() はサーバーサイド関数です。サーバーコンポーネントで使用する際に 'use client' は不要です。
(3) Clerk ミドルウェア
// 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) ロールモデル設計
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 権限チェックコンポーネント
図: User; admin 全アクセス; editor プロジェクトの読み書き; viewer 読み取り専用; アイテムの作成/削除; チーム管理。
// 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}</>
}
PermissionGuard コンポーネントの UI をレンダリングします。
// 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 の総合実装
// 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>
)
}
// 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 })
}
{ "error": "unauthorized" }
{ "error": "forbidden" }
❓ よくある質問
app/api/auth/[...nextauth]/route.ts) を使用し、サーバーコンポーネントで auth() 経由でセッションを直接取得できるようになり、getSession() や SessionProvider が不要になりました。auth() と getToken() の違いは何ですか?auth() は Auth.js v5 のラッパー関数で、完全な Session オブジェクトを返します。getToken() は next-auth/jwt から派生し、JWT トークンのみを解析するため、より優れたパフォーマンスを提供します。ミドルウェアでは auth() のシンプルさを推奨し、API ルートでは getToken() の軽量さを推奨します。output: 'export' で構築された静的サイトはミドルウェアを使用できません (Node.js ランタイムが必要)。この場合、クライアントコンポーネントで useSession() フックを使用してログイン状態をチェックするか、Clerk の <SignedIn> / <SignedOut> 条件付きレンダリングを使用します。📖 まとめ
- Auth.js v5 はルートハンドラパターンを使用します。
auth()はサーバーコンポーネント内で直接セッションを取得できます - Provider は Credentials、GitHub、Google などの複数ソースをサポートし、コールバックメカニズムでカスタムデータの注入が可能です
- JWT セッション (DB クエリゼロ) は小規模アプリケーションに適し、データベースセッション (即時無効化対応) はエンタープライズに適します
- ミドルウェアは
matcher設定でパスを保護し、逆マッチングで公開リソースを除外します - Clerk はすぐに使える UI コンポーネントを提供し、迅速なプロトタイピングに最適。無料枠は 5,000 MAU まで
- RBAC はロール値の比較で権限制御を実装し、PermissionGuard コンポーネントと組み合わせて宣言的に使用します
📝 練習問題
-
基本問題 (⭐): 新しい Next.js プロジェクトを作成し、Auth.js v5 の GitHub Provider を統合して、
/dashboardページにユーザーのアバターとメールアドレスを表示します。 -
応用問題 (⭐⭐): ミドルウェアに RBAC ロジックを追加します:
adminは/admin/*にアクセス可能、editorは/projects/*の編集画面にアクセス可能。その他のロールがこれらのリソースにアクセスしようとした場合は 403 ページを返します。 -
発展問題 (⭐⭐⭐): Auth.js (Credentials Provider) と Prisma を組み合わせます: ユーザーがログインページでメールとパスワードを入力 →
authorizeがデータベースにクエリして検証 →roleとorgIdを JWT に書き込み → ミドルウェアがorgIdに基づいて対応する組織のワークスペースにルーティングします。