Skip to content

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

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

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

Conversation

@sfc-gh-aamadhavan

Copy link
Copy Markdown

Summary

Adds a native JDBC-driver helper, EmbeddedStreamlit, 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 the session:streamlit:<fqn> scope, then assembling the final embed
URL from the returned single-use authorization code. This is the native replacement for the
SYSTEM$STREAMLIT_GENERATE_EMBED_URL SQL system function, so a customer backend can mint an embed
URL without an interactive SQL session.

DRAFT — depends on the Global Services server-side token-exchange streamlit-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 embed URL using a service credential (PAT / key-pair / session token) without
opening a SQL session. This helper provides that as a credential-only side-channel utility.

Design docs: Streamlit Drivers
and Embedded Streamlit Authentication.

What changed

  • api/streamlit/EmbeddedStreamlit.java (new, public): builder with static factory
    forApp(streamlitId, parentOrigin), credential modes withPat / withConnection /
    withKeyPairJwt plus escape hatches withSubjectToken / withSubjectTokenType, endpoint config
    withAccount / withHost / withTokenEndpoint, and a package-private withHttpSender seam for
    tests. Holds the pure logic: scope build, credential→subject_token_type mapping, endpoint
    derivation, code extraction (fragment-first then query), and URL assembly. Terminal ops
    getEmbedUrl() and getExpiresInSeconds().
  • internal/core/auth/oauth/StreamlitEmbedTokenResponseDTO.java (new): response DTO carrying
    redirect_uri + expires_in (the existing TokenResponseDTO requires access_token and
    cannot represent this response).
  • internal/core/auth/oauth/StreamlitEmbedTokenExchange.java (new, internal facade): builds +
    sends the urlencoded token-exchange POST behind an injectable HttpSender seam (default routes
    through HttpUtil.executeGeneralRequestOmitSnowflakeHeaders), parses the DTO, and extracts the
    session token/server URL from a live connection for withConnection.
  • api/exception/ErrorCode.java + jdbc_error_messages.properties: added
    STREAMLIT_EMBED_URL_ERROR(200075) = Error generating Streamlit embed URL: {0}, a clean
    single-placeholder template (mirrors the existing OAUTH_*_FLOW_ERROR codes) so the module no
    longer overloads positional templates.
  • test/.../api/streamlit/EmbeddedStreamlitTest.java (new): 30 unit tests, HTTP layer mocked.
String embedUrl =
    EmbeddedStreamlit.forApp("MY_DB.MY_SCHEMA.MY_APP", "https://analytics.example.com")
        .withAccount("myaccount")
        .withPat(programmaticAccessToken)   // or .withConnection(conn) / .withKeyPairJwt(preSignedJwt)
        .getEmbedUrl();

Wire contract

POST https://<account>.snowflakecomputing.com/oauth/token — headers
Content-Type: application/x-www-form-urlencoded; charset=UTF-8, Accept: application/json, and
no Authorization (the streamlit embed token-exchange 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 dots only, no :).

subject_token_type mapping (verified against SFOAuthTokenType.java): session →
urn:snowflake:token-type:session; PAT → programmatic_access_token; key-pair JWT (provisional)
urn:ietf:params:oauth:token-type:jwt. WIF (urn:snowflake:token-type:wif) and any future type
are reachable via withSubjectToken(token, type).

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

URL assembly (reproduces the system function): extract the code (fragment first, then query)
verbatim (no percent-decoding, so base64url codes survive); compute
base = scheme://host[:port]/path of redirect_uri with the code and any pre-existing
__embeddedApp/__parentOrigin removed; build
base + "?__parentOrigin=" + urlencode(parentOrigin) + "&__embeddedApp=true" + "#code=" + code
(append with & if base already has a query). The credential and code are treated as secrets and
never logged at info level, nor echoed into error messages.

Server dependency / open questions

  1. GS must add EntityType.STREAMLIT and the session:streamlit:<fqn> scope handler at
    POST /oauth/token before this works end-to-end.
  2. Key-pair subject_token_type is provisional — no GS enum value today; default
    urn:ietf:params:oauth:token-type:jwt is overridable via withSubjectTokenType /
    withSubjectToken. The driver does not sign the JWT (caller supplies a pre-signed one;
    SessionUtilKeyPair is package-private).
  3. The /oauth/token path and redirect_uri code placement are assumptions from the verified
    contract; the endpoint is overridable via withTokenEndpoint.

Testing

EmbeddedStreamlitTest — 30 JUnit 5 tests, HTTP mocked via an injectable HttpSender: each
credential mode + correct subject_token_type (PAT / key-pair-provisional / override /
escape-hatch / session); scope; headers incl. the exact Content-Type and the absence of
an Authorization header; code extraction from both fragment and query (incl. fragment precedence
and not-first-param); URL assembly from clean base, query-carried code, and base-with-query
(managed params stripped); endpoint derivation from account and scheme-less host; insecure-http://
warn branch; withConnection session-token extraction via a mocked SnowflakeConnectionImpl/
SFSession plus non-Impl rejection; getExpiresInSeconds() throwing before getEmbedUrl();
end-to-end URL + expires_in; and error cases.

Commands run (observed):

mvn -q -o -DskipTests compile                                  => PASS (exit 0)
mvn -q -o -Dtest=EmbeddedStreamlitTest -DfailIfNoTests=false test
                                                               => Tests run: 30, Failures: 0, Errors: 0
mvn com.spotify.fmt:fmt-maven-plugin:check                     => PASS after :format (780 files, 0 non-complying)

No full mvn verify/package or integration tests (heavy; the GS streamlit-scope endpoint is not
in prod). The default production sender path (FAIL_OPEN + executeGeneralRequestOmitSnowflakeHeaders,
sfSession=null) is verified by reading (matches the WorkloadIdentityUtil pattern); it needs a
live endpoint to exercise and will be validated when the GS endpoint lands.

Out of scope

  • Live integration / e2e tests (the GS token-exchange streamlit-scope endpoint is not in production).
  • Other drivers (Go / .NET / ODBC; Python and Node.js have sibling draft PRs).
  • Removing/deprecating the SYSTEM$STREAMLIT_GENERATE_EMBED_URL SQL system function.
  • Key-pair JWT signing inside the driver (caller passes a pre-signed JWT).

🤖 Generated with Claude Code

…uth token-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 api/streamlit/EmbeddedStreamlit.java, the internal token-exchange facade + response DTO, a dedicated ErrorCode, and 30 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