Ticket-Based Cookie SSO for n8n Community Edition — Complete Design & Implementation

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-auth cookie
  • 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:

  1. Credential resolution: Looks up the corresponding n8n connection per user’s customer_code
  2. Cache isolation: N8NAuthCache keys by customer_code — cookies never interfere
  3. Ticket sharing: SSOTicketStore needs no per-customer isolation (ticket IDs are globally unique)
  4. 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.

@zhangbaoqian nice write-up — the one-time ticket + pop semantics is exactly the right call here, avoids all the PKCE overhead while keeping the security guarantees solid. had a similar cross-subdomain auth problem last year and went the reverse-proxy route but this is much cleaner.

one thing I’m curious about: what happens when the cached n8n-auth cookie expires mid-session (user has the editor open for 2+ hours while the cache TTL resets)? does the next SSO navigation attempt trigger force_refresh automatically, or does the user hit a silent 401 until they open a new tab?

Thanks @Benjamin_Behrens — great question. This touches on one of the trickier parts of the design.

Short answer

The next SSO navigation triggers a fresh login automatically — the user never hits a silent 401 from the SSO path.

How the layered expiry works

There are three independent TTL layers:

Layer TTL Scope
BCP server-side auth cache 1 hour Backend in-memory
Browser cookie (Set-Cookie) 24 hours max-age=86400
n8n session ~7 days n8n server-side

While the user has the n8n editor open, the browser cookie is what matters — it stays valid as long as the n8n server session hasn’t expired (~7 days), regardless of whether the BCP-side cache has been evicted.

When the user clicks another workflow link in BCP:

  1. SSO endpoint calls get_or_login() with force_refresh=True
  2. This triggers a probe — GET /rest/me with the cached cookie (5s timeout)
  3. n8n returns 200 → reuse the cookie, no re-login
  4. n8n returns 401/timeout → transparent POST /rest/login, new ticket, redirect

Flow: probe → reuse or re-login → redirect — all invisible to the user.

Your “2+ hours in editor” scenario:

  • User working in n8n editor → browser cookie valid, n8n accepts all calls directly
  • BCP cache expires after 1h → no impact on active session
  • User clicks a new workflow link → probe confirms cookie still valid → seamless redirect

Honest limitations

That said, I want to be upfront about the known weaknesses of this approach — things I’m actively thinking about improving:

1. Third-party cookie risk (the big one)

The entire mechanism relies on Set-Cookie with SameSite=none across subdomains. As browsers continue tightening third-party cookie policies, this is a real long-term risk.

Planned mitigation: Reverse-proxy n8n traffic through the BCP domain (bcpcloud.cn/n8n/lydx/ instead of bcpn8n-lydx.bcpcloud.cn), turning the cross-origin cookie into a first-party cookie. This eliminates the browser policy risk entirely with just an Nginx config change.

2. In-memory ticket store

The current SSOTicketStore uses a Python dict — process restart loses all in-flight tickets, and it doesn’t work across multiple instances.

Planned mitigation: Replace with a signed JWT ticket (self-contained, stateless). The one-time-use guarantee moves to a Redis SET with 30s auto-expiry. Even without Redis, the 30s TTL window makes the restart risk minimal in practice.

3. No per-user audit trail

All BCP users share one n8n account per customer. Once a user lands in n8n via SSO, there’s no tracking of what they do inside.

Planned mitigation: Two layers — (a) log every SSO ticket creation with user identity, target workflow, IP, and timestamp at the BCP side; (b) use separate read-only (Member) and admin (Owner) n8n accounts, with the default SSO path using the read-only account.

4. Probe latency

The force_refresh probe adds a network round-trip on every SSO navigation. Worst case (n8n slow/unreachable): 5s timeout before the user sees an error.

Planned mitigation: Background warmup — periodically refresh cookies for active customers so the SSO path hits cache with zero extra latency.

Where this approach fits

I think it’s important to be clear about the positioning: this is a pragmatic workaround for n8n Community Edition having no native SSO. It works well today, but it’s not a robust long-term architecture.

The evolution path I’m working toward:

Scenario Approach
View workflow topology API proxy + in-platform rendering (no SSO needed)
Edit/debug workflow SSO redirect (this mechanism)

This hybrid reduces the SSO surface area to only the cases where the full n8n editor is actually needed, while the common “just looking” path avoids cookies entirely.

Happy to share more implementation details or discuss alternative approaches. Curious if anyone else has tackled SSO for self-hosted n8n Community Edition — would love to compare notes.

@zhangbaoqian the layered TTL breakdown is exactly what I was looking for — makes sense that the browser cookie is what matters for the active session and the force_refresh probe handles transparent re-login without the user ever noticing. the third-party cookie risk is real and I’d probably prioritize the reverse-proxy fix sooner rather than later given where browsers are heading. curious about the hybrid approach you mentioned — does rendering workflow topology via API proxy mean you’re building a read-only view inside BCP, or is there a lightweight embed option that avoids the SSO flow entirely?

@Benjamin_Behrens good instinct on prioritizing the reverse-proxy — that’s moved to the top of my list too.

