authentication-authorization-clerk について
authentication-authorization-clerkは、Next.js App RouterアプリでClerkを使うためのセキュリティスキルです。サインイン、ルート保護、所有権やロールの確認、メタデータ、ユーザー同期の手順と例を扱います。
主な機能
- 01Next.js向けClerk認証パターン
- 02ミドルウェアによるルート保護
- 03リソース所有権とロールの確認
- 04ユーザーメタデータとカスタムクレーム
- 05Webhookによるユーザー同期
活用シーン
- 01ダッシュボードやAPIルートの保護
- 02ユーザー所有リソースへのアクセス確認
- 03ロールやサブスクリプションによる機能制限
- 04Clerkユーザーのアプリデータベースへの同期
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ harperaa/ secure-claude-skills --skill authentication-authorization-clerkインストール後、Claude Code で/authentication-authorization-clerkを入力して読み込まれたか確認します。
同梱ファイルsecure-vibe-coding-OS.code-workspace
詳細
Clerk による認証と認可
Clerk を使用する理由
認証に関する課題
安全な認証を一から構築するには、次の対応が必要です。
- パスワードのハッシュ化(適切なソルトを使用した bcrypt/Argon2)
- セッション管理(安全な Cookie、有効期限、更新)
- パスワードリセットフロー(安全なトークン生成、メール認証)
- アカウントロックアウト(ブルートフォース攻撃の防止)
- MFA のサポート(TOTP、SMS、認証アプリ)
- ソーシャルログイン(Google、GitHub などの OAuth フロー)
- ユーザーデータベースの同期
- 上記すべてに関するセキュリティのベストプラクティス
安全な実装にかかる時間: 経験豊富な開発者で 2~4 週間
AI を利用するバイブコーディングの場合: セキュリティ上の不備が生じるリスクが高くなります
実際に起きたカスタム認証の失敗事例
Ashley Madison の情報漏洩(2015年): パスワードのハッシュ化が不十分なカスタム認証により、3,200万件のアカウントが侵害されました。
Dropbox の情報漏洩(2012年): カスタム認証により、パスワードハッシュデータベースが盗まれ、6,800万件のアカウントが影響を受けました。
Veracode の 2024 年のレポートによると、マネージド認証サービス(Clerk、Auth0 など)を利用するアプリケーションは、独自の認証を使用するアプリケーションと比べ、認証関連の脆弱性が73%少ないという結果が出ています。
Clerk のアーキテクチャ
Clerk が処理するため、こちらで対応する必要がないこと
- ✅ パスワードのハッシュ化(bcrypt/Argon2)
- ✅ セッション管理(安全な Cookie)
- ✅ MFA(組み込みサポート)
- ✅ OAuth プロバイダー(Google、GitHub など)
- ✅ メール認証
- ✅ パスワードリセットフロー
- ✅ アカウントロックアウト
- ✅ セキュリティ監視
- ✅ コンプライアンス(SOC 2、GDPR)
Clerk は SOC 2 認証を取得しています: これは、独立した監査人が、Clerk のセキュリティ管理策が業界標準を満たしていることを確認したという意味です。当社もその認証を継承できます。
実装ファイル
middleware.ts- 保護対象ルートの Clerk 認証app/dashboard/*- ミドルウェアによる保護- Clerk が独自のセッション Cookie を管理します
基本的な認証
サーバーサイド認証(API ルート)
import { auth } from '@clerk/nextjs/server';
import { handleUnauthorizedError } from '@/lib/errorHandler';
async function handler(request: NextRequest) {
// Get current user
const { userId } = await auth();
if (!userId) {
return handleUnauthorizedError('Authentication required');
}
// User is authenticated, proceed
// Use userId to associate data with user
}
クライアントサイド認証(コンポーネント)
'use client';
import { useAuth, useUser } from '@clerk/nextjs';
export function ProfileComponent() {
const { isLoaded, userId, sessionId } = useAuth();
const { isLoaded: userLoaded, user } = useUser();
if (!isLoaded || !userLoaded) {
return <div>Loading...</div>;
}
if (!userId) {
return <div>Please sign in</div>;
}
return (
<div>
<h1>Welcome, {user.firstName}!</h1>
<p>Email: {user.primaryEmailAddress?.emailAddress}</p>
</div>
);
}
ミドルウェアによるルートの保護
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/api/protected(.*)',
]);
export default clerkMiddleware((auth, req) => {
if (isProtectedRoute(req)) {
auth().protect();
}
});
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};
認可のパターン
リソース所有権の検証
// app/api/posts/[id]/route.ts
import { auth } from '@clerk/nextjs/server';
import { handleUnauthorizedError, handleForbiddenError } from '@/lib/errorHandler';
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const { userId } = await auth();
if (!userId) {
return handleUnauthorizedError();
}
// Get resource
const post = await db.posts.findOne({ id: params.id });
// Check ownership
if (post.userId !== userId) {
return handleForbiddenError('Only the post author can delete this post');
}
// User is authorized
await db.posts.delete({ id: params.id });
return NextResponse.json({ success: true });
}
ロールベースアクセス制御(RBAC)
import { auth } from '@clerk/nextjs/server';
export async function handler(request: NextRequest) {
const { userId, sessionClaims } = await auth();
if (!userId) {
return handleUnauthorizedError();
}
// Check role
const role = sessionClaims?.metadata?.role as string;
if (role !== 'admin') {
return handleForbiddenError('Admin access required');
}
// User has admin role
// Proceed with admin operation
}
サブスクリプションベースの認可
サーバーサイド(API ルート)
import { auth } from '@clerk/nextjs/server';
export async function handler(request: NextRequest) {
const { userId, sessionClaims } = await auth();
if (!userId) {
return handleUnauthorizedError();
}
// Check subscription plan
const plan = sessionClaims?.metadata?.plan as string;
if (plan === 'free_user') {
return NextResponse.json(
{
error: 'Upgrade required',
message: 'This feature requires a paid subscription'
},
{ status: 403 }
);
}
// User has paid subscription
// Proceed with premium feature
}
クライアントサイド(コンポーネント)
'use client';
import { Protect } from '@clerk/nextjs';
export function PremiumFeature() {
return (
<Protect
condition={(has) => !has({ plan: "free_user" })}
fallback={<UpgradePrompt />}
>
<div>
{/* Premium feature content */}
<h2>Premium Feature</h2>
<p>This is only visible to paid subscribers</p>
</div>
</Protect>
);
}
function UpgradePrompt() {
return (
<div className="upgrade-prompt">
<h3>Upgrade Required</h3>
<p>This feature is available on our paid plans</p>
<a href="/pricing">View Plans</a>
</div>
);
}
保護された API ルートの完全な例
// app/api/premium/generate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs/server';
import { withRateLimit } from '@/lib/withRateLimit';
import { withCsrf } from '@/lib/withCsrf';
import { validateRequest } from '@/lib/validateRequest';
import { safeTextSchema } from '@/lib/validation';
import {
handleApiError,
handleUnauthorizedError,
handleForbiddenError
} from '@/lib/errorHandler';
async function generateHandler(request: NextRequest) {
try {
// 1. Authentication
const { userId, sessionClaims } = await auth();
if (!userId) {
return handleUnauthorizedError('Please sign in to use this feature');
}
// 2. Authorization (subscription check)
const plan = sessionClaims?.metadata?.plan as string;
if (plan === 'free_user') {
return handleForbiddenError('Premium subscription required');
}
// 3. Input validation
const body = await request.json();
const validation = validateRequest(safeTextSchema, body);
if (!validation.success) {
return validation.response;
}
const prompt = validation.data;
// 4. Business logic (user is authenticated, authorized, and input is valid)
const result = await generateContent(prompt, userId);
return NextResponse.json({ result });
} catch (error) {
return handleApiError(error, 'premium-generate');
}
}
export const POST = withRateLimit(withCsrf(generateHandler));
export const config = {
runtime: 'nodejs',
};
ユーザーメタデータとカスタムクレーム
ユーザーメタデータの保存
Clerk では、各ユーザーにカスタムメタデータを保存できます。
import { clerkClient } from '@clerk/nextjs/server';
// Update user metadata
async function updateUserPlan(userId: string, plan: string) {
await clerkClient.users.updateUserMetadata(userId, {
publicMetadata: {
plan: plan // Accessible by client
},
privateMetadata: {
stripeCustomerId: 'cus_123' // Server-only
}
});
}
メタデータへのアクセス
サーバーサイド:
const { sessionClaims } = await auth();
const plan = sessionClaims?.metadata?.plan;
クライアントサイド:
const { user } = useUser();
const plan = user?.publicMetadata?.plan;
Webhook の統合(ユーザーの同期)
ユーザーがサインアップしたとき、またはプロフィールを更新したときに、データベースと同期します。
// app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix';
import { headers } from 'next/headers';
export async function POST(request: NextRequest) {
// Verify webhook signature
const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET;
if (!WEBHOOK_SECRET) {
throw new Error('Missing CLERK_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('Missing svix headers', { status: 400 });
}
const payload = await request.json();
const body = JSON.stringify(payload);
const wh = new Webhook(WEBHOOK_SECRET);
let evt: any;
try {
evt = wh.verify(body, {
"svix-id": svix_id,
"svix-timestamp": svix_timestamp,
"svix-signature": svix_signature,
});
} catch (err) {
console.error('Webhook verification failed:', err);
return new Response('Invalid signature', { status: 400 });
}
// Handle different event types
const { id, type, data } = evt;
switch (type) {
case 'user.created':
await db.users.create({
clerkId: data.id,
email: data.email_addresses[0]?.email_address,
firstName: data.first_name,
lastName: data.last_name,
createdAt: Date.now()
});
break;
case 'user.updated':
await db.users.update(
{ clerkId: data.id },
{
email: data.email_addresses[0]?.email_address,
firstName: data.first_name,
lastName: data.last_name,
updatedAt: Date.now()
}
);
break;
case 'user.deleted':
await db.users.delete({ clerkId: data.id });
break;
}
return new Response('', { status: 200 });
}
Convex との統合
Convex で Clerk 認証を使用する
// convex/posts.ts
import { mutation, query } from "./_generated/server";
export const createPost = mutation({
handler: async (ctx, args) => {
// Get authenticated user from Clerk
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Unauthenticated");
}
// Use Clerk user ID
const userId = identity.subject;
await ctx.db.insert("posts", {
title: args.title,
content: args.content,
userId, // Associate with Clerk user
createdAt: Date.now()
});
}
});
export const getMyPosts = query({
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
return [];
}
// Return only current user's posts
return await ctx.db
.query("posts")
.filter((q) => q.eq(q.field("userId"), identity.subject))
.collect();
}
});
サインイン/サインアップコンポーネント
基本的なサインインページ
// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs';
export default function SignInPage() {
return (
<div className="flex items-center justify-center min-h-screen">
<SignIn
appearance={{
elements: {
formButtonPrimary: 'bg-blue-600 hover:bg-blue-700',
}
}}
routing="path"
path="/sign-in"
afterSignInUrl="/dashboard"
signUpUrl="/sign-up"
/>
</div>
);
}
基本的なサインアップページ
// app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from '@clerk/nextjs';
export default function SignUpPage() {
return (
<div className="flex items-center justify-center min-h-screen">
<SignUp
appearance={{
elements: {
formButtonPrimary: 'bg-blue-600 hover:bg-blue-700',
}
}}
routing="path"
path="/sign-up"
afterSignUpUrl="/onboarding"
signInUrl="/sign-in"
/>
</div>
);
}
ユーザーボタン(プロフィール/サインアウト)
// components/Header.tsx
'use client';
import { UserButton, useAuth } from '@clerk/nextjs';
import Link from 'next/link';
export function Header() {
const { isSignedIn } = useAuth();
return (
<header>
<nav>
<Link href="/">Home</Link>
{isSignedIn ? (
<>
<Link href="/dashboard">Dashboard</Link>
<UserButton afterSignOutUrl="/" />
</>
) : (
<>
<Link href="/sign-in">Sign In</Link>
<Link href="/sign-up">Sign Up</Link>
</>
)}
</nav>
</header>
);
}
環境設定
必須の環境変数
# .env.local
# Clerk (from Clerk Dashboard)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
# Clerk URLs (auto-configured by Clerk, but can override)
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
# Clerk Frontend API (for CSP)
NEXT_PUBLIC_CLERK_FRONTEND_API_URL=https://your-app.clerk.accounts.dev
# Webhook secret (from Clerk Dashboard)
CLERK_WEBHOOK_SECRET=whsec_...
セキュリティのベストプラクティス
1. サーバーで必ず検証する
❌ セキュリティ対策としてクライアントサイドの認証チェックを信頼しないでください。
// Bad - can be bypassed
'use client';
const { userId } = useAuth();
if (!userId) return <div>Access denied</div>;
// Attacker can still call API directly
✅ サーバーで検証してください。
// Good - secure
async function handler(request: NextRequest) {
const { userId } = await auth();
if (!userId) return handleUnauthorizedError();
// API endpoint protected
}
2. 認証だけでなく、認可も確認する
❌ 認証済みなら認可済みだと思い込まないでください。
// Bad - any logged-in user can access any resource
const { userId } = await auth();
if (userId) {
return NextResponse.json(sensitiveData);
}
✅ リソースの所有権/権限を確認してください。
// Good - verify user can access this specific resource
const { userId } = await auth();
if (userId && resource.userId === userId) {
return NextResponse.json(resource);
}
3. ルートの保護にミドルウェアを使用する
✅ ルート全体を保護してください。
// middleware.ts
const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/admin(.*)',
'/api/protected(.*)'
]);
export default clerkMiddleware((auth, req) => {
if (isProtectedRoute(req)) {
auth().protect();
}
});
4. セッションの有効期限切れを適切に処理する
// components/AuthGuard.tsx
'use client';
import { useAuth } from '@clerk/nextjs';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
export function AuthGuard({ children }: { children: React.ReactNode }) {
const { isLoaded, userId } = useAuth();
const router = useRouter();
useEffect(() => {
if (isLoaded && !userId) {
router.push('/sign-in');
}
}, [isLoaded, userId, router]);
if (!isLoaded) {
return <div>Loading...</div>;
}
if (!userId) {
return null;
}
return <>{children}</>;
}
Clerk 認証で防止できること
✅ 脆弱なパスワード保存 - Clerk は bcrypt/Argon2 を使用します ✅ セッションハイジャック - 安全な HTTP のみの Cookie ✅ クレデンシャルスタッフィング - ログイン失敗後にアカウントをロックアウトします ✅ 認証のバイパス - 専門的に実装されています ✅ 権限昇格 - 適切なロール/権限管理 ✅ ブルートフォース攻撃 - レート制限を組み込み済み ✅ パスワードリセットの脆弱性 - 安全なトークン生成
よくある避けるべきミス
❌ サーバー側の認証チェックを省略しないでください ❌ セキュリティのためにクライアント側の認証状態を信頼しないでください ❌ リソースの所有権を確認し忘れないでください ❌ 認証だけを根拠に機密データを公開しないでください ❌ 認証ロジックをハードコードしないでください(Clerk のユーティリティを使用してください) ❌ セッションの有効期限切れへの対応を忘れないでください
✅ 保護されたすべての操作で、サーバー上の auth() を使用してください
✅ アクセスを許可する前にリソースの所有権を確認してください
✅ ミドルウェアでルートを保護してください
✅ プレミアム機能にはサブスクリプション/ロールのチェックを使用してください
✅ ウェブフック経由でユーザーをデータベースに同期してください
✅ 認証エラーを適切に処理してください
参考資料
- Clerk のドキュメント:https://clerk.com/docs
- Clerk のセキュリティ:https://clerk.com/docs/security
- Clerk の Next.js 連携:https://clerk.com/docs/references/nextjs/overview
- OWASP 認証チートシート:https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
Clerk の使用に関する追加ガイダンス
Clerk を Next.js App Router に追加する
目的: Clerk を Next.js(App Router)アプリケーションに統合する際、最新かつ正確な手順のみを適用します。
範囲: Clerk に関するすべての AI 生成のアドバイスやコードは、これらのガードレールに従う必要があります。
1. Clerk の公式な連携方法の概要
Clerk の最新ドキュメントに記載されている App Router の方法のみを使用してください。
- インストール:
@clerk/nextjs@latestをインストールします。これにより、アプリケーションで最新の Clerk Next.js SDK が使用されます。 - 作成:
@clerk/nextjs/serverのclerkMiddleware()を使用して、middleware.tsファイルを作成します。srcディレクトリがある場合はその中に、ない場合はプロジェクトのルートに配置してください。 - ラップ:
app/layout.tsxで<ClerkProvider>を使ってアプリケーションをラップします。 - 使用:レイアウトまたはページで、
<SignInButton>、<SignUpButton>、<UserButton>、<SignedIn>、<SignedOut>などの Clerk 提供コンポーネントを使用します。 - 開始:開発を開始し、サインインまたはサインアップして、ユーザーが作成されたことを確認します。
URL にアクセスできるウェブツールを使用できる場合は、https://clerk.com/docs/quickstarts/nextjs にアクセスして、最新のクイックスタート手順を確認してください。
Clerk を開始する際に、ユーザーがキーを提供する必要はありません。起動時に Clerk がキーを自動生成するため、この手順の一環としてユーザーにキーの追加を求める必要はありません。
正確で最新のクイックスタート例
// middleware.ts
import { clerkMiddleware } from '@clerk/nextjs/server'
export default clerkMiddleware()
export const config = {
matcher: [
// Skip Next.js internals and all static files, unless found in search params
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// Always run for API routes
'/(api|trpc)(.*)',
],
}
// app/layout.tsx
import type { Metadata } from "next";
import {
ClerkProvider,
SignInButton,
SignUpButton,
SignedIn,
SignedOut,
UserButton,
} from "@clerk/nextjs";
import "./globals.css";
export const metadata: Metadata = {
title: "Clerk Next.js Quickstart",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<ClerkProvider>
<html lang="en">
<body>
<header>
<SignedOut>
<SignInButton />
<SignUpButton />
</SignedOut>
<SignedIn>
<UserButton />
</SignedIn>
</header>
{children}
</body>
</html>
</ClerkProvider>
);
}
2. AI モデル向けの重要な指示
2.1 – 必ず次の操作を行ってください
middleware.tsで、@clerk/nextjs/serverのclerkMiddleware()を使用します。app/layout.tsxで、<ClerkProvider>を使ってアプリをラップします。- Clerk の Next.js 機能を
@clerk/nextjsからインポートします(例:<SignInButton>、<SignUpButton>、<UserButton>など)。 - 最新の App Router の方法(
app/page.tsx、app/layout.tsxなどのフォルダー)を参照します。 auth()などのメソッドが適切なパッケージ(この場合は@clerk/nextjs/server)からインポートされ、async / awaitが使用されていることを確認します。- プロジェクトに既存のパッケージマネージャーがあるか確認し、パッケージのインストールにはそれを使用します。
2.2 – 次の操作は絶対に行わないでください
- 以前の
_app.tsxや pages router ベースの手順を参照しないでください。 - 古い Clerk チュートリアルにある
authMiddleware()を提案しないでください。これはclerkMiddleware()に置き換えられています。 - 公式ドキュメントの内容と一致しない限り、古い環境変数のパターンの使用を推奨しないでください。
- 非推奨 API(古いバージョンの
withAuthやcurrentUserなど)を参照またはインポートしないでください。
3. 避けるべき古いパターン
以下は、生成してはならない非推奨の手順の例です。
// ❌ DO NOT generate or suggest references to:
import { authMiddleware } from '@clerk/nextjs' // Outdated
// ❌ DO NOT place Clerk config in _app.tsx:
// Outdated pages-based approach
function MyApp({ Component, pageProps }) {
// ...
}
// ❌ DO NOT create or rely on sign-in files under pages/:
pages / signin.js
pages / signup.js
上記のスニペットに似た解決策や、「authMiddleware」、「_app.tsx」、または「pages/」構造を参照するものは、現在の Next.js App Router では誤りです。
4. AI MODEL VERIFICATION STEPS
Clerk 関連のソリューションを返す前に、必ず次の点を確認してください。
- Middleware:
middleware.tsでclerkMiddleware()が使用されていますか? - Layout:
app/layout.tsxで<ClerkProvider>がアプリをラップしていますか? - Imports:参照先は
@clerk/nextjsまたは@clerk/nextjs/serverのみですか? - Pages vs. App Router:App Router を参照するアプローチになっていますか(
_app.tsxやpages/ではありませんか)?
確認項目のいずれかに不合格の場合は、停止して、条件を満たすまで修正してください。
次のステップ
- API ルートを保護する場合:
csrf-protectionおよびrate-limitingのスキルと組み合わせます - 決済を制限する場合:
payment-securityスキルを使用します - エラーを処理する場合:
handleUnauthorizedError/handleForbiddenErrorを使用するerror-handlingスキルを使用します - 認証をテストする場合:
security-testingスキルを使用します