Claude-code-plugins-plus-skills webflow-enterprise-rbac
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/webflow-pack/skills/webflow-enterprise-rbac" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-webflow-enterprise-rbac && rm -rf "$T"
manifest:
plugins/saas-packs/webflow-pack/skills/webflow-enterprise-rbac/SKILL.mdsource content
Webflow Enterprise RBAC
Overview
Enterprise-grade access control for Webflow Data API v2 integrations. Uses Webflow's OAuth 2.0 scope system, per-site token isolation, and application-level RBAC to enforce least privilege across teams and environments.
Prerequisites
- Webflow workspace (Core plan or higher for multiple members)
- OAuth 2.0 Data Client App (for user-authorized access)
- Understanding of Webflow scopes
Webflow's Native Access Model
Webflow provides three levels of access control:
| Level | Mechanism | Granularity |
|---|---|---|
| Workspace tokens | API token | All sites in workspace |
| Site tokens | API token | Single site |
| OAuth tokens | OAuth 2.0 authorization | User-authorized scopes per site |
Scope-Based Permission Model
Webflow scopes map directly to API access:
| Scope | Read Access | Write Access |
|---|---|---|
| List/get sites | — |
| — | Publish sites |
| List collections, read items | — |
| — | Create/update/delete CMS items |
| List/get pages | — |
| — | Update page settings |
| List forms, read submissions | — |
| List products, orders, inventory | — |
| — | Create products, fulfill orders |
| List registered scripts | — |
| — | Register/apply custom code |
Instructions
Step 1: Define Application Roles
Map your application roles to Webflow scope sets:
enum AppRole { ContentViewer = "content_viewer", ContentEditor = "content_editor", SiteAdmin = "site_admin", EcommerceManager = "ecommerce_manager", FormProcessor = "form_processor", } // Map roles to required Webflow OAuth scopes const ROLE_SCOPES: Record<AppRole, string[]> = { [AppRole.ContentViewer]: ["sites:read", "cms:read", "pages:read"], [AppRole.ContentEditor]: ["sites:read", "cms:read", "cms:write", "pages:read"], [AppRole.SiteAdmin]: [ "sites:read", "sites:write", "cms:read", "cms:write", "pages:read", "pages:write", "custom_code:read", "custom_code:write", ], [AppRole.EcommerceManager]: [ "sites:read", "ecommerce:read", "ecommerce:write", "cms:read", // Products are CMS collections ], [AppRole.FormProcessor]: ["sites:read", "forms:read"], };
Step 2: OAuth App with Role-Based Scopes
Request only the scopes needed for the user's role:
function getAuthorizationUrl(role: AppRole, state: string): string { const scopes = ROLE_SCOPES[role]; const scopeString = scopes.join(" "); return ( `https://webflow.com/oauth/authorize` + `?client_id=${process.env.WEBFLOW_CLIENT_ID}` + `&response_type=code` + `&redirect_uri=${encodeURIComponent(process.env.REDIRECT_URI!)}` + `&scope=${encodeURIComponent(scopeString)}` + `&state=${state}` // CSRF protection ); } // Initiate OAuth flow with role-appropriate scopes app.get("/auth/webflow", (req, res) => { const role = req.user.appRole as AppRole; const state = generateCsrfToken(req.session.id); res.redirect(getAuthorizationUrl(role, state)); });
Step 3: Token Storage with Role Metadata
interface StoredToken { accessToken: string; userId: string; role: AppRole; scopes: string[]; siteIds: string[]; // Which sites this token can access createdAt: Date; lastUsed: Date; } async function storeToken(token: StoredToken): Promise<void> { // Encrypt token before storing const encrypted = encrypt(token.accessToken); await db.webflowTokens.upsert({ where: { userId: token.userId }, create: { ...token, accessToken: encrypted, }, update: { accessToken: encrypted, lastUsed: new Date(), }, }); }
Step 4: Per-Site Token Isolation
Use site-scoped tokens to limit blast radius:
class SiteIsolatedClient { private clients = new Map<string, WebflowClient>(); // Each site gets its own token — compromise of one doesn't affect others registerSite(siteId: string, siteToken: string): void { this.clients.set(siteId, new WebflowClient({ accessToken: siteToken })); } getClient(siteId: string): WebflowClient { const client = this.clients.get(siteId); if (!client) { throw new Error(`No token registered for site ${siteId}`); } return client; } // Rotate a single site's token without affecting others rotateToken(siteId: string, newToken: string): void { this.clients.set(siteId, new WebflowClient({ accessToken: newToken })); console.log(`Token rotated for site ${siteId}`); } } const siteClients = new SiteIsolatedClient(); siteClients.registerSite("site-prod", process.env.WEBFLOW_TOKEN_PROD!); siteClients.registerSite("site-staging", process.env.WEBFLOW_TOKEN_STAGING!);
Step 5: Permission Check Middleware
import { Request, Response, NextFunction } from "express"; function requireWebflowScope(requiredScopes: string[]) { return async (req: Request, res: Response, next: NextFunction) => { const userToken = await db.webflowTokens.findByUserId(req.user.id); if (!userToken) { return res.status(401).json({ error: "No Webflow authorization" }); } // Check if user's token has the required scopes const missingScopes = requiredScopes.filter( s => !userToken.scopes.includes(s) ); if (missingScopes.length > 0) { return res.status(403).json({ error: "Insufficient Webflow permissions", missing: missingScopes, userRole: userToken.role, hint: `Role "${userToken.role}" does not include: ${missingScopes.join(", ")}`, }); } next(); }; } // Usage app.post( "/api/cms/items", requireWebflowScope(["cms:write"]), createItemHandler ); app.get( "/api/forms/submissions", requireWebflowScope(["forms:read"]), listSubmissionsHandler ); app.post( "/api/site/publish", requireWebflowScope(["sites:write"]), publishSiteHandler );
Step 6: Audit Logging
interface WebflowAuditEntry { timestamp: Date; userId: string; role: AppRole; operation: string; siteId: string; resourceType: string; resourceId?: string; result: "success" | "denied" | "error"; scopes: string[]; ipAddress: string; } async function auditWebflowAccess(entry: WebflowAuditEntry): Promise<void> { // Write to audit log (never delete audit entries) await db.auditLog.create({ data: { ...entry, timestamp: entry.timestamp.toISOString(), }, }); // Alert on suspicious patterns if (entry.result === "denied") { console.warn(`ACCESS DENIED: ${entry.userId} attempted ${entry.operation} on ${entry.resourceType}`); } // Alert on admin operations if (entry.role === AppRole.SiteAdmin && entry.operation.includes("publish")) { console.log(`ADMIN ACTION: ${entry.userId} published site ${entry.siteId}`); } } // Wrap API calls with audit logging async function auditedCall<T>( entry: Omit<WebflowAuditEntry, "timestamp" | "result">, operation: () => Promise<T> ): Promise<T> { try { const result = await operation(); await auditWebflowAccess({ ...entry, timestamp: new Date(), result: "success" }); return result; } catch (error) { await auditWebflowAccess({ ...entry, timestamp: new Date(), result: "error" }); throw error; } }
Step 7: Token Rotation Schedule
async function checkTokenAge(): Promise<void> { const tokens = await db.webflowTokens.findMany(); for (const token of tokens) { const ageInDays = (Date.now() - token.createdAt.getTime()) / (1000 * 60 * 60 * 24); if (ageInDays > 90) { console.warn( `Token for user ${token.userId} (${token.role}) is ${Math.floor(ageInDays)} days old. ` + `Rotation recommended.` ); // Send notification to user/admin } } } // Run weekly // cron: "0 9 * * 1"
Output
- Role-to-scope mapping for Webflow OAuth
- OAuth authorization with role-appropriate scopes
- Per-site token isolation
- Permission check middleware for API endpoints
- Comprehensive audit logging
- Token age monitoring and rotation alerts
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| OAuth scope mismatch | App requests more scopes than configured | Match app settings in Webflow dashboard |
| 403 on API call | Token missing required scope | Re-authorize with correct role |
| Token not found | User never completed OAuth flow | Redirect to authorization |
| Audit gap | Error in async logging | Add fallback logging to console |
Resources
Next Steps
For major migrations, see
webflow-migration-deep-dive.