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: Bearerandx-n8n-api-keyheaders - Performs a cheap
decode()+ issuer check before the expensiveverify()call, so non-token-exchange JWTs pass through asnullwithout signature overhead - Resolves
sub→subjectand optionallyact.sub→actorfrom 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.userto the acting principal (actor if delegation is present, subject otherwise) - Sets
req.tokenGrant = { scopes, subject, actor? }
ApiKeyAuthStrategy (updated)
- Added
subject: apiKeyRecord.usertoreq.tokenGrant—TokenGrant.subjectis now required - Added early-exit before the DB lookup: if the value in
x-n8n-api-keyis a JWT with an issuer other thanAPI_KEY_ISSUER, the strategy returnsnull(abstain) instead offalse(fail-fast). Without this, a token-exchange JWT inx-n8n-api-keywould short-circuit the auth chain beforeScopedJwtStrategyruns
TokenExchangeModule (updated)
- Registers
ScopedJwtStrategyintoAuthStrategyRegistryat the end ofinit(), 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, orBackport to v1(if the PR is an urgent fix that needs to be backported)