Back to Skills

composio-oauth-integration

majiayu000
Updated Yesterday
58
9
58
View on GitHub
Metadesign

About

This Claude Skill enables secure OAuth 2.0 connections to external services like GitHub and Slack through Composio. It manages the complete OAuth flow with CSRF protection, connection state transitions, and automatic tool registration from active connections. Use this skill when you need to integrate third-party services that require OAuth authentication in your application.

Quick Install

Claude Code

Recommended
Plugin CommandRecommended
/plugin add https://github.com/majiayu000/claude-skill-registry
Git CloneAlternative
git clone https://github.com/majiayu000/claude-skill-registry.git ~/.claude/skills/composio-oauth-integration

Copy and paste this command in Claude Code to install this skill

Documentation

Composio OAuth Integration

Connect external services (GitHub, Slack, etc.) via Composio OAuth with CSRF protection and dynamic tool registration.

OAuth Flow

Initiate Connection

// From convex/composio/oauth.ts
export const initiateConnection = action({
  args: { integrationId: v.string(), redirectUrl: v.string() },
  handler: async (ctx, { integrationId, redirectUrl }) => {
    // 1. Get user
    const user = await ctx.runQuery(api.users.getUserByClerkId, { clerkId });

    // 2. Check integration limit (before creating new connection)
    const existingConnection = await ctx.runQuery(
      api.composio.connections.getConnectionByIntegration,
      { integrationId }
    );

    if (!existingConnection) {
      const activeConnections = await ctx.runQuery(
        api.composio.connections.getActiveConnections
      );
      if (activeConnections.length >= maxIntegrations) {
        throw new Error("Integration limit reached");
      }
    }

    // 3. Generate CSRF state (32 bytes)
    const oauthState = randomBytes(32).toString("hex");

    // 4. Create unique entity ID
    const composioUserId = `blahchat_${user._id}`;

    // 5. Initiate with Composio
    const connectionRequest = await composio.connectedAccounts.initiate(
      composioUserId,
      authConfigId,
      { callbackUrl: redirectUrl, allowMultiple: true }
    );

    // 6. Store in DB with state (expires in 10 min)
    await ctx.runMutation(internal.composio.connections.createConnection, {
      userId: user._id,
      composioConnectionId: connectionRequest.id,
      integrationId,
      oauthState,
      // stateExpiresAt: now + 10min (set in mutation)
    });

    return {
      redirectUrl: connectionRequest.redirectUrl,
      state: oauthState // Return to frontend for verification
    };
  }
});

Key patterns:

  • CSRF state: 32-byte hex (not UUID), expires 10 min
  • Entity ID format: blahchat_${userId} (matches across flows)
  • allowMultiple: true enables re-auth without losing active status
  • Check limit BEFORE creating new connection, skip for re-auth

Verify Connection (Callback)

// From convex/composio/oauth.ts
export const verifyConnection = action({
  args: { composioConnectionId: v.string(), state: v.optional(v.string()) },
  handler: async (ctx, { composioConnectionId, state }) => {
    // 1. Get connection from DB
    const existingConnection = await ctx.runQuery(
      internal.composio.connections.getConnectionByComposioId,
      { composioConnectionId }
    );

    // 2. SECURITY: Verify ownership
    if (existingConnection.userId !== user._id) {
      throw new Error("Unauthorized: Connection belongs to another user");
    }

    // 3. SECURITY: Validate CSRF state
    if (existingConnection.oauthState) {
      if (!state) throw new Error("Missing state parameter");
      if (state !== existingConnection.oauthState) {
        throw new Error("Invalid state parameter - possible CSRF attack");
      }
      if (Date.now() > existingConnection.oauthStateExpiresAt) {
        throw new Error("OAuth state expired - please try again");
      }
    }

    // 4. Check status with Composio
    const connection = await composio.connectedAccounts.get(composioConnectionId);

    if (connection.status === "ACTIVE") {
      await ctx.runMutation(
        internal.composio.connections.updateConnectionStatus,
        { composioConnectionId, status: "active" }
      );
      return { status: "active" };
    }

    // Handle pending/failed states
    const status = connection.status === "INITIATED" ? "initiated" : "failed";
    await ctx.runMutation(
      internal.composio.connections.updateConnectionStatus,
      { composioConnectionId, status, error: ... }
    );

    return { status };
  }
});

CSRF validation:

  • Check oauthState field exists in DB
  • Verify state matches callback parameter
  • Enforce 10-minute expiration
  • Backwards compatible (optional state for old connections)

Connection Status Lifecycle

States: pending | initiated | active | expired | failed

Transitions:

pending → initiated (OAuth flow starts)
initiated → active (OAuth completes successfully)
active → expired (token refresh fails during tool execution)
initiated → failed (OAuth flow fails)
active → active (re-auth preserves status if user cancels popup)

