mcpField
A
auth-setup

auth-setup

github.com/get-convexセキュリティ117208 回インストール今日更新
リポジトリ

auth-setup について

Convex アプリケーションで認証とアクセス制御を実装するためのスキルです。ユーザーの ID 対応付け、プロバイダー設定、保護されたデータへのアクセス方法を開発者向けに案内します。

主な機能

  1. 01Convex のユーザー ID 対応付け
  2. 02認証ヘルパー関数
  3. 03OAuth プロバイダー設定
  4. 04所有者・チーム単位のアクセス制御
  5. 05公開・非公開クエリのパターン

活用シーン

  1. 01新しい Convex アプリへの認証導入
  2. 02プロバイダー ID とユーザーレコードの対応付け
  3. 03ユーザー所有レコードの保護
  4. 04React または Next.js での WorkOS 認証設定

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/get-convex/convex-agent-plugins --skill auth-setup

インストール後、Claude Code で/auth-setupを入力して読み込まれたか確認します。

詳細

Convex 認証の設定

ユーザー管理とアクセス制御を備えた、安全な認証を Convex に実装します。

使用する場面

  • 初めて認証を設定するとき
  • ユーザー管理(ユーザーテーブル、ID のマッピング)を実装するとき
  • 認証ヘルパー関数を作成するとき
  • OAuth プロバイダー(WorkOS、Auth0 など)を設定するとき

アーキテクチャの概要

Convex 認証には、次の 2 つの主な要素があります。

  1. クライアント認証:プロバイダー(WorkOS、Auth0、カスタム JWT)を使用します
  2. バックエンドの ID:認証プロバイダーの ID をユーザーテーブルにマッピングします

スキーマの設定

// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  users: defineTable({
    // From auth provider identity
    tokenIdentifier: v.string(), // Unique per auth provider

    // User profile data
    name: v.string(),
    email: v.string(),
    pictureUrl: v.optional(v.string()),

    // Your app-specific fields
    role: v.union(
      v.literal("user"),
      v.literal("admin")
    ),

    createdAt: v.number(),
    updatedAt: v.optional(v.number()),
  })
    .index("by_token", ["tokenIdentifier"])
    .index("by_email", ["email"]),
});

主なヘルパー関数

現在のユーザーを取得
// convex/lib/auth.ts
import { QueryCtx, MutationCtx } from "./_generated/server";
import { Doc } from "./_generated/dataModel";

export async function getCurrentUser(
  ctx: QueryCtx | MutationCtx
): Promise<Doc<"users">> {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) {
    throw new Error("Not authenticated");
  }

  const user = await ctx.db
    .query("users")
    .withIndex("by_token", q =>
      q.eq("tokenIdentifier", identity.tokenIdentifier)
    )
    .unique();

  if (!user) {
    throw new Error("User not found");
  }

  return user;
}

export async function getCurrentUserOrNull(
  ctx: QueryCtx | MutationCtx
): Promise<Doc<"users"> | null> {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) {
    return null;
  }

  return await ctx.db
    .query("users")
    .withIndex("by_token", q =>
      q.eq("tokenIdentifier", identity.tokenIdentifier)
    )
    .unique();
}
管理者権限を必須にする
export async function requireAdmin(
  ctx: QueryCtx | MutationCtx
): Promise<Doc<"users">> {
  const user = await getCurrentUser(ctx);

  if (user.role !== "admin") {
    throw new Error("Admin access required");
  }

  return user;
}

ユーザーの作成/アップサート

初回サインイン時
// convex/users.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";

export const storeUser = mutation({
  args: {},
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) {
      throw new Error("Not authenticated");
    }

    // Check if user exists
    const existingUser = await ctx.db
      .query("users")
      .withIndex("by_token", q =>
        q.eq("tokenIdentifier", identity.tokenIdentifier)
      )
      .unique();

    if (existingUser) {
      // Update last seen or other fields
      await ctx.db.patch(existingUser._id, {
        updatedAt: Date.now(),
      });
      return existingUser._id;
    }

    // Create new user
    const userId = await ctx.db.insert("users", {
      tokenIdentifier: identity.tokenIdentifier,
      name: identity.name ?? "Anonymous",
      email: identity.email ?? "",
      pictureUrl: identity.pictureUrl,
      role: "user",
      createdAt: Date.now(),
    });

    return userId;
  },
});

アクセス制御のパターン

所有者のみアクセス可能
import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { getCurrentUser } from "./lib/auth";

export const updateProfile = mutation({
  args: {
    name: v.string(),
  },
  handler: async (ctx, args) => {
    const user = await getCurrentUser(ctx);

    await ctx.db.patch(user._id, {
      name: args.name,
      updatedAt: Date.now(),
    });
  },
});
リソースの所有権
export const deleteTask = mutation({
  args: { taskId: v.id("tasks") },
  handler: async (ctx, args) => {
    const user = await getCurrentUser(ctx);

    const task = await ctx.db.get(args.taskId);
    if (!task) {
      throw new Error("Task not found");
    }

    // Check ownership
    if (task.userId !== user._id) {
      throw new Error("You can only delete your own tasks");
    }

    await ctx.db.delete(args.taskId);
  },
});
チームベースのアクセス
// Schema includes membership table
export default defineSchema({
  teams: defineTable({
    name: v.string(),
    ownerId: v.id("users"),
  }),

  teamMembers: defineTable({
    teamId: v.id("teams"),
    userId: v.id("users"),
    role: v.union(v.literal("owner"), v.literal("member")),
  })
    .index("by_team", ["teamId"])
    .index("by_user", ["userId"])
    .index("by_team_and_user", ["teamId", "userId"]),
});

