Merged
Size
M
Change Breakdown
Feature80%
Config15%
Testing5%
#27844feat(core): Add POST /auth/oauth/token controller and audit event types for token exchange

Token exchange endpoint wired for RFC 8693 OAuth flows

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/token controller — validates an application/x-www-form-urlencoded RFC 8693 token exchange request and returns a well-formed RFC 8693 response. Two-stage validation distinguishes unsupported_grant_type (wrong/missing grant_type) from invalid_request (other schema violations).
  • TokenExchangeService stub — returns a placeholder response; will be replaced with real token issuance in a follow-up ticket.
  • TokenExchangeAuditEvent discriminated union (token_exchange_success, token_exchange_failure, embed_login) and the three corresponding n8n audit event names / relay handlers wired into log-streaming.event-relay.ts.
  • TokenExchangeModule — gated behind feat:tokenExchange license flag (LICENSE_FEATURES.TOKEN_EXCHANGE) so the endpoint only initialises on licensed enterprise instances.
  • N8N_TOKEN_EXCHANGE_ENABLED feature flag (default false) — even on a licensed instance, the endpoint returns 501 server_error until the flag is explicitly enabled.
  • 'token-exchange' registered as a named module in ModuleRegistry / ModulesConfig.

Files changed:

AreaFiles
New moduletoken-exchange.controller.ts, .service.ts, .module.ts, .types.ts
Config@n8n/config — new TokenExchangeConfig, wired into GlobalConfig.tokenExchange
License feature@n8n/constantsLICENSE_FEATURES.TOKEN_EXCHANGE = 'feat:tokenExchange'
Audit eventsrelay.event-map.ts, event-message-classes/index.ts, log-streaming.event-relay.ts
Module registrymodules.config.ts, module-registry.ts
E2E fixturee2e.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)
© 2026 · via Gitpulse