Status preservation during re-auth:

// From convex/composio/connections.ts (createConnection mutation)
if (existing) {
  await ctx.db.patch(existing._id, {
    composioConnectionId: args.composioConnectionId,
    status: existing.status === "active" ? "active" : "initiated",
    // ^ Preserve active status during re-auth
    oauthState: args.oauthState,
    oauthStateExpiresAt: stateExpiresAt,
    lastError: undefined, // Clear previous error
  });
  return existing._id;
}

Why preserve: User clicks "Manage" button → popup opens → popup canceled → connection still works. Don't break tools during re-auth attempt.

Dynamic Tool Building

// From convex/composio/tools.ts
export async function createComposioTools(
  ctx: ActionCtx,
  config: { userId: Id<"users">; connections: Doc<"composioConnections">[] }
) {
  // 1. Filter to active connections only
  const activeConnections = config.connections.filter(c => c.status === "active");

  if (activeConnections.length === 0) {
    return { tools: {}, connectedApps: [] };
  }

  // 2. Initialize Composio with Vercel provider
  const composio = new Composio({
    apiKey: process.env.COMPOSIO_API_KEY,
    provider: new VercelProvider() // Vercel AI SDK compatible
  });

  // 3. Create entity ID (must match OAuth flow)
  const entityId = `blahchat_${userId}`;

  // 4. Get toolkits (lowercase integration IDs)
  const connectedToolkits = activeConnections.map(c => c.integrationId.toLowerCase());

  // 5. Fetch tools from Composio
  const tools = await composio.tools.get(entityId, {
    toolkits: connectedToolkits,
    limit: 100
  });

  // 6. Wrap tools to track usage and handle errors
  const wrappedTools: Record<string, unknown> = {};

  for (const [name, originalTool] of Object.entries(tools)) {
    wrappedTools[name] = {
      ...tool,
      execute: async (...args: unknown[]) => {
        // Update lastUsedAt timestamp
        const appName = name.split("_")[0]; // "GITHUB_CREATE_ISSUE" → "GITHUB"
        const connection = activeConnections.find(
          c => c.integrationId.toUpperCase() === appName.toUpperCase()
        );

        if (connection) {
          await ctx.runMutation(
            internal.composio.connections.markConnectionUsed,
            { connectionId: connection._id }
          );
        }

        try {
          return await tool.execute!(...args);
        } catch (error) {
          // Handle expired tokens
          if (error.message.includes("expired") ||
              error.message.includes("401") ||
              error.message.includes("unauthorized")) {

            // Mark connection as expired
            await ctx.runMutation(
              internal.composio.connections.updateConnectionStatus,
              {
                composioConnectionId: connection.composioConnectionId,
                status: "expired",
                error: "Token expired - please reconnect"
              }
            );

            throw new Error(
              `${appName} connection expired. Please reconnect in Settings > Integrations.`
            );
          }
          throw error;
        }
      }
    };
  }

  return {
    tools: wrappedTools,
    connectedApps: activeConnections.map(c => c.integrationName)
  };
}

Integration with generation:

// From convex/generation/tools.ts
export async function buildToolsAsync(config: BuildToolsConfig) {
  const tools = buildTools(config); // Base tools (Tavily, calculator, etc.)
  let connectedApps: string[] = [];

  // Add Composio tools if not incognito and connections exist
  if (!isIncognito && composioConnections?.length > 0) {
    const composioResult = await createComposioTools(ctx, {
      userId,
      connections: composioConnections.filter(c => c.status === "active")
    });

    Object.assign(tools, composioResult.tools); // Merge into tools object
    connectedApps = composioResult.connectedApps; // For system prompt
  }

  return { tools, connectedApps };
}

Auth Config Management

// From convex/composio/oauth.ts
const authConfigCache = new Map<string, string>();

async function getOrCreateAuthConfig(composio: Composio, integrationId: string) {
  // 1. Check cache
  const cached = authConfigCache.get(integrationId);
  if (cached) return cached;

  // 2. Normalize to lowercase (Composio SDK requirement)
  const normalizedToolkit = integrationId.toLowerCase();

  // 3. Try to list existing configs
  try {
    const configs = await composio.authConfigs.list({ toolkit: normalizedToolkit });
    if (configs?.items?.length > 0) {
      const configId = configs.items[0].id;
      authConfigCache.set(integrationId, configId);
      return configId;
    }
  } catch {
    // Config doesn't exist, create one
  }

  // 4. Create auth config (Composio managed)
  const config = await composio.authConfigs.create(normalizedToolkit, {
    name: `blahchat_${normalizedToolkit}`,
    type: "use_composio_managed_auth" // Use Composio's OAuth credentials
  });

  const configId = config.id;
  authConfigCache.set(integrationId, configId);
  return configId;
}

