intercom-enterprise-rbac

'Configure Intercom enterprise OAuth, admin roles, and app-level access

3 Tools
intercom-pack Plugin
saas packs Category

Allowed Tools

ReadWriteEdit

Provided by Plugin

intercom-pack

Claude Code skill pack for Intercom (24 skills)

saas packs v1.0.0
View Plugin

Installation

This skill is included in the intercom-pack plugin:

/plugin install intercom-pack@claude-code-plugins-plus

Click to copy

Instructions

Intercom Enterprise RBAC

Overview

Configure enterprise-grade access control for Intercom integrations using OAuth scopes, admin role management, and app-level permission enforcement.

Prerequisites

  • Intercom workspace with admin access
  • Understanding of OAuth 2.0 flows
  • For public apps: OAuth configured in Developer Hub

Intercom Admin Roles

Intercom has built-in admin roles that control workspace access:

Role API Access Capabilities
Owner Full All operations, billing, workspace settings
Admin Full Manage contacts, conversations, content
Agent Limited Reply to conversations, view contacts
Custom roles Configurable Enterprise plan feature

Step 1: List Admins and Roles


import { IntercomClient } from "intercom-client";

const client = new IntercomClient({
  token: process.env.INTERCOM_ACCESS_TOKEN!,
});

// List all admins in the workspace
const adminList = await client.admins.list();
for (const admin of adminList.admins) {
  console.log(`${admin.name} (${admin.email})`);
  console.log(`  ID: ${admin.id}`);
  console.log(`  Type: ${admin.type}`);      // "admin" or "team"
  console.log(`  Active: ${admin.awayModeEnabled ? "Away" : "Available"}`);
}

// Find a specific admin by ID
const admin = await client.admins.find({ adminId: "12345" });
console.log(`Admin: ${admin.name} - ${admin.email}`);

Instructions

Step 2: OAuth Scope-Based Access Control

For public apps (OAuth), scopes control what your app can access in a customer's workspace.


// OAuth configuration
const OAUTH_CONFIG = {
  clientId: process.env.INTERCOM_CLIENT_ID!,
  clientSecret: process.env.INTERCOM_CLIENT_SECRET!,
  redirectUri: "https://your-app.com/auth/intercom/callback",
};

// Step 1: Build authorization URL with minimal scopes
function getAuthUrl(state: string): string {
  return `https://app.intercom.com/oauth?` +
    `client_id=${OAUTH_CONFIG.clientId}&` +
    `state=${state}&` +
    `redirect_uri=${encodeURIComponent(OAUTH_CONFIG.redirectUri)}`;
}

// Step 2: Exchange authorization code for token
async function exchangeCode(code: string): Promise<{
  token: string;
  tokenType: string;
}> {
  const response = await fetch("https://api.intercom.io/auth/eagle/token", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      client_id: OAUTH_CONFIG.clientId,
      client_secret: OAUTH_CONFIG.clientSecret,
      code,
    }),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`OAuth token exchange failed: ${error.message}`);
  }

  const data = await response.json();
  return { token: data.token, tokenType: data.token_type };
}

// Step 3: Store token per workspace for multi-tenant
interface WorkspaceAuth {
  workspaceId: string;
  token: string;
  installedAt: Date;
  installedBy: string; // admin email
}

// Usage
const authUrl = getAuthUrl(crypto.randomUUID());
// Redirect user to authUrl
// On callback, exchange code for token

Step 3: App-Level Permission Enforcement

Enforce permissions at the application layer based on the current admin.


// Define permission levels for your app's operations
type IntercomPermission =
  | "contacts:read"
  | "contacts:write"
  | "contacts:delete"
  | "conversations:read"
  | "conversations:reply"
  | "conversations:assign"
  | "conversations:close"
  | "articles:read"
  | "articles:write"
  | "settings:manage";

