TL;DR: n8n Community Edition has no built-in SSO. We built a Ticket-Based Cookie SSO solution using FastAPI as a backend proxy. One-time tickets (256-bit, 30s TTL) + cross-subdomain cookie delivery = seamless single-click navigation from our management platform into n8n workflow editors — no second login required. Production-verified with multi-tenant isolation.
1. The Problem
Our management platform needs to let operations staff click a workflow name and land directly in the n8n editor — without a second login prompt.
Core Challenges:
| Challenge | Description |
|---|---|
| n8n CE has no SSO | No SAML/OIDC/OAuth — only username + password |
| Cross-domain | Platform (app.example.com) and n8n (n8n-abc123.example.com) are on different subdomains |
| Credential security | n8n passwords must not be exposed to the browser |
| Multi-tenant | Each customer has an independent n8n instance |
2. Solution: Ticket-Based Cookie SSO
- Backend proxies the n8n login, obtains an
n8n-authcookie - Generates a one-time ticket (256-bit token, 30-second expiry)
- Browser uses the ticket URL → receives the cookie → 302-redirected to the target page
- Shared parent domain (
.example.com) enables cross-subdomain cookie delivery
Architecture Overview
flowchart TD
subgraph Browser["Browser"]
FE["Frontend<br/>Vue 3 + useSsoNavigation"]
end
subgraph Backend["Backend (FastAPI)"]
SSO_URL["/n8n/sso-url<br/>POST · Create Ticket"]
SSO_REDIRECT["/n8n/sso-redirect<br/>GET · Consume Ticket"]
TS["SSOTicketStore<br/>One-Time Ticket Storage"]
AC["N8NAuthCache<br/>Login Credential Cache"]
CR["resolve_credentials<br/>Credential Resolver (DI)"]
end
subgraph N8N["n8n Instance"]
LOGIN["/rest/login<br/>POST · Username+Password Login"]
ME["/rest/me<br/>GET · Cookie Probe"]
EDITOR["Workflow Editor<br/>/workflow/:id"]
end
FE -->|"1. POST /n8n/sso-url"| SSO_URL
SSO_URL -->|"2. Resolve credentials"| CR
CR -->|"3. Get/cache cookie"| AC
AC -->|"4. POST /rest/login"| LOGIN
AC -.->|"Probe cached cookie"| ME
SSO_URL -->|"5. Create ticket"| TS
SSO_URL -->|"6. Return ticket_url"| FE
FE -->|"7. window.open(ticket_url)"| SSO_REDIRECT
SSO_REDIRECT -->|"8. Consume ticket"| TS
SSO_REDIRECT -->|"9. Set-Cookie + 302 Redirect"| Browser
Browser -->|"10. Access with cookie"| EDITOR
style TS fill:#e1f5fe
style AC fill:#e8f5e9
style CR fill:#fff3e0
3. Core Flow — SSO Navigation Sequence
sequenceDiagram
autonumber
participant U as User Browser
participant FE as Frontend
participant BE as Backend
participant TS as TicketStore
participant AC as AuthCache
participant N8N as n8n Instance
U->>FE: Click workflow name
FE->>BE: POST /api/v1/n8n/sso-url<br/>(target: /workflow/abc)
Note over BE: Verify JWT + resolve user
BE->>BE: resolve_credentials(user)<br/>Get n8n_url / email / password
BE->>AC: get_or_login(customer, url, email, pass, force_refresh=True)
alt Cache hit + probe valid
AC->>N8N: GET /rest/me (cookie probe)
N8N-->>AC: 200 OK
AC-->>BE: Return cached cookie
else Cache miss / probe failed
AC->>N8N: POST /rest/login
N8N-->>AC: 200 + Set-Cookie: n8n-auth
AC->>AC: Cache cookie (TTL=1h)
AC-->>BE: Return new cookie
end
BE->>BE: derive_cookie_domain(n8n_url)<br/>→ .example.com
BE->>TS: create(redirect_url, cookie, domain)
TS-->>BE: ticket_id (256-bit token)
BE-->>FE: (ticket_url: /api/v1/n8n/sso-redirect?ticket=abc123)
FE->>U: window.open(ticket_url, '_blank')
U->>BE: GET /api/v1/n8n/sso-redirect?ticket=abc123
BE->>TS: consume(ticket_id)
TS-->>BE: SSOTicket(redirect_url, cookie, domain)
BE-->>U: 302 Redirect to n8n_url/workflow/abc<br/>Set-Cookie: n8n-auth=... Domain=.example.com
U->>N8N: GET /workflow/abc (with n8n-auth cookie)
N8N-->>U: Workflow editor page
4. Key Components
SSOTicketStore — One-Time Ticket Storage
class SSOTicketStore:
def __init__(self, ttl: int = 30) -> None: ...
def create(self, redirect_url, auth_cookie, cookie_domain) -> str: ...
def consume(self, ticket_id: str) -> SSOTicket | None: ...
| Property | Implementation |
|---|---|
| Token strength | token_urlsafe(32) → 256-bit entropy |
| Single-use | dict.pop() semantics — deleted upon consumption |
| Expiry cleanup | _gc() triggered before every create()/consume() call |
| TTL | 30 seconds by default, configurable |
| Storage | Pure in-memory dict — no persistence required |
N8NAuthCache — Login Credential Cache + Probe
class N8NAuthCache:
def __init__(self, ttl: int = 3600) -> None: ...
async def get_or_login(self, customer_code, n8n_url, email, password,
*, force_refresh=False) -> str: ...
def invalidate(self, customer_code: str) -> None: ...
| Property | Implementation |
|---|---|
| Cache granularity | Isolated per customer_code |
| Default TTL | 1 hour (n8n sessions are typically valid for 7 days) |
| Probe mechanism | GET /rest/me validates cookie liveness (5s timeout) |
| Force refresh | force_refresh=True probes first; re-logins only if probe fails |
Frontend Composable
export function useSsoNavigation() {
async function openN8nLink(target: string) {
try {
const { ticket_url } = await getSsoUrl(target)
window.open(ticket_url, '_blank')
} catch (err: any) {
const detail = err?.response?.data?.detail || err?.message || 'SSO navigation failed'
console.error('[SSO] openN8nLink failed:', detail, err)
alert(detail)
}
}
return { openN8nLink }
}
Usage in Vue:
<script setup>
const { openN8nLink } = useSsoNavigation()
</script>
<template>
<a @click="openN8nLink('/workflow/' + wf.id)">{{ wf.name }}</a>
</template>
5. Security Design
Threat Model
| Threat | Risk Level | Mitigation |
|---|---|---|
| Ticket replay | High | Single-use (pop semantics) — deleted immediately after consumption |
| Ticket guessing | High | 256-bit random token (token_urlsafe(32)) |
| Exploiting expired tickets | Medium | 30-second TTL, lazy cleanup via _gc() |
| Cookie theft | Medium | Secure + SameSite=None — HTTPS-only transmission |
| Open redirect | High | Regex allowlist ^/(workflow|execution)/[a-zA-Z0-9]+$ |
| CSRF against SSO endpoints | Medium | sso-url requires JWT auth; sso-redirect only consumes tickets |
| Credential leakage | High | n8n passwords only flow through the backend — invisible to the frontend |
| Cross-tenant access | High | Credentials resolved in isolation per customer_code |
Security Chain
JWT Token → JWT Verification → Path Allowlist → Credential Isolation →
256-bit Ticket → 30s TTL → Single-Use → Secure Flag → SameSite=None → Domain Scoped
6. Configuration
| Environment Variable | Default | Description |
|---|---|---|
SSO_COOKIE_DOMAIN |
"" (auto-derived) |
Cookie Domain override |
SSO_COOKIE_SECURE |
true |
Cookie Secure flag |
SSO_COOKIE_SAMESITE |
"none" |
SameSite policy (must be none for cross-subdomain) |
SSO_DEV_MODE |
false |
Dev mode: skip login, return URL directly |
@dataclass
class SSOConfig:
ticket_ttl: int = 30 # Ticket expiry in seconds
cache_ttl: int = 3600 # Cookie cache TTL in seconds
cookie_secure: bool = True # Secure flag
cookie_samesite: str = "none" # SameSite policy
cookie_domain: str = "" # Domain override (empty = auto-derive)
cookie_max_age: int = 86400 # Cookie Max-Age in seconds
dev_mode: bool = False # Dev mode
safe_target_re: str = r"^/(workflow|execution)/[a-zA-Z0-9]+$"
7. Multi-Tenant Support
Each customer gets an independent n8n instance. Isolation is maintained at every layer:
- Credential resolution: Looks up the corresponding n8n connection per user’s
customer_code - Cache isolation:
N8NAuthCachekeys bycustomer_code— cookies never interfere - Ticket sharing:
SSOTicketStoreneeds no per-customer isolation (ticket IDs are globally unique) - Cookie Domain: Each customer’s n8n URL independently derives its own Domain
8. Deployment Topology
Internet → Nginx (*.example.com wildcard SSL) →
├── Platform (:8080, FastAPI + SSO Router)
├── n8n-customer1 (:5678)
├── n8n-customer2 (:5678)
└── n8n-customer3 (:5678)
- TLS termination at Nginx with wildcard certificate
- Internal communication: Platform → n8n uses plain HTTP inside Docker network
- Cookie delivery:
Secure; SameSite=None; Domain=.example.com
9. Minimal Integration Example
If you want to add this to your own FastAPI backend:
from n8n_sso import create_sso_router, SSOConfig, N8NCredentials
# 1. Define the credential resolver
async def resolve_creds(user) -> N8NCredentials:
return N8NCredentials(
customer_code="default",
n8n_url="https://n8n.example.com",
email="[email protected]",
password="your-password",
)
# 2. Create the SSO router
sso_router = create_sso_router(
config=SSOConfig(cookie_secure=not DEBUG),
auth_dependency=get_current_user,
resolve_credentials=resolve_creds,
prefix="/api/v1/n8n",
)
# 3. Register the router
app.include_router(sso_router)
Summary
This Ticket-Based Cookie SSO approach works well because:
- Zero n8n modification needed — works with stock n8n Community Edition
- Secure by design — one-time 256-bit tickets, 30s TTL, single-use pop semantics
- Multi-tenant ready — credential and cache isolation per customer
- Simple integration — ~25 lines of Python to wire it up
We’ve been running this in production across multiple n8n instances for several months with zero SSO-related incidents. Happy to answer questions or share more details!
Built with Python/FastAPI + Vue 3. n8n Community Edition 1.x/2.x compatible.