Skip to content

Add native EmbeddedStreamlit helper to generate SiS embed URLs via OAuth token-exchange#2913

Draft
sfc-gh-aamadhavan wants to merge 1 commit into
snowflakedb:mainfrom
sfc-gh-aamadhavan:streamlit-embed-token-exchange
Draft

Add native EmbeddedStreamlit helper to generate SiS embed URLs via OAuth token-exchange#2913
sfc-gh-aamadhavan wants to merge 1 commit into
snowflakedb:mainfrom
sfc-gh-aamadhavan:streamlit-embed-token-exchange

Conversation

@sfc-gh-aamadhavan

Copy link
Copy Markdown

Summary

Adds a native EmbeddedStreamlit helper to the Python connector that generates a
Streamlit-in-Snowflake (SiS) embed URL by calling Snowflake's OAuth 2.0 token-exchange
endpoint (POST /oauth/token) with a service credential and assembling the final embed
URL. This is the native, session-free replacement for the
SYSTEM$STREAMLIT_GENERATE_EMBED_URL SQL system function.

DRAFT — depends on the GlobalServices server-side token-exchange session:streamlit:<fqn>
scope endpoint, which is not yet in production. All tests mock the HTTP layer.

Motivation

Customer backends (e.g. analytics.bmw.com) embed SiS apps into 3rd-party pages and must
mint a short-lived, single-use embed URL from a service credential (PAT / key-pair /
session) without opening an interactive SQL session to call the system function. This
helper performs the token-exchange directly from the driver and never exposes the service
credential to the browser.

Design docs: Streamlit Drivers
and Embedded Streamlit Authentication.

What changed

  • src/snowflake/connector/embedded_streamlit.py (new): class EmbeddedStreamlit.
  • src/snowflake/connector/__init__.py: export EmbeddedStreamlit (added to __all__).
  • test/unit/test_embedded_streamlit.py (new): 29 unit tests, HTTP layer fully mocked.
from snowflake.connector import EmbeddedStreamlit

url = (
    EmbeddedStreamlit("MYDB.MYSCHEMA.MY_APP", "https://analytics.example.com")
    .prepare(pat="<pat>", account="myaccount")   # or conn=..., key_pair=..., or subject_token=...
    .get_embed_url()
)

prepare() takes exactly one primary credential source (pat | conn | key_pair |
explicit subject_token), plus account/host/user as needed for credential-only modes,
and an overridable token_endpoint / session_manager for tests. HTTP goes through the
connector's SessionManager; the key-pair JWT is minted via AuthByKeyPair.prepare(account=, user=)
(or a pre-signed JWT may be passed); conn mode pulls the session token and host from the live
SnowflakeConnection (conn.rest.token, conn.host).

Wire contract

POST https://<host>/oauth/tokenContent-Type: application/x-www-form-urlencoded; charset=UTF-8,
Accept: application/json, and crucially no Authorization header (the streamlit embed
token-exchange path has no OAuth client identity). Body:
grant_type=urn:ietf:params:oauth:grant-type:token-exchange, subject_token=<credential>,
subject_token_type=<URN>, scope=session:streamlit:<db.schema.app> (FQN contains no :).

subject_token_type mapping (verified against GS SFOAuthTokenType.java):

Credential subject_token_type
session urn:snowflake:token-type:session
PAT programmatic_access_token
WIF urn:snowflake:token-type:wif
OAuth access token urn:ietf:params:oauth:token-type:access_token
key-pair JWT urn:ietf:params:oauth:token-type:jwt (provisional, overridable)

Response 200 { "redirect_uri": "...", "expires_in": <int> }; the single-use code may be a
fragment (#code=) or query (?code= / &code=).

URL assembly reproduces StreamlitGenerateEmbedUrl.java: extract the code (fragment first),
compute the base (scheme://host[:port]/path) minus the code and any echoed
__embeddedApp/__parentOrigin, then build
base + "?__parentOrigin=" + urlencode(parentOrigin) + "&__embeddedApp=true" + "#code=" + code
(joining with & when the base already has a query). The authorization code is read
verbatim (raw split on code=), not form-decoded — the GS system function emits it via
URIBuilder.setFragment("code=" + azCode) with no percent-encoding, and base64url codes can
contain +///=; form-decoding would turn + into a space and corrupt the single-use code.
The credential and the code are treated as secrets and never logged.

Server dependency / open questions

  1. GS must add EntityType.STREAMLIT and the session:streamlit:<fqn> scope handler to the
    token-exchange path before this works against a real account (not in prod yet).
  2. The key-pair subject_token_type is provisional — there is no GS enum value for a
    key-pair JWT today; the default urn:ietf:params:oauth:token-type:jwt is overridable, and
    an explicit (subject_token, subject_token_type) escape hatch is provided.
  3. To confirm with GS: the /oauth/token path and that the single-use azcode is delivered via
    redirect_uri (fragment or query) un-percent-encoded — the driver reads the code verbatim,
    matching the system function's fragment output.

Testing

test/unit/test_embedded_streamlit.py — 29 tests, HTTP mocked via an injected fake
SessionManager: each credential mode + subject_token_type; scope string; endpoint, headers,
and absence of any Authorization header; default-endpoint-from-account; code extraction from
fragment, query, and &code= with fragment precedence; regression tests that a code with
+///= round-trips verbatim
from both fragment and query; URL assembly (parentOrigin
url-encoding, __embeddedApp=true, base-with-query, stripping echoed managed params, evil-origin
negative check); expires_in coercion; and the full error matrix.

Commands run (observed):

PYTHONPATH=src python3 -m pytest test/unit/test_embedded_streamlit.py -q   => 29 passed
python3 -m black --safe --check ...                                        => unchanged
python3 -m isort --check-only ...                                          => exit 0
python3 -m flake8 ...                                                      => exit 0

Run with PYTHONPATH=src (package not pip-installed here); the benign Failed to import ArrowResult warning is unrelated (Arrow C-ext not compiled). No live integration/e2e — the GS
endpoint is not in production yet.

Out of scope

  • Live integration / e2e tests (GS server endpoint not in production).
  • Other drivers (Go / .NET / ODBC / JDBC / Node.js — JDBC and Node have sibling draft PRs).
  • Removing the SYSTEM$STREAMLIT_GENERATE_EMBED_URL SQL system function.

🤖 Generated with Claude Code

…en-exchange

Native, session-free replacement for the SYSTEM$STREAMLIT_GENERATE_EMBED_URL SQL system function: exchanges a service credential (PAT / key-pair / session) at POST /oauth/token for a single-use embed authorization code and assembles the Streamlit embed URL. DRAFT - depends on the GS server-side session:streamlit:<fqn> token-exchange endpoint (not yet in prod). Adds embedded_streamlit.py, the __all__ export, and 29 unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant