Adds composio-sdk/ with SKILL.md, AGENTS.md, and 18 rule files covering Tool Router, direct execution, triggers, and auth patterns. Source: composiohq/skills Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
477 lines
12 KiB
Markdown
477 lines
12 KiB
Markdown
---
|
|
title: Choose User IDs Carefully for Security and Isolation
|
|
impact: CRITICAL
|
|
description: Use proper user IDs to ensure data isolation, security, and correct session management
|
|
tags: [tool-router, user-id, security, authentication, multi-tenant]
|
|
---
|
|
|
|
# Choose User IDs Carefully for Security and Isolation
|
|
|
|
User IDs are the **foundation of Tool Router isolation**. They determine which user's connections, data, and permissions are used for tool execution. Choose them carefully to ensure security and proper data isolation.
|
|
|
|
## ❌ Incorrect
|
|
|
|
```typescript
|
|
// DON'T: Use 'default' in production multi-user apps
|
|
async function handleUserRequest(req: Request) {
|
|
const session = await composio.create('default', {
|
|
toolkits: ['gmail', 'slack']
|
|
});
|
|
|
|
// ❌ All users share the same session
|
|
// ❌ No data isolation
|
|
// ❌ Security nightmare
|
|
// ❌ User A can access User B's emails!
|
|
}
|
|
```
|
|
|
|
```python
|
|
# DON'T: Use 'default' in production multi-user apps
|
|
async def handle_user_request(req):
|
|
session = composio.tool_router.create(
|
|
user_id="default",
|
|
toolkits=["gmail", "slack"]
|
|
)
|
|
|
|
# ❌ All users share the same session
|
|
# ❌ No data isolation
|
|
# ❌ Security nightmare
|
|
# ❌ User A can access User B's emails!
|
|
```
|
|
|
|
```typescript
|
|
// DON'T: Use email addresses as user IDs
|
|
async function handleUserRequest(req: Request) {
|
|
const session = await composio.create(req.user.email, {
|
|
toolkits: ['github']
|
|
});
|
|
|
|
// ❌ Emails can change
|
|
// ❌ Breaks session continuity
|
|
// ❌ Historical data loss
|
|
}
|
|
```
|
|
|
|
## ✅ Correct - Use Database User IDs
|
|
|
|
```typescript
|
|
// DO: Use your database user ID (UUID, primary key, etc.)
|
|
import { Composio } from '@composio/core';
|
|
import { VercelProvider } from '@composio/vercel';
|
|
|
|
const composio = new Composio({
|
|
provider: new VercelProvider()
|
|
});
|
|
|
|
async function handleUserRequest(req: Request) {
|
|
// Get user ID from your auth system
|
|
const userId = req.user.id; // e.g., "550e8400-e29b-41d4-a716-446655440000"
|
|
|
|
// Create isolated session for this user
|
|
const session = await composio.create(userId, {
|
|
toolkits: ['gmail', 'slack']
|
|
});
|
|
|
|
const tools = await session.tools();
|
|
|
|
// ✅ Each user gets their own session
|
|
// ✅ Complete data isolation
|
|
// ✅ User A cannot access User B's data
|
|
// ✅ Connections tied to correct user
|
|
return await agent.run(req.message, tools);
|
|
}
|
|
```
|
|
|
|
```python
|
|
# DO: Use your database user ID (UUID, primary key, etc.)
|
|
from composio import Composio
|
|
from composio_openai import OpenAIProvider
|
|
|
|
composio = Composio(provider=OpenAIProvider())
|
|
|
|
async def handle_user_request(req):
|
|
# Get user ID from your auth system
|
|
user_id = req.user.id # e.g., "550e8400-e29b-41d4-a716-446655440000"
|
|
|
|
# Create isolated session for this user
|
|
session = composio.tool_router.create(
|
|
user_id=user_id,
|
|
toolkits=["gmail", "slack"]
|
|
)
|
|
|
|
tools = session.tools()
|
|
|
|
# ✅ Each user gets their own session
|
|
# ✅ Complete data isolation
|
|
# ✅ User A cannot access User B's data
|
|
# ✅ Connections tied to correct user
|
|
return await agent.run(req.message, tools)
|
|
```
|
|
|
|
## ✅ Correct - Use Auth Provider IDs
|
|
|
|
```typescript
|
|
// DO: Use IDs from your auth provider
|
|
import { Composio } from '@composio/core';
|
|
import { VercelProvider } from '@composio/vercel';
|
|
|
|
const composio = new Composio({
|
|
provider: new VercelProvider()
|
|
});
|
|
|
|
async function handleClerkUser(userId: string) {
|
|
// Using Clerk user ID
|
|
// e.g., "user_2abc123def456"
|
|
const session = await composio.create(userId, {
|
|
toolkits: ['github']
|
|
});
|
|
|
|
return session;
|
|
}
|
|
|
|
async function handleAuth0User(userId: string) {
|
|
// Using Auth0 user ID
|
|
// e.g., "auth0|507f1f77bcf86cd799439011"
|
|
const session = await composio.create(userId, {
|
|
toolkits: ['gmail']
|
|
});
|
|
|
|
return session;
|
|
}
|
|
|
|
async function handleSupabaseUser(userId: string) {
|
|
// Using Supabase user UUID
|
|
// e.g., "d7f8b0c1-1234-5678-9abc-def012345678"
|
|
const session = await composio.create(userId, {
|
|
toolkits: ['slack']
|
|
});
|
|
|
|
return session;
|
|
}
|
|
```
|
|
|
|
```python
|
|
# DO: Use IDs from your auth provider
|
|
from composio import Composio
|
|
from composio_openai import OpenAIProvider
|
|
|
|
composio = Composio(provider=OpenAIProvider())
|
|
|
|
async def handle_clerk_user(user_id: str):
|
|
# Using Clerk user ID
|
|
# e.g., "user_2abc123def456"
|
|
session = composio.tool_router.create(
|
|
user_id=user_id,
|
|
toolkits=["github"]
|
|
)
|
|
return session
|
|
|
|
async def handle_auth0_user(user_id: str):
|
|
# Using Auth0 user ID
|
|
# e.g., "auth0|507f1f77bcf86cd799439011"
|
|
session = composio.tool_router.create(
|
|
user_id=user_id,
|
|
toolkits=["gmail"]
|
|
)
|
|
return session
|
|
|
|
async def handle_supabase_user(user_id: str):
|
|
# Using Supabase user UUID
|
|
# e.g., "d7f8b0c1-1234-5678-9abc-def012345678"
|
|
session = composio.tool_router.create(
|
|
user_id=user_id,
|
|
toolkits=["slack"]
|
|
)
|
|
return session
|
|
```
|
|
|
|
## ✅ Correct - Organization-Level Applications
|
|
|
|
```typescript
|
|
// DO: Use organization ID for org-level apps
|
|
import { Composio } from '@composio/core';
|
|
import { VercelProvider } from '@composio/vercel';
|
|
|
|
const composio = new Composio({
|
|
provider: new VercelProvider()
|
|
});
|
|
|
|
// When apps are connected at organization level (not individual users)
|
|
async function handleOrgLevelApp(req: Request) {
|
|
// Use organization ID, NOT individual user ID
|
|
const organizationId = req.user.organizationId;
|
|
|
|
const session = await composio.create(organizationId, {
|
|
toolkits: ['slack', 'github'], // Org-wide tools
|
|
manageConnections: true
|
|
});
|
|
|
|
// All users in the organization share these connections
|
|
// Perfect for team collaboration tools
|
|
const tools = await session.tools();
|
|
return await agent.run(req.message, tools);
|
|
}
|
|
|
|
// Example: Slack workspace integration
|
|
async function createWorkspaceSession(workspaceId: string) {
|
|
// Workspace ID as user ID
|
|
const session = await composio.create(`workspace_${workspaceId}`, {
|
|
toolkits: ['slack', 'notion', 'linear']
|
|
});
|
|
|
|
return session;
|
|
}
|
|
```
|
|
|
|
```python
|
|
# DO: Use organization ID for org-level apps
|
|
from composio import Composio
|
|
from composio_openai import OpenAIProvider
|
|
|
|
composio = Composio(provider=OpenAIProvider())
|
|
|
|
# When apps are connected at organization level (not individual users)
|
|
async def handle_org_level_app(req):
|
|
# Use organization ID, NOT individual user ID
|
|
organization_id = req.user.organization_id
|
|
|
|
session = composio.tool_router.create(
|
|
user_id=organization_id,
|
|
toolkits=["slack", "github"], # Org-wide tools
|
|
manage_connections=True
|
|
)
|
|
|
|
# All users in the organization share these connections
|
|
# Perfect for team collaboration tools
|
|
tools = session.tools()
|
|
return await agent.run(req.message, tools)
|
|
|
|
# Example: Slack workspace integration
|
|
async def create_workspace_session(workspace_id: str):
|
|
# Workspace ID as user ID
|
|
session = composio.tool_router.create(
|
|
user_id=f"workspace_{workspace_id}",
|
|
toolkits=["slack", "notion", "linear"]
|
|
)
|
|
return session
|
|
```
|
|
|
|
## When to Use 'default'
|
|
|
|
The `'default'` user ID should **ONLY** be used in these scenarios:
|
|
|
|
### ✅ Development and Testing
|
|
```typescript
|
|
// Testing locally
|
|
const session = await composio.create('default', {
|
|
toolkits: ['gmail']
|
|
});
|
|
```
|
|
|
|
### ✅ Single-User Applications
|
|
```typescript
|
|
// Personal automation script
|
|
// Only YOU use this app
|
|
const session = await composio.create('default', {
|
|
toolkits: ['github', 'notion']
|
|
});
|
|
```
|
|
|
|
### ✅ Demos and Prototypes
|
|
```typescript
|
|
// Quick demo for investors
|
|
const session = await composio.create('default', {
|
|
toolkits: ['hackernews']
|
|
});
|
|
```
|
|
|
|
### ❌ NEVER in Production Multi-User Apps
|
|
```typescript
|
|
// Production API serving multiple users
|
|
// ❌ DON'T DO THIS
|
|
const session = await composio.create('default', {
|
|
toolkits: ['gmail']
|
|
});
|
|
```
|
|
|
|
## User ID Best Practices
|
|
|
|
### 1. **Use Stable, Immutable Identifiers**
|
|
|
|
✅ **Good:**
|
|
- Database primary keys (UUIDs)
|
|
- Auth provider user IDs
|
|
- Immutable user identifiers
|
|
|
|
❌ **Bad:**
|
|
- Email addresses (can change)
|
|
- Usernames (can be modified)
|
|
- Phone numbers (can change)
|
|
|
|
```typescript
|
|
// ✅ Good: Stable UUID
|
|
const userId = user.id; // "550e8400-e29b-41d4-a716-446655440000"
|
|
|
|
// ❌ Bad: Email (mutable)
|
|
const userId = user.email; // "john@example.com" -> changes to "john@newdomain.com"
|
|
|
|
// ❌ Bad: Username (mutable)
|
|
const userId = user.username; // "john_doe" -> changes to "john_smith"
|
|
```
|
|
|
|
### 2. **Ensure Uniqueness**
|
|
|
|
```typescript
|
|
// ✅ Good: Guaranteed unique
|
|
const userId = database.users.findById(id).id;
|
|
|
|
// ✅ Good: Auth provider guarantees uniqueness
|
|
const userId = auth0.user.sub; // "auth0|507f1f77bcf86cd799439011"
|
|
|
|
// ❌ Bad: Not guaranteed unique
|
|
const userId = user.firstName; // Multiple "John"s exist
|
|
```
|
|
|
|
### 3. **Match Your Authentication System**
|
|
|
|
```typescript
|
|
// Express.js with Passport
|
|
app.post('/api/agent', authenticateUser, async (req, res) => {
|
|
const userId = req.user.id; // From Passport
|
|
const session = await composio.create(userId, config);
|
|
});
|
|
|
|
// Next.js with Clerk
|
|
export async function POST(req: NextRequest) {
|
|
const { userId } = auth(); // From Clerk
|
|
const session = await composio.create(userId!, config);
|
|
}
|
|
|
|
// FastAPI with Auth0
|
|
@app.post("/api/agent")
|
|
async def agent_endpoint(user: User = Depends(get_current_user)):
|
|
user_id = user.id # From Auth0
|
|
session = composio.tool_router.create(user_id=user_id, **config)
|
|
```
|
|
|
|
### 4. **Namespace for Multi-Tenancy**
|
|
|
|
```typescript
|
|
// When you have multiple applications/workspaces per user
|
|
const userId = `app_${appId}_user_${user.id}`;
|
|
// e.g., "app_saas123_user_550e8400"
|
|
|
|
const session = await composio.create(userId, {
|
|
toolkits: ['gmail']
|
|
});
|
|
|
|
// Each app instance gets isolated connections
|
|
```
|
|
|
|
### 5. **Be Consistent Across Your Application**
|
|
|
|
```typescript
|
|
// ✅ Good: Same user ID everywhere
|
|
async function handleRequest(req: Request) {
|
|
const userId = req.user.id;
|
|
|
|
// Use same ID for Tool Router
|
|
const session = await composio.create(userId, config);
|
|
|
|
// Use same ID for direct tool execution
|
|
await composio.tools.execute('GMAIL_SEND_EMAIL', {
|
|
userId: userId,
|
|
arguments: { to: 'user@example.com', subject: 'Test' }
|
|
});
|
|
|
|
// Use same ID for connected accounts
|
|
await composio.connectedAccounts.get(userId, 'gmail');
|
|
}
|
|
```
|
|
|
|
## Security Implications
|
|
|
|
### ⚠️ User ID Leakage
|
|
```typescript
|
|
// ❌ DON'T: Expose user IDs to client
|
|
app.get('/api/session', (req, res) => {
|
|
res.json({
|
|
sessionId: session.sessionId,
|
|
userId: req.user.id // ❌ Sensitive information
|
|
});
|
|
});
|
|
|
|
// ✅ DO: Keep user IDs server-side only
|
|
app.get('/api/session', (req, res) => {
|
|
res.json({
|
|
sessionId: session.sessionId
|
|
// Don't send userId to client
|
|
});
|
|
});
|
|
```
|
|
|
|
### ⚠️ User ID Validation
|
|
```typescript
|
|
// ✅ Always validate user IDs match authenticated user
|
|
app.post('/api/agent/:userId', authenticateUser, async (req, res) => {
|
|
const requestedUserId = req.params.userId;
|
|
const authenticatedUserId = req.user.id;
|
|
|
|
// Validate user can only access their own data
|
|
if (requestedUserId !== authenticatedUserId) {
|
|
return res.status(403).json({ error: 'Forbidden' });
|
|
}
|
|
|
|
const session = await composio.create(authenticatedUserId, config);
|
|
});
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### Pattern 1: User-Level Isolation (Most Common)
|
|
```typescript
|
|
// Each user has their own connections
|
|
// Use user ID from your database/auth system
|
|
const session = await composio.create(req.user.id, {
|
|
toolkits: ['gmail', 'github']
|
|
});
|
|
```
|
|
|
|
### Pattern 2: Organization-Level Sharing
|
|
```typescript
|
|
// All org members share connections
|
|
// Use organization ID
|
|
const session = await composio.create(req.user.organizationId, {
|
|
toolkits: ['slack', 'notion']
|
|
});
|
|
```
|
|
|
|
### Pattern 3: Hybrid (User + Org)
|
|
```typescript
|
|
// Personal tools use user ID
|
|
const personalSession = await composio.create(req.user.id, {
|
|
toolkits: ['gmail'] // Personal Gmail
|
|
});
|
|
|
|
// Team tools use org ID
|
|
const teamSession = await composio.create(req.user.organizationId, {
|
|
toolkits: ['slack', 'jira'] // Team Slack/Jira
|
|
});
|
|
```
|
|
|
|
## Key Principles
|
|
|
|
1. **Never use 'default' in production multi-user apps**
|
|
2. **Use stable, immutable identifiers** (UUIDs, not emails)
|
|
3. **Match your authentication system's user IDs**
|
|
4. **Validate user IDs server-side** for security
|
|
5. **Be consistent** across Tool Router and direct tool usage
|
|
6. **Use org IDs** for organization-level applications
|
|
7. **Namespace when needed** for multi-tenancy
|
|
|
|
## Reference
|
|
|
|
- [Tool Router Sessions](https://docs.composio.dev/sdk/typescript/api/tool-router#creating-sessions)
|
|
- [User ID Security](https://docs.composio.dev/sdk/typescript/core-concepts#user-ids)
|
|
- [Connected Accounts](https://docs.composio.dev/sdk/typescript/api/connected-accounts)
|