// Helper to check team access
async function requireTeamAccess(
  ctx: MutationCtx,
  teamId: Id<"teams">
): Promise<{ user: Doc<"users">, membership: Doc<"teamMembers"> }> {
  const user = await getCurrentUser(ctx);

  const membership = await ctx.db
    .query("teamMembers")
    .withIndex("by_team_and_user", q =>
      q.eq("teamId", teamId).eq("userId", user._id)
    )
    .unique();

  if (!membership) {
    throw new Error("You don't have access to this team");
  }

  return { user, membership };
}

// Use in functions
export const createProject = mutation({
  args: {
    teamId: v.id("teams"),
    name: v.string(),
  },
  handler: async (ctx, args) => {
    await requireTeamAccess(ctx, args.teamId);

    return await ctx.db.insert("projects", {
      teamId: args.teamId,
      name: args.name,
    });
  },
});

パブリッククエリとプライベートクエリ

パブリッククエリ(認証不要)
export const listPublicPosts = query({
  args: {},
  handler: async (ctx) => {
    // No auth check - anyone can read
    return await ctx.db
      .query("posts")
      .withIndex("by_published", q => q.eq("published", true))
      .collect();
  },
});
プライベートクエリ(認証必須)
export const getMyPosts = query({
  args: {},
  handler: async (ctx) => {
    const user = await getCurrentUser(ctx);

    return await ctx.db
      .query("posts")
      .withIndex("by_user", q => q.eq("userId", user._id))
      .collect();
  },
});
ハイブリッドクエリ(認証は任意)
export const getPosts = query({
  args: {},
  handler: async (ctx) => {
    const user = await getCurrentUserOrNull(ctx);

    if (user) {
      // Show all posts including drafts for this user
      return await ctx.db
        .query("posts")
        .withIndex("by_user", q => q.eq("userId", user._id))
        .collect();
    } else {
      // Show only public posts for anonymous users
      return await ctx.db
        .query("posts")
        .withIndex("by_published", q => q.eq("published", true))
        .collect();
    }
  },
});

WorkOS を使用したクライアントの設定

WorkOS AuthKit は、最小限の設定で包括的な認証ソリューションを提供します。

React/Vite の設定
npm install @workos-inc/authkit-react
// src/main.tsx
import { AuthKitProvider, useAuth } from "@workos-inc/authkit-react";
import { ConvexReactClient } from "convex/react";
import { ConvexProvider } from "convex/react";

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);

// Configure Convex to use WorkOS auth
convex.setAuth(useAuth);

function App() {
  return (
    <AuthKitProvider clientId={import.meta.env.VITE_WORKOS_CLIENT_ID}>
      <ConvexProvider client={convex}>
        <YourApp />
      </ConvexProvider>
    </AuthKitProvider>
  );
}
Next.js の設定
npm install @workos-inc/authkit-nextjs
// app/layout.tsx
import { AuthKitProvider } from "@workos-inc/authkit-nextjs";
import { ConvexClientProvider } from "./ConvexClientProvider";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <AuthKitProvider>
          <ConvexClientProvider>
            {children}
          </ConvexClientProvider>
        </AuthKitProvider>
      </body>
    </html>
  );
}
// app/ConvexClientProvider.tsx
"use client";

import { ConvexReactClient } from "convex/react";
import { ConvexProvider } from "convex/react";
import { useAuth } from "@workos-inc/authkit-nextjs";

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

export function ConvexClientProvider({ children }: { children: React.ReactNode }) {
  const { getToken } = useAuth();

  convex.setAuth(async () => {
    return await getToken();
  });

  return <ConvexProvider client={convex}>{children}</ConvexProvider>;
}
環境変数
# .env.local (React/Vite)
VITE_CONVEX_URL=https://your-deployment.convex.cloud
VITE_WORKOS_CLIENT_ID=your_workos_client_id

# .env.local (Next.js)
NEXT_PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud
NEXT_PUBLIC_WORKOS_CLIENT_ID=your_workos_client_id
WORKOS_API_KEY=your_workos_api_key
WORKOS_COOKIE_PASSWORD=generate_a_random_32_character_string
サインイン時に storeUser を呼び出す
// In your app after user signs in
import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
import { useEffect } from "react";
import { useAuth } from "@workos-inc/authkit-react";

function YourApp() {
  const { user } = useAuth();
  const storeUser = useMutation(api.users.storeUser);

  useEffect(() => {
    if (user) {
      storeUser();
    }
  }, [user, storeUser]);

  // ... rest of your app
}
その他の認証プロバイダー

別のプロバイダーを使用する必要がある場合は、Convex の認証ドキュメントを参照してください。

  • カスタム JWT
  • Auth0
  • その他の OAuth プロバイダー

チェックリスト

  • tokenIdentifier インデックスを持つユーザーテーブル
  • getCurrentUser ヘルパー関数
  • 初回サインイン時の storeUser ミューテーション
  • 保護されたすべての関数での認証チェック
  • リソースアクセスに対する認可チェック
  • 明確なエラーメッセージ(「Not authenticated」、「Unauthorized」)
  • クライアント認証プロバイダーの設定(WorkOS、Auth0 など)

コメント