To answer your question directly: yes, it’s a read-only view built inside BCP, not an embed. n8n Community Edition has no embed/iframe option — no postMessage API, no sandboxed viewer mode, nothing you can drop into another app.

How it works

The backend proxies n8n’s REST API to fetch the workflow JSON:

GET /rest/workflows/:id
→ Returns: nodes[], connections{}, name, active, ...

Each node in the JSON contains its type, position ({x, y}), parameters, and connection map. That’s everything you need to render a topology graph.

On the frontend, we use Vue Flow (Vue 3 wrapper around reactflow) to render the node graph:

n8n workflow JSON → normalize nodes/edges → Vue Flow canvas

The result is a read-only, interactive topology view — you can pan, zoom, click nodes to inspect parameters — all inside the BCP platform, with zero cookies, zero SSO, zero cross-domain concerns.

The split

User intent Path Auth mechanism
“Which workflows does this customer have?” BCP list view API proxy (backend-to-backend)
“Show me the topology of workflow X” BCP canvas view API proxy (backend-to-backend)
“I need to edit/debug workflow X” n8n editor (new tab) SSO redirect (this mechanism)

The API proxy path uses the same N8NAuthCache cookie internally — it just never exposes it to the browser. The backend calls n8n’s REST API with the cached cookie server-side, transforms the response, and returns clean JSON to the frontend. No cross-domain anything.

Why not iframe?

I explored a few embed options before going this route:

  • iframe with SSO cookie: works technically, but X-Frame-Options: SAMEORIGIN on n8n blocks it unless you patch n8n’s response headers — which means maintaining a fork or adding Nginx header manipulation per customer
  • n8n’s public API: CE doesn’t expose a public/external API key system — you’d still need cookie-based auth
  • Headless screenshot: considered puppeteer-based snapshots, but stale by definition and loses interactivity

The Vue Flow approach turned out to be the sweet spot — lightweight, fully interactive, and completely decoupled from n8n’s frontend.

What you lose

Honest trade-off: you don’t get n8n’s execution history, debug panel, or expression editor in the read-only view. It’s purely topology + node parameter inspection. For anything beyond “look at the structure,” users click “Open in n8n Editor” and the SSO flow kicks in.

In practice, ~80% of our operations staff interactions are “just looking” — checking which workflows exist, verifying the topology is correct, confirming a node’s configuration. The SSO path only fires for the 20% that actually need to edit.

the Vue Flow approach is the right call — building the read-only view over the API proxy means you can layer your own UX on top of the topology (status overlays, last-execution indicators, direct links to related resources) without touching n8n at all. the 80/20 split makes sense too; most monitoring/verification use cases really don’t need the editor, and reducing the SSO surface to just the edit path makes the whole thing much easier to reason about and audit.

@Benjamin_Behrens exactly — that’s actually the part I’m most excited about. Once you own the rendering layer, you can do things n8n’s native UI never will:

  • Status overlays: green/red/yellow badges per node based on last execution result
  • Last-run timestamps: “this node last fired 3 hours ago” directly on the canvas
  • Data flow indicators: show record counts on each connection edge (we pull this from execution data)
  • Business context links: click a node → see the related ERP/SRM config in BCP, not just the raw HTTP parameters

None of that requires touching n8n. The workflow JSON + execution API gives us everything we need, and Vue Flow makes the custom rendering straightforward.

Thanks for the thoughtful back-and-forth on this — your questions pushed me to articulate the design trade-offs more clearly than I had before. If you end up tackling something similar for your setup, I’d be curious to hear how it goes.

the status overlays and data flow indicators are exactly the kind of contextual layer that makes these tools genuinely useful for ops teams rather than just developers — execution counts on the edges especially, that’s something the raw n8n editor can never really show you in the right context. will share back if we end up going this route — the design trade-offs you laid out here are the clearest articulation I’ve seen of this problem space.

Really enjoyed this exchange. To be honest, the SSO challenge for self-hosted n8n has been sitting in the back of my mind for a long time — not something I was actively working on every day, but something I never quite let go of either.

This conversation actually helped me sharpen my own thinking on the design trade-offs. I should also credit Claude as a thought partner here — working through the architectural options with it helped me articulate things I’d been feeling but hadn’t fully structured yet.

@zhangbaoqian mutual — threads like this where both sides come away with sharper framing than they started with are genuinely rare. the hybrid proxy + SSO split is what I’ll be reaching for the next time I need to explain these tradeoffs to someone.

thanks! agreed — it’s rare to find threads where both sides genuinely refine each other’s thinking rather than just restating positions.

that said, I’m a bit surprised this area doesn’t get more attention. embedding and SSO for self-hosted n8n feels like a real need for anyone building a product layer on top — yet the conversation stays pretty quiet. maybe everyone’s just too shy to share their battle scars? :grinning_face_with_smiling_eyes:

would love to see more folks jump in. the more real-world patterns we surface, the better the ecosystem gets for everyone.

@zhangbaoqian yeah, I think you’re right — there’s a lot of quiet engineering happening in this space that never makes it to a forum post. threads like this one actually make it easier for the next person to surface their approach, because there’s suddenly a reference point to build on or push back against. hopefully a few more people take the bait.