chore: Add tokengrant for JWT permission context (no-changelog)#28295
Merged
phyllis-noester merged 2 commits intomasterfrom Apr 10, 2026
Merged
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Contributor
There was a problem hiding this comment.
13 issues found across 17 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts">
<violation number="1" location="packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts:29">
P0: Custom agent: **Security Review**
According to linked Linear issue IAM-467, these workflow routes must enforce `tokenGrant.resource` as well as `tokenGrant.scopes`. Switching them to `publicApiScope()` introduces a resource-level authorization bypass: a token scoped to project X can still operate on workflows from another project if the backing user has access.</violation>
</file>
<file name="packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts">
<violation number="1" location="packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts:111">
P1: According to linked Linear issue IAM-467, missing `tokenGrant` should preserve the previous scope-check path instead of returning `403` immediately.</violation>
<violation number="2" location="packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts:116">
P0: Custom agent: **Security Review**
Security Review / Authorization & Access Control: according to linked Linear issue IAM-467, token grants with a `resource` constraint must also validate the requested resource. This middleware only checks `tokenGrant.scopes`, so a token scoped to project X can still access project Y.</violation>
</file>
<file name="packages/cli/src/public-api/v1/handlers/credentials/credentials.handler.ts">
<violation number="1" location="packages/cli/src/public-api/v1/handlers/credentials/credentials.handler.ts:116">
P0: Custom agent: **Security Review**
According to linked Linear issue IAM-467, these credential write routes must enforce `tokenGrant.resource` as well as `tokenGrant.scopes`. Using `publicApiScope` here only checks scopes, so a JWT scoped to project X can still update, move, or delete a credential in project Y if `req.user` has broader access.</violation>
<violation number="2" location="packages/cli/src/public-api/v1/handlers/credentials/credentials.handler.ts:161">
P1: According to linked Linear issue IAM-467, this route still ignores token `resource` constraints, so a project-scoped grant can move credentials across projects as long as the backing user has access.</violation>
</file>
<file name="packages/cli/src/services/api-key-auth.strategy.ts">
<violation number="1" location="packages/cli/src/services/api-key-auth.strategy.ts:46">
P0: Custom agent: **Security Review**
According to linked Linear issue IAM-467, this scopes-only token grant strips the JWT `resource` constraint, so `publicApiScope` cannot enforce project/resource boundaries. Populate `req.tokenGrant` from the verified JWT claims, including `resource`, instead of replacing it with only persisted scopes.</violation>
</file>
<file name="packages/cli/src/public-api/v1/handlers/community-packages/community-packages.handler.ts">
<violation number="1" location="packages/cli/src/public-api/v1/handlers/community-packages/community-packages.handler.ts:17">
P0: Custom agent: **Security Review**
According to linked Linear issue IAM-467, token-grant enforcement must reject requests whose target does not match `tokenGrant.resource`. These routes now use `publicApiScope`, but that middleware only checks scopes, so a resource-constrained JWT can still perform global community-package operations.</violation>
</file>
<file name="packages/cli/src/public-api/v1/__tests__/global.middleware.test.ts">
<violation number="1" location="packages/cli/src/public-api/v1/__tests__/global.middleware.test.ts:40">
P0: Custom agent: **Security Review**
According to linked Linear issue IAM-467, token grants must enforce their `resource` constraint as well as scopes. This change only validates `tokenGrant.scopes`, so a token scoped to project X can still be used against project Y.</violation>
</file>
<file name="packages/@n8n/db/src/entities/types-db.ts">
<violation number="1" location="packages/@n8n/db/src/entities/types-db.ts:421">
P0: Custom agent: **Security Review**
According to linked Linear issue IAM-467, `TokenGrant` must carry an optional `resource` constraint. Omitting it here removes the request-level field needed to enforce project/resource boundaries for scoped tokens.</violation>
</file>
<file name="packages/cli/src/public-api/v1/handlers/insights/insights.handler.ts">
<violation number="1" location="packages/cli/src/public-api/v1/handlers/insights/insights.handler.ts:36">
P0: Custom agent: **Security Review**
According to linked Linear issue IAM-467, token grants on project-scoped routes must reject requests whose target project does not match the token resource. This endpoint now uses `publicApiScope('insights:read')`, but that middleware only checks `tokenGrant.scopes` and this handler still forwards any `query.data.projectId`, so a token scoped to project X can read project Y insights.</violation>
</file>
<file name="packages/cli/src/public-api/v1/handlers/data-tables/data-tables.handler.ts">
<violation number="1" location="packages/cli/src/public-api/v1/handlers/data-tables/data-tables.handler.ts:72">
P0: Custom agent: **Security Review**
According to linked Linear issue IAM-467, these data-table routes must enforce `tokenGrant.resource` as well as `tokenGrant.scopes`. Swapping to `publicApiScope()` here authorizes the token's scope but still lets requests act on any project the underlying user can access, so a token constrained to project X can list/create/read/update/delete data tables in project Y.</violation>
</file>
<file name="packages/cli/src/public-api/v1/handlers/data-tables/data-tables.rows.handler.ts">
<violation number="1" location="packages/cli/src/public-api/v1/handlers/data-tables/data-tables.rows.handler.ts:69">
P0: Custom agent: **Security Review**
According to linked Linear issue IAM-467, this route must enforce `tokenGrant.resource` on token-grant requests. Replacing `apiKeyHasScope` with `publicApiScope` here enables scope-only checks on a resource-scoped data-table endpoint, so a token for project X can authorize row access in project Y.</violation>
</file>
<file name="packages/cli/src/public-api/v1/handlers/executions/executions.handler.ts">
<violation number="1" location="packages/cli/src/public-api/v1/handlers/executions/executions.handler.ts:37">
P1: According to linked Linear issue IAM-467, these execution routes must reject token grants constrained to a different resource, but `publicApiScope()` only checks `tokenGrant.scopes`. A token scoped to project X can still reach execution endpoints for project Y if the user otherwise has access.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Client
participant Auth as ApiKeyAuthStrategy
participant DB as ApiKeyRepository
participant MW as publicApiScope Middleware
participant Handler as API Route Handler
Note over Client,Handler: Public API Request Flow (Scoped JWT/API Key)
Client->>Auth: Request with x-n8n-api-key
Auth->>DB: CHANGED: findOne() via ApiKeyRepository (Direct lookup)
DB-->>Auth: apiKeyRecord (includes User and Scopes)
alt API Key invalid or User disabled
Auth-->>Client: 401 Unauthorized / 403 Forbidden
else Valid Credentials
Auth->>Auth: NEW: Extract scopes from record
Auth->>Auth: NEW: Attach tokenGrant to Request object
Note right of Auth: req.tokenGrant = { scopes: [...] }
Auth-->>MW: next()
end
Note over MW: Enforcement Phase
alt NEW: req.tokenGrant is missing
MW-->>Client: 403 Forbidden
else tokenGrant present
MW->>MW: CHANGED: Check req.tokenGrant.scopes (No DB hit)
alt Required scope missing
MW-->>Client: 403 Forbidden
else Required scope present
MW-->>Handler: next()
Handler-->>Client: 200 OK + Data
end
end
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.
packages/cli/src/public-api/v1/handlers/credentials/credentials.handler.ts
Show resolved
Hide resolved
packages/cli/src/public-api/v1/handlers/community-packages/community-packages.handler.ts
Show resolved
Hide resolved
packages/cli/src/public-api/v1/handlers/data-tables/data-tables.handler.ts
Show resolved
Hide resolved
packages/cli/src/public-api/v1/handlers/data-tables/data-tables.rows.handler.ts
Show resolved
Hide resolved
packages/cli/src/public-api/v1/handlers/executions/executions.handler.ts
Show resolved
Hide resolved
packages/cli/src/public-api/v1/handlers/credentials/credentials.handler.ts
Show resolved
Hide resolved
phyllis-noester
commented
Apr 10, 2026
| if (typeof providedApiKey !== 'string' || !providedApiKey) return null; | ||
|
|
||
| const user = await this.userRepository.findOne({ | ||
| where: { |
Contributor
Author
There was a problem hiding this comment.
I'm not sure this performance improvement e(not doing two separate calls) it worth it. In theory, it should result in the same result and I tested the behavior but also not worth it to break the public API for people in case there is an edge case I didn't consider
afitzek
approved these changes
Apr 10, 2026
Contributor
afitzek
left a comment
There was a problem hiding this comment.
Looks good, and tested manually 🚀
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
IAM-467: TokenGrant type and scope enforcement changes
What was done
1. Added the
TokenGrantinterface (packages/@n8n/db/src/entities/types-db.ts)A new interface attached to
AuthenticatedRequestthat carries the permission context from a scoped JWT:rolesis optional onTokenGrant. Theresourcefield from the ticket spec wasdeliberately omitted — resource constraint enforcement is out of scope for this slice.
2. Rewired
ApiKeyAuthStrategyto populatereq.tokenGrantThe strategy now queries
ApiKeyRepositorydirectly (instead ofUserRepositoryviarelation join), and after successful auth sets:
Scopes are resolved once at auth time and stored on the request, rather than
re-checking the raw API key in the scope middleware.
3. Replaced
apiKeyHasScopewithpublicApiScopeinglobal.middleware.tsThe old
getApiKeyScopeMiddlewarere-fetched the API key from the DB on every scopecheck — a redundant DB hit after auth. The new
makeScopeEnforcementMiddlewaresimplyreads
req.tokenGrant.scopes. If notokenGrantis present, it returns 403 immediately.4. Removed
getApiKeyScopeMiddlewarefromPublicApiKeyService— dead code oncescope enforcement moved to reading from the request.
Design decisions
rolespreserved but not enforcedscopesarray — avoids re-doing role→scope resolution on every request.resourcefield omittedtokenGrantabsentpublicApiScope, this will need revisiting.tokenGrantis optional onAuthenticatedRequest; existing session-based routes that don't go throughpublicApiScopeare entirely unaffected.ApiKeyRepositorydirectlyUserRepositoryto reach the API key. Reversing the join (ApiKey → User) gives direct access toapiKeyRecord.scopeswithout an extra query.Related Linear tickets, Github issues, and Community forum posts
[
](https://linear.app/n8n/issue/IAM-467/2b3-tokengrant-type-and-scope-enforcement-changes)
Manual API testing
Three curl calls against
http://localhost:5678to verify the full auth path end-to-end.Replace
<API_KEY>with a valid scoped JWT API key.1. GET /api/v1/workflows — scope gate + role-based result filtering
Exercises
publicApiScope('workflow:list')(readsreq.tokenGrant.scopes) and the role branch in the handler (req.user.role.slugatworkflows.handler.ts:158). If the role relation was not loaded by the newApiKeyRepositoryjoin, this call would 500.Expected:
{ "count": N, "firstWorkflow": "..." }2. POST /api/v1/users — user creation through
apiKeyHasScopeWithGlobalScopeFallbackExercises the scope path that now also reads from
req.tokenGrantrather than re-fetching the API key from the DB.Expected: array with the created user, or
{ "message": "User already exists" }on re-run.3. GET /api/v1/credentials — direct
req.user.role.slugcheck in handler bodyAt
credentials.handler.ts:185the handler readsreq.user.role.slugto decide whether to return all credentials or only the caller's own. This is the most targeted test thatrelations: { user: { role: true } }in the newApiKeyRepositoryquery actually populates the role.Expected:
{ "count": N, "role_check": "passed - role was loaded" }— a 500 here meansreq.user.rolewas null.Review / Merge checklist
Backport to Beta,Backport to Stable, orBackport to v1(if the PR is an urgent fix that needs to be backported)