Merged
Size
L
Change Breakdown
Feature70%
Refactor20%
Security10%
#28333chore: Add scoped JWT strategy for public API (no-changelog)

Public API gains JWT Bearer authentication

Public API gains JWT Bearer authentication

The public API now accepts scoped JWTs from the token exchange endpoint via Bearer tokens, with permission changes taking effect immediately.

The public API previously supported only API key authentication. Now it accepts scoped JWTs issued by the token exchange endpoint, delivered either via Authorization: Bearer headers or the existing x-n8n-api-key header — the same flexibility already available in the main n8n application.

A key architectural decision: scopes are resolved from the acting user's role at request time, not embedded in the JWT payload. This means when an administrator updates permissions, those changes take effect immediately without waiting for tokens to expire and be re-issued. JWTs now carry only identity claims — who the user is and, optionally, who they're acting on behalf of.

The system handles delegation gracefully. If a token carries an actor claim but that user's account has been deleted since issuance, authentication continues with the subject as principal. Only a disabled actor causes authentication to fail.

In the CLI package, the new ScopedJwtStrategy sits behind ApiKeyAuthStrategy in the authentication chain. The API key strategy abstains on token-exchange JWTs — returning null instead of false — so requests pass through to the JWT strategy without being blocked.

View Original GitHub Description

PR Summary

https://linear.app/n8n/issue/IAM-469

What this PR does

Implements ScopedJwtStrategy — the auth strategy that validates scoped JWTs issued by the token exchange endpoint and authenticates public API requests with them.

Scoped JWTs can be presented in either Authorization: Bearer <token> or x-n8n-api-key headers. The strategy identifies them by their issuer (n8n-token-exchange) and is registered into AuthStrategyRegistry by TokenExchangeModule on startup, making it the second strategy after ApiKeyAuthStrategy.

Changes

ScopedJwtStrategy (new)

  • Extracts tokens from both Authorization: Bearer and x-n8n-api-key headers
  • Performs a cheap decode() + issuer check before the expensive verify() call, so non-token-exchange JWTs pass through as null without signature overhead
  • Resolves subsubject and optionally act.subactor from the DB
  • Loads scopes from the acting user's role (actor ?? subject) — not from the JWT payload — so permission changes take effect immediately without re-issuing tokens
  • Sets req.user to the acting principal (actor if delegation is present, subject otherwise)
  • Sets req.tokenGrant = { scopes, subject, actor? }

ApiKeyAuthStrategy (updated)

  • Added subject: apiKeyRecord.user to req.tokenGrantTokenGrant.subject is now required
  • Added early-exit before the DB lookup: if the value in x-n8n-api-key is a JWT with an issuer other than API_KEY_ISSUER, the strategy returns null (abstain) instead of false (fail-fast). Without this, a token-exchange JWT in x-n8n-api-key would short-circuit the auth chain before ScopedJwtStrategy runs

TokenExchangeModule (updated)

  • Registers ScopedJwtStrategy into AuthStrategyRegistry at the end of init(), behind the existing feature flag guard

Key decisions

Scopes from role, not JWT payload. Scopes are resolved at request time from the acting user's role.scopes rather than from the scope claim embedded in the JWT. This means permission changes take effect immediately and the JWT itself only needs to carry identity claims (sub, act).

Actor is optional. If the JWT carries an act claim but the actor user ID is not found in the DB (e.g. the account was deleted after the token was issued), authentication continues with the subject acting as principal. Only if the actor is found and disabled does authentication fail. This keeps token-exchange tokens usable across user lifecycle events.

ApiKeyAuthStrategy abstains on non-API-key JWTs. The issuer check (decoded.iss !== API_KEY_ISSUER) is placed before the DB lookup so a token-exchange JWT in x-n8n-api-key gets null (pass through) rather than false (block). A null decode (non-JWT string) still returns false to preserve existing rejection behaviour for garbage values.

Module self-registration (Option B). ScopedJwtStrategy is registered by TokenExchangeModule.init() rather than in the public API bootstrap. This keeps the token-exchange module self-contained and ensures the strategy is only active when the module is enabled.

Review / Merge checklist

  • I have seen this code, I have run this code, and I take responsibility for this code.
  • PR title and summary are descriptive. (conventions) <!-- **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** -->
  • Docs updated or follow-up ticket created.
  • Tests included. <!-- A bug is not considered fixed, unless a test is added to prevent it from happening again. A feature is not complete without tests. -->
  • PR Labeled with Backport to Beta, Backport to Stable, or Backport to v1 (if the PR is an urgent fix that needs to be backported)
© 2026 · via Gitpulse