Token exchange endpoint wired for RFC 8693 OAuth flows

Enterprise users gain an RFC 8693 token exchange endpoint for OAuth flows, with audit logging, rate limiting, and license-gated access.
n8n is laying groundwork for OAuth token exchange. A new endpoint at POST /auth/oauth/token now accepts RFC 8693 token exchange requests — clients can swap one token for another using the standard urn:ietf:params:oauth:grant-type:token-exchange grant type.
The endpoint validates incoming requests in two stages: first checking the grant type against the RFC 8693 value, then running full schema validation. Error responses follow the spec — wrong or missing grant type returns unsupported_grant_type, schema violations return invalid_request. The endpoint is rate-limited to 20 requests per minute per client IP.
Three new audit event types track usage: token_exchange_success, token_exchange_failure, and embed_login. All token exchange attempts are logged with client IP, grant type, and scope data. A separate commit addresses a security concern — raw JWT tokens are scrubbed from audit event payloads to prevent token leakage.
Access is controlled at two levels. The feat:tokenExchange license flag gates whether the module initializes at all — unlicensed instances return 404. Even on licensed instances, the N8N_TOKEN_EXCHANGE_ENABLED feature flag defaults to false, so the endpoint returns 501 server_error until explicitly enabled.
This PR ships the API contract and audit infrastructure. The actual token issuance logic lives in a TokenExchangeService stub that returns placeholder responses — real token exchange logic is scheduled for a follow-up ticket.
In the n8n CLI package, under the token-exchange module.
View Original GitHub Description
Summary
Establishes the API contract for the token-exchange flow. Reviewers can see the endpoint shape, request validation, esponse format, and audit event structure before any real token-issuance logic lands.
What's included:
POST /auth/oauth/tokencontroller — validates anapplication/x-www-form-urlencodedRFC 8693 token exchange request and returns a well-formed RFC 8693 response. Two-stage validation distinguishesunsupported_grant_type(wrong/missinggrant_type) frominvalid_request(other schema violations).TokenExchangeServicestub — returns a placeholder response; will be replaced with real token issuance in a follow-up ticket.TokenExchangeAuditEventdiscriminated union (token_exchange_success,token_exchange_failure,embed_login) and the three corresponding n8n audit event names / relay handlers wired intolog-streaming.event-relay.ts.TokenExchangeModule— gated behindfeat:tokenExchangelicense flag (LICENSE_FEATURES.TOKEN_EXCHANGE) so the endpoint only initialises on licensed enterprise instances.N8N_TOKEN_EXCHANGE_ENABLEDfeature flag (defaultfalse) — even on a licensed instance, the endpoint returns501 server_erroruntil the flag is explicitly enabled.'token-exchange'registered as a named module inModuleRegistry/ModulesConfig.
Files changed:
| Area | Files |
|---|---|
| New module | token-exchange.controller.ts, .service.ts, .module.ts, .types.ts |
| Config | @n8n/config — new TokenExchangeConfig, wired into GlobalConfig.tokenExchange |
| License feature | @n8n/constants — LICENSE_FEATURES.TOKEN_EXCHANGE = 'feat:tokenExchange' |
| Audit events | relay.event-map.ts, event-message-classes/index.ts, log-streaming.event-relay.ts |
| Module registry | modules.config.ts, module-registry.ts |
| E2E fixture | e2e.controller.ts — exhaustive BooleanLicenseFeature map updated |
Manual testing
Start n8n with the license flag and feature flag enabled:
N8N_TOKEN_EXCHANGE_ENABLED=true \
N8N_LICENSE_ACTIVATION_KEY=<your-enterprise-key> \
pnpm start
1. Feature flag disabled (default) — expect 501
curl -s -X POST http://localhost:5678/rest/auth/oauth/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange&subject_token=test' \
| jq .
# → { "error": "server_error", "error_description": "Token exchange is not enabled on this instance" }
2. Wrong grant_type — expect 400 unsupported_grant_type
curl -s -X POST http://localhost:5678/rest/auth/oauth/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=client_credentials&subject_token=test' \
| jq .
# → { "error": "unsupported_grant_type", "error_description": "..." }
3. Missing subject_token — expect 400 invalid_request
curl -s -X POST http://localhost:5678/rest/auth/oauth/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
| jq .
# → { "error": "invalid_request", "error_description": "..." }
4. Valid request (stub) — expect 200 RFC 8693 response
curl -s -X POST http://localhost:5678/rest/auth/oauth/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange&subject_token=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyMSJ9.sig' \
| jq .
# → {
# "access_token": "stub-access-token",
# "token_type": "Bearer",
# "expires_in": 3600,
# "issued_token_type": "urn:ietf:params:oauth:token-type:access_token"
# }
5. Valid request with optional fields
curl -s -X POST http://localhost:5678/rest/auth/oauth/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
--data-urlencode 'subject_token=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyMSJ9.sig' \
--data-urlencode 'subject_token_type=urn:ietf:params:oauth:token-type:jwt' \
--data-urlencode 'scope=openid profile' \
--data-urlencode 'audience=https://api.example.com' \
| jq .
6. Unlicensed instance — module should not initialise
Start n8n without a license (or without feat:tokenExchange) and confirm the
endpoint returns 404:
curl -s -o /dev/null -w "%{http_code}" \
-X POST http://localhost:5678/rest/auth/oauth/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange&subject_token=test'
# → 404
Related Linear tickets, Github issues, and Community forum posts
https://linear.app/n8n/issue/IAM-459
Review / Merge checklist
- PR title and summary are descriptive. (conventions)
- Docs updated or follow-up ticket created.
- Tests included.
- PR Labeled with
release/backport(if the PR is an urgent fix that needs to be backported)