mcpField
A
authentication-authorization-clerk

authentication-authorization-clerk

github.com/harperaaセキュリティ19186 回インストール45 週間前に更新
リポジトリ

authentication-authorization-clerk について

authentication-authorization-clerkは、Next.js App RouterアプリでClerkを使うためのセキュリティスキルです。サインイン、ルート保護、所有権やロールの確認、メタデータ、ユーザー同期の手順と例を扱います。

主な機能

  1. 01Next.js向けClerk認証パターン
  2. 02ミドルウェアによるルート保護
  3. 03リソース所有権とロールの確認
  4. 04ユーザーメタデータとカスタムクレーム
  5. 05Webhookによるユーザー同期

活用シーン

  1. 01ダッシュボードやAPIルートの保護
  2. 02ユーザー所有リソースへのアクセス確認
  3. 03ロールやサブスクリプションによる機能制限
  4. 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 の使用に関する追加ガイダンス

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 – 必ず次の操作を行ってください
  1. middleware.ts で、@clerk/nextjs/server の clerkMiddleware() を使用します。
  2. app/layout.tsx で、<ClerkProvider> を使ってアプリをラップします。
  3. Clerk の Next.js 機能を @clerk/nextjs からインポートします(例:<SignInButton>、<SignUpButton>、<UserButton> など)。
  4. 最新の App Router の方法(app/page.tsx、app/layout.tsx などのフォルダー)を参照します。
  5. auth() などのメソッドが適切なパッケージ(この場合は @clerk/nextjs/server)からインポートされ、async / await が使用されていることを確認します。
  6. プロジェクトに既存のパッケージマネージャーがあるか確認し、パッケージのインストールにはそれを使用します。
2.2 – 次の操作は絶対に行わないでください
  1. 以前の _app.tsx や pages router ベースの手順を参照しないでください。
  2. 古い Clerk チュートリアルにある authMiddleware() を提案しないでください。これは clerkMiddleware() に置き換えられています。
  3. 公式ドキュメントの内容と一致しない限り、古い環境変数のパターンの使用を推奨しないでください。
  4. 非推奨 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 関連のソリューションを返す前に、必ず次の点を確認してください。

  1. Middleware:middleware.ts で clerkMiddleware() が使用されていますか?
  2. Layout:app/layout.tsx で <ClerkProvider> がアプリをラップしていますか?
  3. Imports:参照先は @clerk/nextjs または @clerk/nextjs/server のみですか?
  4. Pages vs. App Router:App Router を参照するアプローチになっていますか(_app.tsx や pages/ ではありませんか)?

確認項目のいずれかに不合格の場合は、停止して、条件を満たすまで修正してください。

次のステップ

  • API ルートを保護する場合:csrf-protection および rate-limiting のスキルと組み合わせます
  • 決済を制限する場合:payment-security スキルを使用します
  • エラーを処理する場合:handleUnauthorizedError/handleForbiddenError を使用する error-handling スキルを使用します
  • 認証をテストする場合:security-testing スキルを使用します

コメント