Clerkで始める次世代認証・ユーザー管理
Clerkとは
Clerk は、モダンなWebアプリケーション向けの認証・ユーザー管理プラットフォームです。従来の認証ライブラリとは異なり、UI コンポーネントから認証フロー、セキュリティまでをトータルでサポートする、完全なソリューションを提供します。
Clerkの主な特徴
- 完全なUIコンポーネント: サインイン、サインアップ、ユーザープロフィールなど、すぐに使える美しいUIコンポーネント
- 多様な認証方法: Email/パスワード、ソーシャルログイン、パスワードレス、多要素認証(MFA)
- 開発者体験の最適化: TypeScript完全対応、フレームワーク別SDK、詳細なドキュメント
- エンタープライズグレードのセキュリティ: SAML SSO、組織管理、高度な権限制御
- 無料プランの充実: 月間10,000アクティブユーザーまで無料
なぜClerkを選ぶのか
従来の認証実装の課題
従来、認証機能を実装するには以下のような課題がありました:
- 複雑な実装: パスワードのハッシュ化、トークン管理、セッション管理など、セキュリティを考慮した実装は複雑
- UI/UXの構築: サインイン/サインアップフォーム、パスワードリセット、メール認証など、多くのUIを構築する必要がある
- 保守の負担: セキュリティアップデート、GDPR対応、監査ログなど、継続的な保守が必要
Clerkが解決すること
Clerk を使うことで、これらの課題を解決できます:
// たった数行で完全な認証機能を実装
import { SignIn } from "@clerk/nextjs";
export default function SignInPage() {
return <SignIn />;
}
セットアップと基本実装
1. プロジェクト作成とインストール
Next.js プロジェクトでの例:
# Next.js プロジェクト作成
npx create-next-app@latest my-clerk-app
cd my-clerk-app
# Clerk SDK インストール
npm install @clerk/nextjs
2. 環境変数の設定
Clerk ダッシュボード(https://clerk.com)でアプリケーションを作成し、APIキーを取得します。
.env.local:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxxxx
CLERK_SECRET_KEY=sk_test_xxxxx
# オプション: カスタム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=/onboarding
3. ClerkProviderのセットアップ
App Router(Next.js 13+)の場合:
app/layout.tsx:
import { ClerkProvider } from '@clerk/nextjs';
import { jaJP } from '@clerk/localizations';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<ClerkProvider localization={jaJP}>
<html lang="ja">
<body>{children}</body>
</html>
</ClerkProvider>
);
}
Pages Router(Next.js 12以前)の場合:
pages/_app.tsx:
import { ClerkProvider } from '@clerk/nextjs';
import type { AppProps } from 'next/app';
function MyApp({ Component, pageProps }: AppProps) {
return (
<ClerkProvider {...pageProps}>
<Component {...pageProps} />
</ClerkProvider>
);
}
export default MyApp;
4. サインイン/サインアップページの作成
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: {
rootBox: "mx-auto",
card: "shadow-lg",
},
}}
/>
</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: {
rootBox: "mx-auto",
card: "shadow-lg",
},
}}
/>
</div>
);
}
認証状態の取得と活用
クライアントコンポーネントでの認証状態取得
'use client';
import { useUser, useAuth } from '@clerk/nextjs';
export function UserProfile() {
const { user, isLoaded, isSignedIn } = useUser();
const { signOut } = useAuth();
if (!isLoaded) {
return <div>読み込み中...</div>;
}
if (!isSignedIn) {
return <div>ログインしてください</div>;
}
return (
<div>
<h2>ようこそ、{user.firstName}さん</h2>
<p>メール: {user.primaryEmailAddress?.emailAddress}</p>
<img
src={user.imageUrl}
alt={user.fullName || 'User'}
className="w-12 h-12 rounded-full"
/>
<button onClick={() => signOut()}>ログアウト</button>
</div>
);
}
サーバーコンポーネントでの認証状態取得
import { currentUser, auth } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';
export default async function DashboardPage() {
const user = await currentUser();
if (!user) {
redirect('/sign-in');
}
return (
<div>
<h1>ダッシュボード</h1>
<p>ようこそ、{user.firstName}さん</p>
</div>
);
}
ミドルウェアでの認証保護
middleware.ts:
import { authMiddleware } from "@clerk/nextjs";
export default authMiddleware({
// 公開ページ(認証不要)
publicRoutes: ["/", "/api/webhook"],
// 無視するルート(Clerk が処理しない)
ignoredRoutes: ["/api/public"],
});
export const config = {
matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
};
より細かい制御が必要な場合:
import { authMiddleware } from "@clerk/nextjs";
import { NextResponse } from "next/server";
export default authMiddleware({
publicRoutes: ["/"],
async afterAuth(auth, req) {
// 未認証ユーザーを保護されたルートからリダイレクト
if (!auth.userId && !auth.isPublicRoute) {
const signInUrl = new URL('/sign-in', req.url);
signInUrl.searchParams.set('redirect_url', req.url);
return NextResponse.redirect(signInUrl);
}
// 管理者のみアクセス可能なルート
if (req.nextUrl.pathname.startsWith('/admin')) {
const user = await auth.user;
const isAdmin = user?.publicMetadata?.role === 'admin';
if (!isAdmin) {
return NextResponse.redirect(new URL('/forbidden', req.url));
}
}
return NextResponse.next();
},
});
export const config = {
matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
};
API ルートでの認証
App Router での API ルート認証
app/api/protected/route.ts:
import { auth } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';
export async function GET() {
const { userId } = auth();
if (!userId) {
return NextResponse.json(
{ error: '認証が必要です' },
{ status: 401 }
);
}
// ユーザー情報を使った処理
const data = await fetchUserData(userId);
return NextResponse.json({ data });
}
async function fetchUserData(userId: string) {
// データベースからユーザーデータを取得
return { userId, message: 'Protected data' };
}
トークンの検証
外部APIとの連携時にトークンを検証:
import { auth } from '@clerk/nextjs/server';
export async function POST(req: Request) {
const { userId, getToken } = auth();
if (!userId) {
return new Response('Unauthorized', { status: 401 });
}
// カスタムクレームを含むJWTトークンを取得
const token = await getToken({ template: 'custom-template' });
// 外部APIにトークンを渡す
const response = await fetch('https://api.example.com/data', {
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
return Response.json(data);
}
ユーザーメタデータとカスタマイズ
ユーザーメタデータの種類
Clerk では3種類のメタデータを扱えます:
- publicMetadata: クライアント側で読み取り可能、サーバー側で書き込み可能
- privateMetadata: サーバー側でのみアクセス可能
- unsafeMetadata: クライアント側で読み書き可能(注意して使用)
メタデータの更新
import { clerkClient } from '@clerk/nextjs/server';
export async function updateUserRole(userId: string, role: string) {
await clerkClient.users.updateUser(userId, {
publicMetadata: {
role: role,
department: 'engineering',
},
});
}
export async function updatePrivateData(userId: string) {
await clerkClient.users.updateUser(userId, {
privateMetadata: {
stripeCustomerId: 'cus_xxxxx',
subscription: 'pro',
},
});
}
カスタムフィールドの追加
Clerk ダッシュボードでカスタムフィールドを定義した後、サインアップフォームで使用:
'use client';
import { useSignUp } from '@clerk/nextjs';
import { useState } from 'react';
export function CustomSignUpForm() {
const { signUp, setActive } = useSignUp();
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
emailAddress: '',
password: '',
company: '', // カスタムフィールド
});
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
try {
const result = await signUp?.create({
firstName: formData.firstName,
lastName: formData.lastName,
emailAddress: formData.emailAddress,
password: formData.password,
unsafeMetadata: {
company: formData.company,
},
});
if (result?.status === 'complete') {
await setActive({ session: result.createdSessionId });
}
} catch (error) {
console.error('サインアップエラー:', error);
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="名"
value={formData.firstName}
onChange={(e) => setFormData({ ...formData, firstName: e.target.value })}
/>
<input
type="text"
placeholder="姓"
value={formData.lastName}
onChange={(e) => setFormData({ ...formData, lastName: e.target.value })}
/>
<input
type="email"
placeholder="メールアドレス"
value={formData.emailAddress}
onChange={(e) => setFormData({ ...formData, emailAddress: e.target.value })}
/>
<input
type="password"
placeholder="パスワード"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
/>
<input
type="text"
placeholder="会社名"
value={formData.company}
onChange={(e) => setFormData({ ...formData, company: e.target.value })}
/>
<button type="submit">登録</button>
</form>
);
}
組織機能(Organizations)
組織の作成と管理
Clerk の組織機能を使うと、チーム/組織単位でのアクセス制御が可能になります:
'use client';
import { useOrganization, useOrganizationList } from '@clerk/nextjs';
export function OrganizationManager() {
const { organization, membership } = useOrganization();
const { createOrganization, setActive } = useOrganizationList();
async function handleCreateOrg() {
try {
const org = await createOrganization?.({ name: '新しい組織' });
await setActive?.({ organization: org?.id });
} catch (error) {
console.error('組織作成エラー:', error);
}
}
if (!organization) {
return (
<div>
<p>組織に所属していません</p>
<button onClick={handleCreateOrg}>組織を作成</button>
</div>
);
}
return (
<div>
<h2>{organization.name}</h2>
<p>あなたの役割: {membership?.role}</p>
<p>メンバー数: {organization.membersCount}</p>
</div>
);
}
組織メンバーの招待
import { clerkClient } from '@clerk/nextjs/server';
export async function inviteMember(
organizationId: string,
emailAddress: string,
role: 'admin' | 'member'
) {
const invitation = await clerkClient.organizations.createOrganizationInvitation({
organizationId,
emailAddress,
role,
});
return invitation;
}
組織スイッチャー
import { OrganizationSwitcher } from '@clerk/nextjs';
export function Header() {
return (
<header>
<OrganizationSwitcher
appearance={{
elements: {
rootBox: "flex items-center",
},
}}
/>
</header>
);
}
Webhookとイベント処理
Webhookのセットアップ
Clerk からのイベント通知を受け取るWebhookエンドポイントを作成:
app/api/webhooks/clerk/route.ts:
import { Webhook } from 'svix';
import { headers } from 'next/headers';
import { WebhookEvent } from '@clerk/nextjs/server';
export async function POST(req: Request) {
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
if (!WEBHOOK_SECRET) {
throw new Error('WEBHOOK_SECRET が設定されていません');
}
// ヘッダーを取得
const headerPayload = headers();
const svix_id = headerPayload.get('svix-id');
const svix_timestamp = headerPayload.get('svix-timestamp');
const svix_signature = headerPayload.get('svix-signature');
if (!svix_id || !svix_timestamp || !svix_signature) {
return new Response('Error: Missing svix headers', { status: 400 });
}
const payload = await req.json();
const body = JSON.stringify(payload);
// Webhookの検証
const wh = new Webhook(WEBHOOK_SECRET);
let evt: WebhookEvent;
try {
evt = wh.verify(body, {
'svix-id': svix_id,
'svix-timestamp': svix_timestamp,
'svix-signature': svix_signature,
}) as WebhookEvent;
} catch (err) {
console.error('Webhook検証エラー:', err);
return new Response('Error: Verification failed', { status: 400 });
}
// イベントタイプに応じた処理
const eventType = evt.type;
switch (eventType) {
case 'user.created':
await handleUserCreated(evt.data);
break;
case 'user.updated':
await handleUserUpdated(evt.data);
break;
case 'user.deleted':
await handleUserDeleted(evt.data);
break;
case 'organization.created':
await handleOrganizationCreated(evt.data);
break;
default:
console.log(`未処理のイベント: ${eventType}`);
}
return new Response('Webhook処理完了', { status: 200 });
}
async function handleUserCreated(data: any) {
// データベースにユーザーを作成
console.log('ユーザー作成:', data.id);
// await db.user.create({ ... });
}
async function handleUserUpdated(data: any) {
// データベースのユーザーを更新
console.log('ユーザー更新:', data.id);
// await db.user.update({ ... });
}
async function handleUserDeleted(data: any) {
// データベースからユーザーを削除
console.log('ユーザー削除:', data.id);
// await db.user.delete({ ... });
}
async function handleOrganizationCreated(data: any) {
// データベースに組織を作成
console.log('組織作成:', data.id);
// await db.organization.create({ ... });
}
まとめ
Clerk を使うことで、認証・ユーザー管理の実装を大幅に簡素化できます。主なメリット:
- 開発速度の向上: すぐに使えるUIコンポーネントとシンプルなAPI
- セキュリティの強化: エンタープライズグレードのセキュリティ機能
- 柔軟なカスタマイズ: メタデータ、Webhook、カスタムUIでニーズに対応
- スケーラビリティ: 組織機能で大規模なアプリケーションにも対応
Next.js、React、Remix、Vue、その他多くのフレームワークで利用可能なので、まずは公式ドキュメントを参照して、プロジェクトに導入してみてください。