Token exchange for embedding partners#
Feature availability
- Available on Enterprise plans.
- Enabled with the
N8N_ENV_FEAT_TOKEN_EXCHANGEenvironment variable set totrue. Self-hosted instances can set it directly. On Cloud, contact n8n support to request it. - Intended for embedding partners who run an external identity provider (IdP) or backend that mints signed JWTs.
Preview feature
Token exchange is a preview feature behind an environment flag. The environment variables, endpoint paths, and JWT claim contract can change before the feature reaches general availability. Pin your n8n version and retest your integration after each upgrade.
OAuth 2.0 Token Exchange (RFC 8693) lets embedding partners authenticate users and act on their behalf within an embedded n8n instance. Token exchange supports two use cases:
- Iframe SSO: Exchange an external JWT for an n8n session cookie, so users log in seamlessly when you embed n8n in an iframe.
- Delegated API access: Exchange an external JWT for an n8n access token to call n8n APIs on behalf of a user, for example to trigger workflows or manage credentials.
Both flows start the same way. Your backend mints a short-lived JWT signed with your private key. n8n verifies it using the registered public key, resolves the user, and returns either a session cookie or an access token.
Before you begin#
You need:
- An Enterprise license with the token exchange feature enabled.
- The preview feature flag set on your instance:
N8N_ENV_FEAT_TOKEN_EXCHANGE=true. - An RSA or EC key pair, or a JWKS endpoint that your IdP already publishes. See Generate a key pair.
- An n8n instance served over HTTPS. Browsers reject the
SameSite=None; Securesession cookie over plain HTTP, so the iframe SSO flow silently fails without it. If you run n8n behind a TLS-terminating proxy, make sure the proxy sets theX-Forwarded-Protoheader.
Generate a key pair#
You need an asymmetric key pair. Your backend signs JWTs with the private key, and n8n verifies them with the public key.
1 2 3 4 5 | |
1 2 3 4 5 | |
Keep private.pem secret in your backend. You register the contents of public.pem in n8n's trusted keys configuration.
If your IdP already publishes a JWKS endpoint, as most OAuth 2.0 and OIDC providers do, you can skip key generation and point n8n at the JWKS URL instead. See JWKS key source.
Environment variables#
Required for all setups#
1 2 3 4 5 | |
Per-flow toggles#
The two flows are independent. Enable only the endpoint you use:
1 2 3 4 5 | |
The embed login endpoint doesn't require N8N_TOKEN_EXCHANGE_ENABLED, and the token exchange endpoint doesn't require N8N_EMBED_LOGIN_ENABLED.
File-based configuration
You can add _FILE to individual variables to provide their configuration in a separate file. Refer to Keeping sensitive data in separate files for more details.
For example, for large or multi-line JSON, store the trusted keys in a file and set N8N_TOKEN_EXCHANGE_TRUSTED_KEYS_FILE=/path/to/trusted-keys.json.
Optional tuning settings#
These have sensible defaults. You don't usually need to change them:
| Variable | Default | Description |
|---|---|---|
N8N_TOKEN_EXCHANGE_MAX_TOKEN_TTL |
900 (15 minutes) |
Maximum lifetime in seconds for issued n8n tokens. The actual expiry is the minimum of this value, the subject token's remaining lifetime, and the actor token's remaining lifetime (if present). |
N8N_TOKEN_EXCHANGE_KEY_REFRESH_INTERVAL_SECONDS |
300 (5 minutes) |
Fallback interval for refreshing JWKS keys when the endpoint doesn't provide a cache lifetime. Every instance refreshes keys at startup; in multi-main deployments, only the leader instance refreshes them periodically. |
N8N_TOKEN_EXCHANGE_JTI_CLEANUP_INTERVAL_SECONDS |
60 (1 minute) |
How often n8n cleans up expired replay-protection records. |
N8N_TOKEN_EXCHANGE_JTI_CLEANUP_BATCH_SIZE |
1000 |
Maximum expired records deleted per cleanup run. |
N8N_TOKEN_EXCHANGE_EMBED_LOGIN_PER_MINUTE |
20 |
Rate limit for the embed login endpoint (requests per IP per minute). |
N8N_TOKEN_EXCHANGE_TOKEN_EXCHANGE_PER_MINUTE |
20 |
Rate limit for the token exchange endpoint (requests per IP per minute). |
Configure trusted keys#
The N8N_TOKEN_EXCHANGE_TRUSTED_KEYS environment variable accepts a JSON array of trusted key sources. Each entry tells n8n how to verify JWTs from your IdP.
There are two source types: static (an inline public key) and jwks (a remote JWKS endpoint). You can mix both types in the same array.
Static key source#
Use this when you generated your own key pair and want to embed the public key directly.
1 2 3 4 5 6 7 8 9 | |
| Field | Required | Description |
|---|---|---|
type |
Yes | Must be "static". |
kid |
Yes | Key ID. Must match the kid header in incoming JWTs. |
algorithms |
Yes | Array of allowed signing algorithms, for example ["RS256"]. See Supported algorithms. |
key |
Yes | PEM-encoded public key. Use \n for line breaks in JSON. |
issuer |
Yes | Expected iss claim in incoming JWTs. |
expectedAudience |
No | If set, the JWT aud claim must match this value. If unset, n8n skips audience verification entirely. Always set it in production, using an instance-specific value such as your n8n base URL. |
allowedRoles |
No | If set, only these roles can be assigned through the role claim. Only include global:admin if you deliberately want the integration to manage admin users. See Role handling. |
JWKS key source#
Use this when your IdP publishes a JWKS endpoint.
1 2 3 4 5 6 7 | |
| Field | Required | Description |
|---|---|---|
type |
Yes | Must be "jwks". |
url |
Yes | URL of the JWKS endpoint. |
issuer |
Yes | Expected iss claim in incoming JWTs. |
expectedAudience |
No | If set, the JWT aud claim must match this value. If unset, n8n skips audience verification entirely. Always set it in production, using an instance-specific value such as your n8n base URL. |
allowedRoles |
No | If set, only these roles can be assigned through the role claim. See Role handling. |
cacheTtlSeconds |
No | Fallback cache duration when the JWKS endpoint doesn't send a Cache-Control: max-age header. When the header is present, it takes precedence. Defaults to 3600 seconds. The effective value is bounded between 60 and 86400 seconds. |
Full example#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Supported algorithms#
n8n accepts only asymmetric algorithms. It excludes HMAC and none.
| Family | Algorithms |
|---|---|
| RSA | RS256, RS384, RS512 |
| RSA-PSS | PS256, PS384, PS512 |
| Elliptic Curve | ES256, ES384, ES512 |
| Edwards Curve | EdDSA |
For static keys, the algorithms in the config must all belong to the same family and match the key type. For JWKS keys, n8n infers the algorithms from the JWK alg and kty or crv fields automatically.
Required and optional JWT claims#
Required claims#
Your IdP tokens must include these claims for n8n to accept them:
| Claim | Type | Description |
|---|---|---|
sub |
string | Subject identifier. The unique user ID at the IdP. |
iss |
string (URL) | Issuer. Must match the issuer in your trusted key config. |
aud |
string or string array | Audience. Must be present. n8n validates the value only when expectedAudience is configured on the trusted key source. |
iat |
number | Issued-at timestamp (Unix epoch seconds). |
exp |
number | Expiration timestamp (Unix epoch seconds). |
jti |
string | Unique token ID. n8n accepts each value only once (replay protection). |
Optional claims#
| Claim | Type | Description |
|---|---|---|
email |
string (valid email) | The user's email, used to match existing n8n users. Required for JIT provisioning, the first login of a user n8n doesn't know yet. Always send it unless you're certain every user already exists. |
given_name |
string | First name, synced to the n8n user profile. |
family_name |
string | Last name, synced to the n8n user profile. |
role |
string | n8n role to assign, for example global:member or global:admin. See User provisioning. |
nbf |
number | Not-before timestamp. |
Iframe SSO flow#
Use this flow when embedding n8n in an iframe. n8n logs the user in transparently with a session cookie.
sequenceDiagram
participant Browser as User browser
participant Backend as Your backend
participant n8n as n8n instance
Browser->>Backend: 1. Load iframe
Backend->>Backend: 2. Mint JWT (private key)
Backend-->>Browser: 3. Return auto-submitting form (token)
Browser->>n8n: 4. POST /rest/auth/embed (token in body)
n8n->>n8n: 5. Verify JWT
n8n->>n8n: 6. Resolve user
n8n->>n8n: 7. Set session cookie
n8n-->>Browser: 8. Redirect (with session cookie)
Browser->>n8n: 9. User is logged in, loads n8n UI
Step 1: Mint a JWT in your backend#
Your backend creates a short-lived JWT signed with your private key.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | |
Maximum 60-second lifetime
For the embed login flow, the JWT lifetime (exp - iat) must not exceed 60 seconds. n8n enforces this server-side.
Step 2: Send the token to the embed endpoint#
Send the token to POST /rest/auth/embed as a form submission. Point the iframe src at a page on your backend that returns an auto-submitting form:
1 2 3 4 5 | |
The optional redirectTo field accepts only relative paths starting with /. n8n falls back to / for absolute URLs or anything else.
There's also a GET /rest/auth/embed?token=<jwt>&redirectTo=/workflow/abc123 variant. Prefer the POST form: with GET, the token appears in server and proxy logs, browser history, and possibly Referer headers. If you must use GET, make sure your infrastructure scrubs query strings from logs.
Step 3: n8n verifies and issues a session#
n8n verifies the JWT signature, resolves or provisions the user (see User provisioning), sets a secure session cookie (SameSite=None; Secure), and redirects to the specified path.
Third-party cookie restrictions#
The session cookie is a third-party cookie when n8n runs on a different registrable domain than your product. Browser privacy features, such as Safari's Intelligent Tracking Prevention and Chrome's third-party cookie restrictions, can block or partition it, which breaks the iframe login. Host n8n on a subdomain of your product's site, for example automation.your-product.example.com, to avoid this.
Delegated API access flow#
Use this flow when your backend needs to call n8n APIs on behalf of a user, for example to trigger a workflow or manage credentials programmatically.
This flow supports an optional actor token for delegation. An actor, such as a service account or admin, acts on behalf of a subject, the end user. This enables audit attribution, so n8n records both who performed the action and on whose behalf.
When you supply an actor token, n8n authorizes API calls with the actor's identity and permissions. n8n records the subject for audit attribution only, and the subject doesn't limit what the token can do. This differs from RFC 8693, where the subject usually remains the effective principal. Use an actor whose n8n role grants only the permissions your integration needs.
sequenceDiagram
participant Backend as Your backend
participant n8n as n8n instance
Backend->>Backend: 1. Mint subject JWT (+ optional actor JWT)
Backend->>n8n: 2. POST /rest/auth/oauth/token (token-exchange grant)
n8n->>n8n: 3. Verify JWTs
n8n->>n8n: 4. Resolve users
n8n->>n8n: 5. Issue n8n access token
n8n-->>Backend: 6. access_token, token_type, expires_in
Backend->>n8n: 7. Call n8n APIs with Bearer token
Step 1: Mint JWTs in your backend#
Create a subject token representing the end user. The 60-second limit from the embed flow doesn't apply here. The issued n8n access token expires after the minimum of the subject token's remaining lifetime, the actor token's remaining lifetime (if present), and N8N_TOKEN_EXCHANGE_MAX_TOKEN_TTL (default 15 minutes).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
For delegation, also mint an actor token representing the service or admin performing the action. The actor's n8n role determines the issued token's permissions, so keep it as low-privileged as possible:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Step 2: Exchange for an n8n access token#
1 2 3 4 5 | |
Request fields (application/x-www-form-urlencoded):
| Field | Required | Description |
|---|---|---|
grant_type |
Yes | Must be urn:ietf:params:oauth:grant-type:token-exchange. |
subject_token |
Yes | JWT representing the end user. |
subject_token_type |
No | Token type identifier. n8n accepts and ignores this field. RFC 8693 requires it, so send urn:ietf:params:oauth:token-type:jwt if you use a spec-compliant client library. |
actor_token |
No | JWT representing the actor (for delegation). |
actor_token_type |
No | Actor token type identifier. Accepted and ignored, like subject_token_type. |
requested_token_type |
No | Requested token type identifier. Accepted and ignored. n8n always issues an access token. |
scope |
No | Requested scope (maximum 1024 characters). Recorded in the issued token and audit events, but not enforced. |
audience |
No | Intended audience (maximum 1024 characters). Accepted and ignored. |
resource |
No | Target resource URIs, space-separated (maximum 2048 characters). Recorded in the issued token and audit events, but not enforced. |
Scopes aren't enforced
The issued access token carries the acting user's full permissions: the actor's when you send an actor token, otherwise the subject's. n8n records scope and resource for auditing only and doesn't restrict the token to them. To limit what an integration can do, use a low-privileged actor and the allowedRoles setting instead.
Success response (200 OK):
1 2 3 4 5 6 | |
Step 3: Use the access token#
Include the token in subsequent n8n API calls:
1 2 | |
The token expires after expires_in seconds. Request a new one when it expires. Don't reuse the original external JWT, since each jti is single-use.
User provisioning#
When you exchange a token, n8n resolves the external identity to an n8n user in this order:
- Known identity: n8n looks up the
subclaim in its identity store. If a previous exchange already linked thissubto an n8n user, n8n returns that user. - Email fallback: If the
subis unknown but the JWT contains anemailclaim, n8n searches for an existing user with that email. If found, n8n links the external identity to that user going forward. - Just-in-time (JIT) provisioning: If neither matches, n8n creates a new user automatically. The JWT must include an
emailclaim for this to work. n8n creates the new user with password login disabled, so they can only authenticate through token exchange.
Role handling#
If you include the role claim, your IdP becomes the source of truth for that user's global role: n8n applies it on every exchange, overwriting roles assigned in the n8n UI. Omit the claim unless you manage roles in your IdP.
| Scenario | Behavior |
|---|---|
New user, no role claim |
Assigned global:member. |
New user, role claim present |
Assigned the claimed role. The exchange fails if the role is unrecognized, is global:owner, or isn't in allowedRoles. |
Existing user, no role claim |
Role unchanged. |
Existing user, valid role claim |
Role updated if different. The exchange fails if the role isn't in allowedRoles. |
Existing user, unrecognized or global:owner role claim |
Claim ignored with a server-side warning. The login proceeds with the role unchanged. |
Existing user who is global:owner |
Role sync skipped entirely. The owner role can't be changed through token exchange. |
Profile sync#
n8n syncs the given_name and family_name claims to the user profile on each login. It applies changes only when the values differ from what's stored, and truncates values longer than 32 characters.
Security considerations#
Short-lived tokens#
- External JWTs for the embed flow must have a lifetime of at most 60 seconds (
exp - iat <= 60). - For the token exchange flow, the issued n8n token expiry is the minimum of the subject token's remaining lifetime, the actor token's remaining lifetime (if present), and
N8N_TOKEN_EXCHANGE_MAX_TOKEN_TTL(default 900 seconds). - In the token exchange flow, n8n rejects the request when the computed expiry of the issued token is fewer than five seconds away.
Replay protection#
Every external JWT must include a unique jti (JWT ID) claim. n8n records each jti and rejects any token whose jti has already been consumed. n8n cleans up expired jti records automatically.
Asymmetric signatures only#
n8n accepts only asymmetric algorithms (RSA, EC, EdDSA). It rejects HMAC algorithms such as HS256 and none by design. This ensures that n8n never needs access to your signing secret.
Audience restriction#
Always set expectedAudience on every trusted key source. Without it, n8n skips audience verification, and any valid token from the configured issuer can be exchanged, including tokens your IdP mints for other services or other n8n instances. Use an instance-specific value such as your n8n base URL.
Role constraints#
The allowedRoles field on trusted key sources restricts which roles can be assigned through token exchange. Use it to enforce least privilege, for example to restrict an embedding integration to provisioning only global:member users. The OAuth scope and resource request fields don't restrict permissions. See Step 2: Exchange for an n8n access token.
Restrict framing#
The embed session cookie uses SameSite=None, so the browser sends it in any third-party iframe context. Restrict which sites can frame your n8n instance: configure your reverse proxy to send a Content-Security-Policy: frame-ancestors header listing only your product's origins. Never disable framing protection wholesale.
Key rotation and compromise#
To rotate keys without downtime, add a new entry with a new kid to N8N_TOKEN_EXCHANGE_TRUSTED_KEYS, switch your backend to sign with the new key, then remove the old entry. If a private key is compromised, remove its entry immediately. This stops new exchanges, but already-issued n8n tokens and sessions stay valid until they expire, bounded by the short token lifetimes.
Audit attribution#
n8n emits audit events for all token exchange activity:
| Event | When |
|---|---|
n8n.audit.token-exchange.succeeded |
Successful token exchange. |
n8n.audit.token-exchange.failed |
Failed token exchange (with failure reason). |
n8n.audit.token-exchange.embed-login |
Successful embed login. |
n8n.audit.token-exchange.embed-login-failed |
Failed embed login (with failure reason). |
n8n.audit.token-exchange.user-provisioned |
New user created through JIT provisioning. |
n8n.audit.token-exchange.identity-linked |
Existing user linked to a new external identity. |
n8n.audit.token-exchange.role-updated |
User role changed through token exchange. |
When you use the actor token flow, n8n records both the subject (on whose behalf) and the actor (who performed the action), enabling full attribution in audit logs.
Verify your setup#
Mint a one-off JWT and exchange it. Save the Step 1 example as mint-token.js, add console.log(token); at the end, and run it:
1 | |
Then call the token exchange endpoint:
1 2 3 4 | |
A 200 response with an access_token confirms your keys, claims, and configuration work. For the embed flow, submit a fresh token to POST /rest/auth/embed in a browser and expect the n8n UI to load.
Troubleshooting#
| Symptom | Likely cause |
|---|---|
404 on both endpoints |
The preview flag N8N_ENV_FEAT_TOKEN_EXCHANGE isn't true, or your license doesn't include the token exchange feature. n8n doesn't load the module at all. |
501 - Token exchange is not enabled on this instance |
N8N_TOKEN_EXCHANGE_ENABLED isn't true. |
501 - Embed login is not enabled on this instance |
N8N_EMBED_LOGIN_ENABLED isn't true. |
400 - unsupported_grant_type |
The grant_type field is missing or not exactly urn:ietf:params:oauth:grant-type:token-exchange. |
400 - invalid_grant with description Token exchange failed |
Any token verification failure on the token exchange endpoint: invalid signature, unknown kid, missing kid header, replayed jti, expired or near-expiry token, audience mismatch, or disallowed role. n8n returns a generic description by design. Check the n8n server logs for the specific reason. |
400 - invalid_request with description Token claims validation failed |
On the token exchange endpoint: the JWT is missing a required claim (sub, iss, aud, iat, exp, jti), or a claim has the wrong type. |
401 from the embed endpoint |
The response includes the specific failure message, for example Token has already been used (replayed jti), Token lifetime exceeds maximum allowed (embed tokens must have exp - iat <= 60 seconds), or Token header missing kid. |
500 from the embed endpoint |
The JWT is missing a required claim or a claim has the wrong type. |
| Raw JSON error shows in the iframe | Embed login failures return a JSON response without an error redirect. Check the message against the rows above. |
| Session cookie isn't set after embed login | The instance isn't served over HTTPS, or a TLS-terminating proxy doesn't forward X-Forwarded-Proto. Browsers reject SameSite=None; Secure cookies over plain HTTP. Third-party cookie restrictions can also block the cookie, see Third-party cookie restrictions. |
| User not created on first login | The JWT is missing the email claim, which is required for JIT provisioning. |
| Role not applied | For existing users, n8n ignores unrecognized and global:owner role claims with a server-side warning. A valid role that's missing from allowedRoles rejects the exchange instead. See Role handling. |
Related resources#
- OEM deployment overview: embed and surface n8n's interface inside your product.
- Set up SSO: organization-wide single sign-on through SAML or OIDC.
- HTTP Request credentials: Using OAuth2: set up a generic OAuth 2.0 credential.
- OAuth 2.0 Token Exchange (RFC 8693): the token exchange specification.