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]

Reply via email to