Cache strategy: In-memory Map, no expiration. Auth config IDs stable across restarts. If Composio changes config ID, cache miss creates new config (idempotent).

Integration Limits

// From convex/composio/connections.ts
export const getIntegrationLimits = query({
  handler: async (ctx) => {
    const connections = await ctx.db
      .query("composioConnections")
      .withIndex("by_user", q => q.eq("userId", user._id))
      .collect();

    const activeCount = connections.filter(c => c.status === "active").length;

    // Get max from admin settings (default: 5)
    const adminSettings = await ctx.db.query("adminSettings").first();
    const maxIntegrations = adminSettings?.maxActiveIntegrations ?? 5;

    return {
      current: activeCount,
      max: maxIntegrations,
      canAddMore: activeCount < maxIntegrations
    };
  }
});

Enforcement: Check limit in initiateConnection action BEFORE creating new connection. Skip check for re-auth (existing connection found).

Disconnect Flow

// From convex/composio/oauth.ts
export const revokeConnection = action({
  handler: async (ctx, { integrationId }) => {
    const connection = await ctx.runQuery(
      api.composio.connections.getConnectionByIntegration,
      { integrationId }
    );

    // 1. Delete from Composio (best effort)
    if (process.env.COMPOSIO_API_KEY) {
      try {
        await composio.connectedAccounts.delete(connection.composioConnectionId);
      } catch {
        console.warn(`Failed to delete Composio connection for ${integrationId}`);
        // Continue - still clean up locally
      }
    }

    // 2. Delete local record (always happens)
    await ctx.runMutation(
      api.composio.connections.disconnectIntegration,
      { integrationId }
    );

    return { success: true };
  }
});

Best effort deletion: If Composio API fails, still delete local record. User can re-auth if needed.

Key Files

  • packages/backend/convex/composio/oauth.ts - OAuth flow actions (initiate, verify, refresh, revoke)
  • packages/backend/convex/composio/connections.ts - Connection CRUD queries/mutations
  • packages/backend/convex/composio/tools.ts - Dynamic tool building from active connections
  • packages/backend/convex/generation/tools.ts - Integration with main tool builder

Error Patterns

Expired tokens during tool execution:

// Catch 401/expired errors, mark connection as expired
if (error.message.includes("expired") || error.message.includes("401")) {
  await ctx.runMutation(updateConnectionStatus, {
    composioConnectionId: connection.composioConnectionId,
    status: "expired",
    error: "Token expired - please reconnect"
  });
  throw new Error(`${appName} connection expired. Please reconnect in Settings > Integrations.`);
}

OAuth flow failures:

// Check Composio status, update local status
const connection = await composio.connectedAccounts.get(composioConnectionId);
if (connection.status === "FAILED") {
  await ctx.runMutation(updateConnectionStatus, {
    composioConnectionId,
    status: "failed",
    error: "OAuth flow failed"
  });
}

Avoid

  • Don't skip CSRF state validation (critical security)
  • Don't use UUID for state (use randomBytes for crypto randomness)
  • Don't forget entity ID format: blahchat_${userId} (must match OAuth and tools)
  • Don't lowercase integration ID in DB (only for Composio SDK calls)
  • Don't check limit during re-auth (breaks "Manage" button workflow)
  • Don't fail disconnect if Composio API fails (local cleanup always happens)
  • Don't create tools from non-active connections (status must be "active")

GitHub Repository

majiayu000/claude-skill-registry
Path: skills/composio-oauth-integration

Related Skills

content-collections

Meta

This skill provides a production-tested setup for Content Collections, a TypeScript-first tool that transforms Markdown/MDX files into type-safe data collections with Zod validation. Use it when building blogs, documentation sites, or content-heavy Vite + React applications to ensure type safety and automatic content validation. It covers everything from Vite plugin configuration and MDX compilation to deployment optimization and schema validation.

View skill

creating-opencode-plugins

Meta

This skill provides the structure and API specifications for creating OpenCode plugins that hook into 25+ event types like commands, files, and LSP operations. It offers implementation patterns for JavaScript/TypeScript modules that intercept and extend the AI assistant's lifecycle. Use it when you need to build event-driven plugins for monitoring, custom handling, or extending OpenCode's capabilities.

View skill

langchain

Meta

LangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.

View skill

Algorithmic Art Generation

Meta

This skill helps developers create algorithmic art using p5.js, focusing on generative art, computational aesthetics, and interactive visualizations. It automatically activates for topics like "generative art" or "p5.js visualization" and guides you through creating unique algorithms with features like seeded randomness, flow fields, and particle systems. Use it when you need to build reproducible, code-driven artistic patterns.

View skill