[SIP-229] Proposal for one-time login tokens for iframe embedding
Motivation
Organizations increasingly want to embed Superset dashboards and charts as iframes inside their own web applications, presenting them to a user who is already signed in to that application. Today this is difficult to do as the real, fully-permissioned user when Superset sits behind an SSO provider that uses a browser-redirect login flow (OAuth2 / OIDC / SAML), because the redirect flow does not run inside an iframe:
- The IdP refuses to be framed. Many identity providers send
X-Frame-Options: DENYor a restrictiveContent-Security-Policy: frame-ancestors, so the login redirect cannot render inside the iframe. - Third-party cookies are blocked. Even when the IdP can be framed, the redirect chain relies on cookies that are now treated as third-party in an embedded context. Modern browsers block these by default, so the session cookie is never established.
So even though the embedding application already holds a trustworthy proof of the user's identity, there is currently no clean way to turn that proof into a normal Superset session inside the frame. The common workaround is a "click to open in a new window" affordance, which abandons the embedded experience entirely.
This is not hypothetical. At least one large deployment behind an enterprise OIDC provider independently produced a design document describing precisely this mechanism - a token exchange yielding a short-lived, single-use handle passed to the iframe - because there was no upstream option. Absent a supported path, deployments in this position either fork the security API or give up on embedding as the real user.
Superset already provides an embedding path for a related but distinct need - guest tokens + the Embedded SDK - which lets a host grant scoped access to specific dashboards, with host-supplied row-level security, without the viewer needing a Superset account. That fits cases where the host is the source of authorization and the embedded viewer does not need a real Superset identity.
This proposal targets a different case: the embedding application already has the user's identity, and requires the embedded view to be a 1-to-1 mirror of that user's own Superset session - the same account, the same roles, the same row-level security, the same object ownership and audit trail they would get by navigating to Superset directly. Many organizations need the same RBAC to apply whether Superset is accessed directly or embedded; constructing a separate guest session with host-supplied RLS does not meet that requirement, because the authorization then lives in the host rather than in Superset.
Proposed Change
Introduce an opt-in, two-step one-time login token flow that lets a trusted parent application establish a real, fully-authenticated Superset session inside an iframe without running the interactive SSO redirect in the frame.
Both steps live on one path on the existing security REST API, distinguished by verb:
POST /api/v1/security/login-token/ mint
GET /api/v1/security/login-token/ consume
Step 1 - mint a token (server-to-server).
The endpoint resolves the caller's identity through a pluggable identity resolver (see below), and on success:
- generates a cryptographically random, opaque token (a
uuid4), - stores the resolved
userinfoin Superset's existing key-value store viaKeyValueDAOunder a newKeyValueResource, with a short server-side TTL (expires_on), - returns
{ "access_token": "<opaque-token>", "expires_at": <unix-ts> }.
No identity data is placed in the token itself; the token is only an opaque handle to a short-lived server-side record.
Step 2 - consume the token in the iframe.
The browser reaches this by navigating to the iframe src, so it must be a GET, and it returns a 302 + Set-Cookie rather than JSON. The endpoint:
- looks up the
userinfoby token under a row lock (KeyValueDAO.get_entry(..., for_update=True)), - deletes the entry in the same transaction, making single use atomic rather than best-effort - two concurrent requests cannot both observe it,
- provisions/syncs the real user and their roles via
security_manager.auth_user_oauth(), the same code path a normal OAuth login uses, - calls
login_user()to establish the standard session cookie, - redirects to the validated
nextURL.
From that point the iframe holds an ordinary Superset session: identical RBAC, RLS, ownership, saved objects and audit attribution to a normal login. Links, drill-throughs and navigation to any route work because a real session cookie is present.
Pluggable identity resolver. The only deployment-specific part is how the caller proves who they are at mint time. This is exposed as a configurable hook so Superset core stays provider-agnostic. The resolver receives the incoming request and returns a userinfo dict, or None/raises to reject:
class LoginTokenUserInfo(TypedDict, total=False):
username: str
email: str
first_name: str
last_name: str
role_keys: list[str]
def LOGIN_TOKEN_IDENTITY_RESOLVER(
request: Request, **kwargs: Any
) -> LoginTokenUserInfo | None:
...
This is deliberately not a new contract: it is exactly the userinfo dict Flask-AppBuilder's auth_user_oauth already consumes. username identifies the user (falling back to email), and role_keys are resolved through AUTH_ROLES_MAPPING. Reusing it means provisioning and role sync are delegated wholesale to existing, exercised code rather than reimplemented.
It also answers the central security question. Because role_keys resolve only against AUTH_ROLES_MAPPING - a mapping the operator controls - and get_roles_from_keys only returns roles that already exist, a parent application cannot invent or escalate a role. It can only present keys the operator has already authorized. Authorization stays with Superset, which is precisely the requirement that rules out guest tokens for this use case.
Deployments can plug in, for example:
- validating an OIDC / SSO id token presented by the parent app,
- an already-authenticated Superset session (the caller mints a handoff token for its own identity),
- username/password verified against the configured auth backend,
- exchanging a custom opaque token with an internal identity service.
Worked example - OAuth2 / OIDC. A common deployment has the embedding application and Superset both registered with the same IdP. The mint call is made from the embedding service's backend, so the primary credential never travels through the browser - only the opaque one-time token does:
- The embedding service's backend holds (or obtains via the OAuth2 Token Exchange grant, RFC 8693) an id token scoped to Superset's audience for the signed-in user.
- The backend calls
POST /api/v1/security/login-token/with that id token. The resolver validates it (issuer, audience, signature, expiry) and returns the opaqueaccess_token. - The backend hands only the opaque token to its frontend, which sets it as the iframe
src:.../api/v1/security/login-token/?token=<opaque>&next=/dashboard/1/. - The browser loads the iframe, the token is consumed and deleted, and the frame now holds the user's real Superset session.
Flow diagram:
sequenceDiagram
participant IdP as OAuth2 / OIDC IdP
participant Backend as Embedding Service<br/>(backend)
participant Mint as Superset<br/>POST login-token
participant KV as Key-Value Store
participant Frame as Browser (iframe)
participant Consume as Superset<br/>GET login-token
participant SM as Security Manager
Backend->>IdP: token exchange -> id token (aud: superset)
IdP-->>Backend: id token
Backend->>Mint: POST login-token (id token, server-to-server)
Mint->>Mint: identity resolver validates -> userinfo
Mint->>KV: store userinfo, short TTL, key = random uuid
Mint-->>Backend: { access_token, expires_at }
Backend->>Frame: only the opaque token -> iframe src
Frame->>Consume: GET login-token?token=...&next=...
Consume->>KV: row-locked get + delete (atomic single use)
Consume->>SM: provision/sync user + roles
Consume-->>Frame: Set-Cookie (session) + 302 -> next
Frame->>Frame: renders as the real, authenticated user
Security properties (built in):
- Single use, atomically. The entry is row-locked before it is read and deleted in the same transaction, so a race cannot double-spend it.
- Short TTL. Server-side expiry, independent of the URL. Default 60 seconds.
- Opaque handle. No identity or claims travel in the URL, referrer, history or logs.
- Uniform failure. Unknown, malformed, expired and already-spent tokens all return an identical 401, so the response cannot be used to probe token state.
- A raising resolver rejects. An exception during validation is treated as a denial, not a server error - a validation failure surfacing as a 500 would be indistinguishable from an outage, and a retrying caller must never be mistaken for an authenticated one.
- Validated redirect.
nextis checked with the existing internal-URL helper and falls back to/, so it cannot become an open redirect. - Closed by default. Two independent switches are required - the feature flag and a configured resolver. With either missing, both endpoints return 404 rather than 403. A half-configured deployment of this feature would be worse than not having it.
- Rate-limiting the mint endpoint is recommended.
New or Changed Public Interfaces
New endpoints (one path, two verbs)
POST /api/v1/security/login-token/- mint a one-time login token. A JSON endpoint on the security API, public at the API layer exactly like the existingPOST /api/v1/security/login, and a direct sibling ofPOST /api/v1/security/guest_token; the identity resolver is what gates it. Response:{ access_token, expires_at }.GET /api/v1/security/login-token/- consume the token and establish the session. Public (the opaque token is the credential), returning 302 +Set-Cookie. Query params:token(required),next(optional, validated).
Both verbs share one path deliberately. An earlier draft split them across a JSON API route and a browser auth-view route on the theory that /api/v1 should return JSON only, but that is not an existing constraint - /api/v1 already serves image/png for chart and dashboard screenshots and application/zip for exports. One path means one place to reason about, one CSRF entry, and no risk of shadowing the dynamic /login/<provider> route.
New config / feature flag
LOGIN_TOKENfeature flag, off by default.LOGIN_TOKEN_IDENTITY_RESOLVER- callable resolving a request touserinfo;Noneby default, which leaves the feature inert.LOGIN_TOKEN_TTL_SECONDS- server-side token lifetime, default 60.- One entry added to
WTF_CSRF_EXEMPT_LISTfor the mint endpoint. Minting is a server-to-server call from a backend that holds no Superset session and therefore has no CSRF token to present, and there is no session for an attacker to forge against - the same rationale as the token-authenticated chart-data and datasource-query endpoints already listed there. Without it the endpoint returns400 The CSRF token is missingand the flow cannot be used at all. Only thePOSTrequires it;GETis not CSRF-protected.
New model / storage
- A new
KeyValueResourceenum value insuperset/key_value/types.py. No new table - this reuses the existingkey_valuestore,KeyValueDAOandJsonKeyValueCodec.
Interaction with existing auth settings, which operators should be aware of:
AUTH_USER_REGISTRATION(defaultFalse) - an unknown username is rejected with 401 rather than auto-created. With it on, unknown users are provisioned withAUTH_USER_REGISTRATION_ROLE.AUTH_ROLES_SYNC_AT_LOGIN(defaultFalse) - with it off, an existing user's roles are never modified by this flow. With it on, the resolver'srole_keysreplace them on every consumption, bounded byAUTH_ROLES_MAPPING.
No changes to existing visualizations, dashboards, React components, the CLI, or the default deployment. The Embedded SDK and guest-token flow are untouched; this is an additional, independent option.
Required deployment configuration
The flow establishes a normal session cookie, so the usual embedding constraints apply and are worth stating concretely rather than left to the reader:
- Framing. Superset must permit being framed by the parent origin, via
frame-ancestorsinTALISMAN_CONFIG(or the equivalent inHTTP_HEADERS). - Cookies, cross-site. If the parent application is on a different registrable domain,
SESSION_COOKIE_SAMESITE = "None"andSESSION_COOKIE_SECURE = Trueare required - the defaultLaxwithholds the cookie in a cross-site frame, and the user lands on a login page even though the flow itself worked correctly. Browsers rejectSameSite=NonewithoutSecure, so HTTPS is required. - Cookies, same-site. If the parent and Superset share a registrable domain (common in enterprise deployments, e.g. both under
example.com), the defaults are sufficient and none of the above applies. - Third-party cookie deprecation. Even with
SameSite=None, current browsers block third-party cookies for many users. Genuinely cross-domain embedding may additionally require the Storage Access API or a partitioned (CHIPS) cookie. This proposal does not change that, and does not claim to solve it.
New dependencies
None. The feature is built entirely on primitives already in Superset: the key_value store and its DAO/codecs, Flask-AppBuilder's security manager and user provisioning, flask_login.login_user, and the existing safe-redirect utility. No new npm or PyPI
packages are required.
Migration Plan and Compatibility
- No database migration. The feature reuses the existing
key_valuetable; the only schema-level addition is a newKeyValueResourceenum value in code. - Opt-in and backward compatible. Gated behind a feature flag that is off by default, and inert unless an identity resolver is configured. Existing deployments are unaffected; no stored URLs change.
- Rollback is disabling the flag. Any outstanding tokens self-expire via TTL.
Known limitations
Stated explicitly so reviewers can weigh them:
- The resolver is the entire authentication boundary. Whatever it accepts is what Superset will issue a session for. A permissive resolver is a full impersonation vulnerability. This is the same trust placed in an OAuth provider's claims today, but it moves the validation into operator-supplied code, so the documentation must be emphatic.
- The token is a bearer credential for its lifetime. Anyone who obtains it - from a reverse-proxy access log, for instance - can redeem it. Single use and a seconds-long TTL bound the exposure; they do not eliminate it. Operators terminating TLS in front of Superset may wish to redact the
tokenquery parameter from access logs. - It does not solve third-party cookie blocking. See required configuration above.
Rejected Alternatives
1. Use Superset's built-in guest tokens + Embedded SDK. Guest tokens are the closest existing feature: they grant scoped access to specific dashboards with host-supplied row-level security, without the viewer needing a Superset account. They address a different need:
- Host-defined session vs. the real user. A guest token establishes a host-defined guest session (typically a shared
GUEST_ROLE_NAMEwith host-supplied RLS). This works when the host is the source of authorization. Our case is the inverse: the user already exists in Superset with their own roles and RLS, and the requirement is that the same RBAC apply in the embed as when accessing Superset directly - so re-deriving an equivalent session in the host would move authorization out of Superset and duplicate what it already enforces. - Scoped resources vs. full session. Guest tokens scope access to a registered set of embedded resources via
/embedded/<uuid>. This proposal targets the full application surface the user normally has (Explore, SQL Lab, drill-through, links between objects). - Header-carried vs. cookie-carried. Guest tokens are carried as a request header injected by the
superset-embedded-sdk, suited to SDK-managed components. Full-page navigation inside the frame relies on a standard session cookie instead. - Attribution. A shared guest identity attributes activity to the guest rather than the individual; this proposal preserves per-user audit trails.
Guest tokens build a host-defined session for the host; this proposal reflects an existing, fully-provisioned user session into the frame. The two are complementary, and this SIP leaves the guest-token flow untouched.
2. Run the SSO/OAuth redirect flow inside the iframe. This is the root problem: IdPs commonly refuse framing (X-Frame-Options / frame-ancestors), and the redirect chain depends on cookies that browsers now block as third-party in embedded contexts. It does not work reliably across modern browsers.
3. Pass the raw provider token (e.g. the SSO id token) directly in the iframe URL. This leaks a long-lived, broadly-scoped credential into browser history, Referer headers, and server access logs. The one-time, short-lived, opaque token minimizes blast radius and carries no identity data in the URL.
4. Rely on silent auth / shared cookies / postMessage token injection. Fragile under third-party-cookie deprecation and cross-origin restrictions; requires the parent to inject scripts and the browser to permit third-party storage, which is increasingly unavailable.
5. Long-lived API JWT + a fully custom embedded frontend. Superset's server-rendered pages (dashboards, SQL Lab) depend on the session cookie and CSRF protections; a bearer-only approach would require rebuilding those surfaces and still would not give the iframe a real browser session.
6. Split mint and consume across an API route and a browser view route. Rejected: the premise that /api/v1 must be JSON-only does not hold (screenshots return image/png, exports return application/zip), and splitting them doubles the surface to reason about for no benefit.
Source: apache/superset