madhushreeag opened a new issue, #44363:
URL: https://github.com/apache/superset/issues/44363
## [SIP] 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:
1. **The IdP refuses to be framed.** Many identity providers send
`X-Frame-Options: DENY` or a restrictive `Content-Security-Policy:
frame-ancestors`,
so the login redirect cannot render inside the iframe.
2. **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 `userinfo` in Superset's existing key-value store via
`KeyValueDAO`
under a new `KeyValueResource`, 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 `userinfo` by 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 `next` URL.
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:
```python
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:
1. 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.
2. The backend calls `POST /api/v1/security/login-token/` with that id
token. The resolver
validates it (issuer, audience, signature, expiry) and returns the opaque
`access_token`.
3. 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/`.
4. The browser loads the iframe, the token is consumed and deleted, and the
frame now holds
the user's real Superset session.
**Flow diagram:**
```mermaid
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.** `next` is 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 existing
`POST /api/v1/security/login`, and a direct sibling of
`POST /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_TOKEN` feature flag, off by default.
- `LOGIN_TOKEN_IDENTITY_RESOLVER` — callable resolving a request to
`userinfo`; `None` by
default, which leaves the feature inert.
- `LOGIN_TOKEN_TTL_SECONDS` — server-side token lifetime, default 60.
- One entry added to `WTF_CSRF_EXEMPT_LIST` for 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 returns `400 The CSRF token is
missing` and the flow
cannot be used at all. Only the `POST` requires it; `GET` is not
CSRF-protected.
**New model / storage**
- A new `KeyValueResource` enum value in `superset/key_value/types.py`. No
new table — this
reuses the existing `key_value` store, `KeyValueDAO` and
`JsonKeyValueCodec`.
**Interaction with existing auth settings**, which operators should be aware
of:
- `AUTH_USER_REGISTRATION` (default `False`) — an unknown username is
rejected with 401
rather than auto-created. With it on, unknown users are provisioned with
`AUTH_USER_REGISTRATION_ROLE`.
- `AUTH_ROLES_SYNC_AT_LOGIN` (default `False`) — with it off, an existing
user's roles are
never modified by this flow. With it on, the resolver's `role_keys`
replace them on every
consumption, bounded by `AUTH_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-ancestors` in `TALISMAN_CONFIG` (or the equivalent in
`HTTP_HEADERS`).
- **Cookies, cross-site.** If the parent application is on a *different
registrable domain*,
`SESSION_COOKIE_SAMESITE = "None"` and `SESSION_COOKIE_SECURE = True` are
required — the
default `Lax` withholds the cookie in a cross-site frame, and the user
lands on a login
page even though the flow itself worked correctly. Browsers reject
`SameSite=None` without
`Secure`, 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_value`
table; the only
schema-level addition is a new `KeyValueResource` enum 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 `token` query 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_NAME` with 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.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]