// Map admin types to permissions
const ROLE_PERMISSIONS: Record<string, Set<IntercomPermission>> = {
  owner: new Set([
    "contacts:read", "contacts:write", "contacts:delete",
    "conversations:read", "conversations:reply", "conversations:assign", "conversations:close",
    "articles:read", "articles:write", "settings:manage",
  ]),
  admin: new Set([
    "contacts:read", "contacts:write",
    "conversations:read", "conversations:reply", "conversations:assign", "conversations:close",
    "articles:read", "articles:write",
  ]),
  agent: new Set([
    "contacts:read",
    "conversations:read", "conversations:reply",
  ]),
};

function checkPermission(adminRole: string, permission: IntercomPermission): boolean {
  return ROLE_PERMISSIONS[adminRole]?.has(permission) ?? false;
}

// Express middleware
function requirePermission(permission: IntercomPermission) {
  return (req: any, res: any, next: any) => {
    const adminRole = req.user?.intercomRole || "agent";
    if (!checkPermission(adminRole, permission)) {
      return res.status(403).json({
        error: "Forbidden",
        message: `Missing permission: ${permission}`,
        required: permission,
        currentRole: adminRole,
      });
    }
    next();
  };
}

// Usage
app.delete(
  "/api/contacts/:id",
  requirePermission("contacts:delete"),
  deleteContactHandler
);

app.post(
  "/api/conversations/:id/assign",
  requirePermission("conversations:assign"),
  assignConversationHandler
);

Step 4: Team-Based Conversation Assignment


// List teams in workspace
const admins = await client.admins.list();
const teams = admins.admins.filter(a => a.type === "team");
console.log("Teams:", teams.map(t => `${t.name} (${t.id})`));

// Assign conversation to team based on topic
async function routeConversation(
  conversationId: string,
  adminId: string,
  topic: string
): Promise<void> {
  const teamRouting: Record<string, string> = {
    billing: "team-billing-123",
    technical: "team-engineering-456",
    sales: "team-sales-789",
  };

  const teamId = teamRouting[topic];
  if (teamId) {
    await client.conversations.assign({
      conversationId,
      type: "team",
      adminId,
      assigneeId: teamId,
      body: `Routed to ${topic} team`,
    });
  }
}

Step 5: Audit Logging for Admin Actions


interface AdminAuditEntry {
  timestamp: string;
  adminId: string;
  adminEmail: string;
  action: string;
  resource: string;
  resourceId: string;
  success: boolean;
  ipAddress?: string;
}

async function auditAdminAction(entry: AdminAuditEntry): Promise<void> {
  // Log to your audit database
  await db.auditLog.insert(entry);

  // Track as Intercom data event for visibility
  await client.dataEvents.create({
    eventName: "admin-action-logged",
    createdAt: Math.floor(Date.now() / 1000),
    userId: entry.adminId,
    metadata: {
      action: entry.action,
      resource: entry.resource,
      resource_id: entry.resourceId,
      success: entry.success,
    },
  });

  // Alert on sensitive operations
  if (entry.action.includes("delete") || entry.action.includes("settings")) {
    console.warn(`[AUDIT] Sensitive action: ${entry.action} by ${entry.adminEmail}`);
  }
}

OAuth Scope Reference

Scope Grants
Read admins GET /admins
Read contacts GET /contacts, POST /contacts/search
Write contacts POST /contacts, PUT /contacts/{id}, DELETE /contacts/{id}
Read conversations GET /conversations, GET /conversations/{id}
Write conversations POST /conversations, reply, close, assign
Read messages Read sent messages
Write messages POST /messages
Read articles GET /articles, GET /help_center/collections
Write articles Create, update, delete articles and collections
Read tags GET /tags
Write tags Create, apply, remove tags
Read events GET /events
Write events POST /events

Error Handling

Issue Cause Solution
OAuth callback fails Wrong redirect URI Match exactly in Developer Hub
forbidden (403) Missing OAuth scope Add scope, user must re-authorize
Token revoked User uninstalled app Handle gracefully, notify admin
Admin not found Admin left workspace Remove from your system
Team assignment fails Team ID invalid List teams first with admins.list()

Resources

Next Steps

For major migrations, see intercom-migration-deep-dive.

Ready to use intercom-pack?