
Protect Endpoints
FreeSecure your REST endpoints with RBAC decorators.
Free · Opens the source repo
What Protect Endpoints does
The Protect Endpoints skill is designed for developers working with n8n who need to enforce role-based access control (RBAC) on their REST endpoints. This skill ensures that every authenticated route in a @RestController is secured with the appropriate access-scope decorators, specifically @ProjectScope or @GlobalScope. This is crucial for preventing unauthorized access and ensuring that permissions are correctly applied based on user roles and project associations.
When implementing a new REST route or reviewing existing ones, developers must apply these decorators to maintain security standards. The skill provides clear rules on how to determine which decorator to use based on the presence of a :projectId in the URL. If the route is associated with a project, @ProjectScope is used; if not, @GlobalScope is applied. This helps in creating a robust authorization structure that can adapt to different access requirements.
Additionally, the skill includes guidance for scenarios where the necessary scope does not yet exist, detailing the steps to add new resources and operations to the n8n permissions system. This ensures that developers can extend their authorization model as needed without compromising security. The skill also emphasizes the importance of documenting any public or unauthenticated routes, ensuring that they are clearly marked and understood by future developers.
Overall, this skill is essential for n8n developers who want to implement best practices in securing their REST APIs, making it easier to manage user permissions and maintain application integrity.
When to use it
Use this skill when creating or modifying REST endpoints in n8n to ensure they are properly secured with access controls.
When not to use it
This skill may not be suitable for applications that do not require strict access control or for endpoints that are intentionally left public without authentication.
What you can build with it
Creating a New REST Endpoint
When adding a new REST endpoint, apply the appropriate RBAC decorator to ensure it is secured from unauthorized access.
Reviewing Existing Endpoints
Use this skill to audit existing routes in your n8n application, verifying that each authenticated route has the necessary access controls.
Defining New Access Scopes
When introducing new resources, follow the skill's guidelines to properly add and configure new access scopes in the n8n permissions system.
How to install Protect Endpoints
View source1. Install with the skills CLI
npx skills add n8n-io/n8n/protect-endpoints --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by n8n-ioProtect REST endpoints with RBAC
Rule: every authenticated route on a @RestController MUST carry an access-scope decorator. If you add a route without one, the IDOR/permission bypass is on you.
Decision
URL has :projectId → @ProjectScope('<resource>:<op>')
URL has no project → @GlobalScope('<resource>:<op>')
skipAuth: true → no decorator + comment explaining alternate auth
@ProjectScope succeeds if the user has the scope globally OR in the project named in the URL. @GlobalScope ignores project relations entirely.
Both decorators come from @n8n/decorators. The middleware lives in packages/cli/src/controller.registry.ts (createScopedMiddleware) and resolves access via userHasScopes in packages/cli/src/permissions.ee/check-access.ts.
Apply the decorator
import { Get, Post, ProjectScope, RestController } from '@n8n/decorators';
@RestController('/projects/:projectId/widgets')
export class WidgetsController {
@Post('/')
@ProjectScope('widget:create') // create
async create(...) { ... }
@Get('/:widgetId')
@ProjectScope('widget:read') // read one
async get(...) { ... }
@Get('/')
@ProjectScope('widget:list') // list
async list(...) { ... }
@Patch('/:widgetId')
@ProjectScope('widget:update') // update
async update(...) { ... }
@Delete('/:widgetId')
@ProjectScope('widget:delete') // delete
async delete(...) { ... }
}
Conventions:
- One decorator per route, placed directly under the HTTP-method decorator.
- Use the most specific scope that fits. Reuse
*:updatefor state-changing actions likepublish/unpublish/buildunless the resource needs to gate them separately (seeworkflow:publishfor the precedent). - Routes without
:projectIdand not global-only operations are usually a design smell — flag it.
When the scope doesn't exist yet
Add the resource and ops in packages/@n8n/permissions/:
src/constants.ee.ts— add toRESOURCES(alphabetical):
Thewidget: [...DEFAULT_OPERATIONS, 'execute'] as const,Scopeunion (<resource>:<op>template-literal type) auto-derives.src/scope-information.ts— add a display name + description per scope.src/roles/scopes/project-scopes.ee.ts— add to project roles. Match theworkflowprecedent unless product says otherwise:REGULAR_PROJECT_ADMIN_SCOPES,PERSONAL_PROJECT_OWNER_SCOPES,PROJECT_EDITOR_SCOPES→ all CRUDL+execute scopes.PROJECT_VIEWER_SCOPES→ read/list/execute only.PROJECT_CHAT_USER_SCOPES→ execute only (if applicable).
src/roles/scopes/global-scopes.ee.ts— add toGLOBAL_OWNER_SCOPES(admin inherits viaconcat()). Do not add to member/chat-user globals — they get scopes via project relations.- Personal-space publishing: if you add a
<resource>:publishscope, also append it toPERSONAL_SPACE_PUBLISHING_SETTING.scopesinconstants.ee.tsso personal-owner gating matchesworkflow:publish. src/roles/custom-role-scopes.ee.ts— add the resource toPROJECT_CUSTOM_ROLE_OPERATIONSwith the ops to render in the permissions matrix, in display order. The editor'sSCOPES/SCOPE_TYPESand the save-time whitelistPROJECT_CUSTOM_ROLE_SCOPESboth derive from it: a resource missing here cannot reach the UI, and a scope missing from it is rejected on save.- Frontend wiring — three files; skipping any of them means the new scopes will not appear in the project-role configuration UI:
packages/frontend/@n8n/stores/src/rbac.store.ts— add<resource>: {}toscopesByResourceId(typecheck will fail otherwise).packages/frontend/editor-ui/src/features/roles/project/projectRoleScopes.ts— add the resource toSCOPE_TYPES(the order the resource group appears on the page).packages/frontend/@n8n/i18n/src/locales/en.json— addprojectRoles.<resource>:<op>(column label) andprojectRoles.<resource>:<op>.tooltip(hover description) for every op, plusprojectRoles.type.<resource>(the group header).
- Snapshot — update
packages/@n8n/permissions/src/__tests__/__snapshots__/scope-information.test.ts.snapto include the new<resource>:*entries.
No DB migration needed — AuthRolesService.init() syncs scopes/roles on every startup. Custom team roles created in the UI are not auto-updated; mention this in the PR description.
Public / unauthenticated routes
{ skipAuth: true } skips the auth middleware → req.user is undefined → adding @ProjectScope would 401 every call. Public routes (third-party webhooks, signed callbacks) must:
- Omit the scope decorator.
- Authenticate via signature/HMAC verification inside the handler (or another route-specific mechanism).
- Carry a comment explaining why no scope is applied, so the next reviewer doesn't try to "fix" it.
Example:
// Third-party webhook callback: do not add @ProjectScope. Auth happens
// via per-platform signature verification inside webhookHandler, and
// :projectId is unused in the (agentId, platform) lookup.
@Post('/:agentId/webhooks/:platform', { skipAuth: true, allowBots: true })
async handleWebhook(...) { ... }
Verify with a route-metadata test
Add a regression test that fails when a future route is added without a scope. Iterate every route on the controller via ControllerRegistryMetadata and assert the gate.
import { ControllerRegistryMetadata } from '@n8n/decorators';
import { Container } from '@n8n/di';
import { WidgetsController } from '../widgets.controller';
const UNAUTHENTICATED_HANDLERS = new Set<string>(); // add public handler names here
const metadata = Container.get(ControllerRegistryMetadata).getControllerMetadata(
WidgetsController as never,
);
const routeCases = Array.from(metadata.routes.entries()).map(([handlerName, route]) => ({
handlerName, route,
}));
describe('WidgetsController route access scopes', () => {
it.each(routeCases)(
'$handlerName is gated by a project-scoped widget:* check',
({ handlerName, route }) => {
if (UNAUTHENTICATED_HANDLERS.has(handlerName)) {
expect(route.accessScope).toBeUndefined();
expect(route.skipAuth).toBe(true);
return;
}
expect(route.accessScope).toBeDefined();
expect(route.accessScope?.globalOnly).toBe(false);
expect(route.accessScope?.scope.startsWith('widget:')).toBe(true);
},
);
});
Defense in depth (still required)
Decorator alone is not enough when handlers leak data via downstream calls. Service/repository methods should still filter by projectId (or user-scoped helpers like findByUser). The decorator gates who can call this URL; the service gates what they can read. Both, always.
Reference patterns
- Project-scoped CRUD:
packages/cli/src/workflows/workflows.controller.ts,packages/cli/src/credentials/credentials.controller.ts,packages/cli/src/modules/data-table/data-table.controller.ts. - Mixed global + project:
packages/cli/src/controllers/project.controller.ts.
Frequently asked questions about Protect Endpoints
Similar skills
WinMD API Search
Easily find and explore Windows desktop APIs.
WebMCPify
Transform any web app into an agent-ready platform.
Phoenix Tracing
Instrument LLM applications with OpenInference tracing.
Foundry Hosted Agent CopilotKit
Guidance for developing agentic web apps on Azure.
Power Automate Foundation
Connect AI agents to Power Automate seamlessly.
Power Automate Flow Builder
Efficiently build and deploy Power Automate flows programmatically.
