Clerk Next.js
認証ライブラリ
Clerk Next.js
概要
Clerk Next.js は、Next.jsアプリケーション専用に設計されたモダンな認証・ユーザー管理ライブラリです。2025年現在、App Router と Pages Router の両方をフルサポートし、開発者にとって最も使いやすい認証ソリューションとして広く採用されています。JWT、OAuth 2.0、セッション管理、多要素認証(MFA)などの包括的な認証機能を提供し、数行のコードで完全なユーザー認証システムを実装できます。Reactのフックパターンと完全に統合されており、useAuth、useUser、useClerk などの直感的なAPIを提供します。
詳細
Clerk Next.js は、現代的なWebアプリケーションに求められる認証機能を網羅的に提供するライブラリです。主な特徴:
- Next.js最適化: App Router、Pages Router、API Routes、ミドルウェアとの完全統合
- JWT統合: 自動的なJWTトークン管理とセッションクレーム処理
- OAuth プロバイダー: Google、GitHub、Discord、Apple、Microsoft など20+の外部認証対応
- セッション管理: サーバーサイドとクライアントサイドでの統一的なセッション処理
- リアクティブフック:
useAuth、useUser、useClerkによる宣言的な認証状態管理 - 保護ルート: ミドルウェアベースの自動ルート保護とリダイレクト機能
- カスタマイズ可能: 認証フロー、UI コンポーネント、リダイレクト先の完全カスタマイズ
メリット・デメリット
メリット
- Next.js に特化した最適化により、シームレスな統合と高パフォーマンス
- 数行のコードで完全な認証システムを実装、開発速度の大幅向上
- JWT、OAuth、MFA、RBAC などエンタープライズレベルのセキュリティ機能
- React hooks パターンによる直感的で宣言的なAPI設計
- App Router と Pages Router の両方で一貫した開発体験
- 豊富なドキュメントとアクティブなコミュニティサポート
デメリット
- Next.js 以外のフレームワークでは使用不可(React、Vue、Angular等への移植不可)
- 外部サービス依存のため、Clerk のサービス停止時は影響を受ける
- 高度なカスタマイズには Clerk のエコシステム内での制約
- 大規模アプリケーションでは利用料金が高額になる可能性
- 自社認証システムからの移行時にベンダーロックインのリスク
参考ページ
書き方の例
基本セットアップとインストール
# Clerk Next.js パッケージのインストール
npm install @clerk/nextjs
# 環境変数の設定 (.env.local)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
# オプション: リダイレクトURL設定
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
App Router での基本認証設定
// app/layout.tsx - ClerkProviderでアプリ全体をラップ
import { ClerkProvider } from '@clerk/nextjs'
import './globals.css'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<ClerkProvider>
<html lang="ja">
<body>{children}</body>
</html>
</ClerkProvider>
)
}
// middleware.ts - ルート保護設定
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/admin(.*)',
'/profile(.*)'
])
const isPublicRoute = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
'/api/public(.*)'
])
export default clerkMiddleware((auth, request) => {
// パブリックルートは認証をスキップ
if (isPublicRoute(request)) {
return NextResponse.next()
}
// 保護されたルートでは認証を必須とする
if (isProtectedRoute(request)) {
auth().protect()
}
return NextResponse.next()
})
export const config = {
matcher: ['/((?!.*\..*|_next).*)', '/', '/(api|trpc)(.*)']
}
サインイン・サインアップページの実装
// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs'
export default function SignInPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<SignIn
appearance={{
elements: {
formButtonPrimary: 'bg-blue-600 hover:bg-blue-700 text-white',
card: 'shadow-lg'
}
}}
routing="hash"
signUpUrl="/sign-up"
redirectUrl="/dashboard"
/>
</div>
)
}
// app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from '@clerk/nextjs'
export default function SignUpPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<SignUp
appearance={{
elements: {
formButtonPrimary: 'bg-green-600 hover:bg-green-700 text-white',
card: 'shadow-lg'
}
}}
routing="hash"
signInUrl="/sign-in"
redirectUrl="/dashboard"
/>
</div>
)
}
認証状態に基づく条件レンダリング
// app/page.tsx - ホームページでの認証状態表示
import {
SignedIn,
SignedOut,
SignInButton,
UserButton,
useUser
} from '@clerk/nextjs'
import Link from 'next/link'
export default function HomePage() {
return (
<div className="container mx-auto px-4 py-8">
<nav className="flex justify-between items-center mb-8">
<h1 className="text-2xl font-bold">My App</h1>
<div className="flex items-center space-x-4">
<SignedOut>
<SignInButton mode="modal">
<button className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
サインイン
</button>
</SignInButton>
<Link href="/sign-up">
<button className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700">
サインアップ
</button>
</Link>
</SignedOut>
<SignedIn>
<Link href="/dashboard">
<button className="text-blue-600 hover:text-blue-800">
ダッシュボード
</button>
</Link>
<UserButton
afterSignOutUrl="/"
appearance={{
elements: {
avatarBox: 'w-10 h-10'
}
}}
/>
</SignedIn>
</div>
</nav>
<main>
<SignedOut>
<div className="text-center">
<h2 className="text-4xl font-bold mb-4">Welcome to My App</h2>
<p className="text-xl text-gray-600 mb-8">
サインインして始めましょう
</p>
</div>
</SignedOut>
<SignedIn>
<WelcomeMessage />
</SignedIn>
</main>
</div>
)
}
function WelcomeMessage() {
const { user } = useUser()
return (
<div className="text-center">
<h2 className="text-4xl font-bold mb-4">
こんにちは、{user?.firstName || user?.username}さん!
</h2>
<p className="text-xl text-gray-600">
認証が完了しました。アプリをお楽しみください。
</p>
</div>
)
}
保護されたダッシュボードページ
// app/dashboard/page.tsx - 認証必須ページ
import { auth, currentUser } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
import UserProfile from '@/components/UserProfile'
import DashboardStats from '@/components/DashboardStats'
export default async function DashboardPage() {
// サーバーサイドでの認証チェック
const { userId } = await auth()
if (!userId) {
redirect('/sign-in')
}
// 現在のユーザー情報を取得
const user = await currentUser()
if (!user) {
redirect('/sign-in')
}
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold mb-2">ダッシュボード</h1>
<p className="text-gray-600">
ようこそ、{user.firstName || user.username}さん
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<UserProfile user={user} />
<DashboardStats userId={userId} />
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-semibold mb-4">アカウント情報</h3>
<div className="space-y-2">
<p><strong>メール:</strong> {user.primaryEmailAddress?.emailAddress}</p>
<p><strong>サインアップ日:</strong> {user.createdAt?.toLocaleDateString('ja-JP')}</p>
<p><strong>最終ログイン:</strong> {user.lastSignInAt?.toLocaleDateString('ja-JP')}</p>
</div>
</div>
</div>
</div>
)
}
API Routes での認証とJWT処理
// app/api/profile/route.ts - 保護されたAPI
import { auth, currentUser } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
export async function GET() {
try {
// 認証状態とJWTクレームを取得
const { userId, sessionClaims, getToken } = await auth()
if (!userId) {
return new NextResponse(
JSON.stringify({ error: '認証が必要です' }),
{ status: 401 }
)
}
// JWTトークンを取得(外部API呼び出し用)
const token = await getToken()
// 現在のユーザー情報を取得
const user = await currentUser()
// セッションクレームの詳細情報
const userMetadata = {
userId: userId,
sessionId: sessionClaims?.sid,
email: user?.primaryEmailAddress?.emailAddress,
roles: sessionClaims?.metadata?.roles || [],
permissions: sessionClaims?.metadata?.permissions || []
}
return NextResponse.json({
success: true,
user: userMetadata,
tokenAvailable: !!token
})
} catch (error) {
console.error('API error:', error)
return new NextResponse(
JSON.stringify({ error: 'サーバーエラー' }),
{ status: 500 }
)
}
}
export async function PUT(request: Request) {
try {
const { userId } = await auth()
if (!userId) {
return new NextResponse(
JSON.stringify({ error: '認証が必要です' }),
{ status: 401 }
)
}
const updateData = await request.json()
// プロファイル更新ロジック
// userService.updateProfile(userId, updateData)
return NextResponse.json({
success: true,
message: 'プロファイルが更新されました'
})
} catch (error) {
return new NextResponse(
JSON.stringify({ error: 'プロファイル更新に失敗しました' }),
{ status: 500 }
)
}
}
外部API認証とトークン管理
// components/ExternalDataFetcher.tsx - 外部APIとの連携
'use client'
import { useAuth } from '@clerk/nextjs'
import { useState, useEffect } from 'react'
interface ExternalApiData {
id: string
name: string
data: any
}
export default function ExternalDataFetcher() {
const { getToken, isLoaded, isSignedIn } = useAuth()
const [data, setData] = useState<ExternalApiData[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const fetchExternalData = async () => {
try {
setLoading(true)
setError(null)
// ClerkからJWTトークンを取得
const token = await getToken()
if (!token) {
throw new Error('認証トークンが取得できません')
}
// 外部APIにJWTトークンを使ってリクエスト
const response = await fetch('/api/external-data', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
})
if (!response.ok) {
throw new Error(`API Error: ${response.status}`)
}
const result = await response.json()
setData(result.data)
} catch (err) {
setError(err instanceof Error ? err.message : '不明なエラー')
console.error('External API fetch error:', err)
} finally {
setLoading(false)
}
}
// マウント時に自動取得
useEffect(() => {
if (isLoaded && isSignedIn) {
fetchExternalData()
}
}, [isLoaded, isSignedIn])
if (!isLoaded) {
return <div className="animate-pulse">読み込み中...</div>
}
if (!isSignedIn) {
return <div className="text-red-600">認証が必要です</div>
}
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">外部データ</h3>
<button
onClick={fetchExternalData}
disabled={loading}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
>
{loading ? '更新中...' : '更新'}
</button>
</div>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
エラー: {error}
</div>
)}
<div className="grid gap-4">
{data.map((item) => (
<div key={item.id} className="bg-white p-4 rounded-lg shadow">
<h4 className="font-medium">{item.name}</h4>
<pre className="mt-2 text-sm text-gray-600">
{JSON.stringify(item.data, null, 2)}
</pre>
</div>
))}
</div>
{data.length === 0 && !loading && !error && (
<div className="text-center text-gray-500 py-8">
データがありません
</div>
)}
</div>
)
}
高度な認証フックとカスタマイズ
// hooks/useAuthStatus.ts - カスタム認証フック
import { useAuth, useUser, useClerk } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'
import { useCallback } from 'react'
export function useAuthStatus() {
const {
isLoaded: authLoaded,
isSignedIn,
userId,
sessionId,
getToken
} = useAuth()
const {
isLoaded: userLoaded,
user
} = useUser()
const { signOut, openSignIn } = useClerk()
const router = useRouter()
// カスタムサインアウト処理
const handleSignOut = useCallback(async () => {
try {
await signOut()
router.push('/')
} catch (error) {
console.error('Sign out error:', error)
}
}, [signOut, router])
// トークン取得とリフレッシュ
const getAuthToken = useCallback(async () => {
try {
return await getToken()
} catch (error) {
console.error('Token fetch error:', error)
return null
}
}, [getToken])
// 認証必須アクション
const requireAuth = useCallback((action: () => void) => {
if (!isSignedIn) {
openSignIn()
return
}
action()
}, [isSignedIn, openSignIn])
return {
// 基本状態
isLoaded: authLoaded && userLoaded,
isSignedIn,
user,
userId,
sessionId,
// 拡張機能
getAuthToken,
handleSignOut,
requireAuth,
// ユーザー情報
userEmail: user?.primaryEmailAddress?.emailAddress,
userName: user?.firstName || user?.username || 'Unknown',
userRoles: user?.publicMetadata?.roles as string[] || [],
// 状態チェック
isAdmin: (user?.publicMetadata?.roles as string[])?.includes('admin') || false,
isVerified: user?.primaryEmailAddress?.verification?.status === 'verified'
}
}
// components/AuthGuard.tsx - 認証ガードコンポーネント
import { ReactNode } from 'react'
import { useAuthStatus } from '@/hooks/useAuthStatus'
import { SignInButton } from '@clerk/nextjs'
interface AuthGuardProps {
children: ReactNode
fallback?: ReactNode
requireAdmin?: boolean
requireVerified?: boolean
}
export default function AuthGuard({
children,
fallback,
requireAdmin = false,
requireVerified = false
}: AuthGuardProps) {
const {
isLoaded,
isSignedIn,
isAdmin,
isVerified
} = useAuthStatus()
// ローディング中
if (!isLoaded) {
return (
<div className="flex justify-center items-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
)
}
// 未認証
if (!isSignedIn) {
return fallback || (
<div className="flex flex-col items-center justify-center min-h-screen space-y-4">
<h2 className="text-2xl font-bold">認証が必要です</h2>
<p className="text-gray-600">このページにアクセスするにはサインインしてください</p>
<SignInButton mode="modal">
<button className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700">
サインイン
</button>
</SignInButton>
</div>
)
}
// 管理者権限チェック
if (requireAdmin && !isAdmin) {
return (
<div className="flex flex-col items-center justify-center min-h-screen space-y-4">
<h2 className="text-2xl font-bold text-red-600">アクセス拒否</h2>
<p className="text-gray-600">このページにアクセスする権限がありません</p>
</div>
)
}
// メール認証チェック
if (requireVerified && !isVerified) {
return (
<div className="flex flex-col items-center justify-center min-h-screen space-y-4">
<h2 className="text-2xl font-bold text-yellow-600">メール認証が必要</h2>
<p className="text-gray-600">このページにアクセスするにはメールアドレスの認証が必要です</p>
</div>
)
}
return <>{children}</>
}