incorta-auth (Python)
Authenticate Incorta users in Python apps. The Python port of
@incorta/auth — it gates your
app behind the real Incorta login (no custom login page) using the OAuth
2.0 / OIDC authorization server built into Incorta.
To read Incorta with the session this package holds — schemas, tables,
columns, rows, all filtered to the signed-in user — add
incorta-sdk, released from this repository at the
same version.
Both SDKs implement one spec: the same OAuth authorization-code flow against
{INCORTA_URL}/oauth/{tenant}, the same INCORTA_* configuration, and the
same encrypted-cookie sealing (compact JWE, dir + A256GCM, key derived
SHA-256("incorta-auth:" + secret)) — cookies sealed by either language
unseal in the other, verified by fixtures generated with the TS
implementation (tests/fixtures/).
Status
- Core protocol layer: configuration, endpoint discovery with split-horizon support, authorization-code exchange, silent refresh with rotation, RS256/JWKS token verification, session sealing, and RFC 7591/7592 client registration as library functions.
- ASGI / FastAPI adapter:
IncortaAuthMiddlewaregates any ASGI app behind Incorta login and serves/auth/*;incorta_auth.fastapiprovides theget_sessiondependency. Seeexamples/fastapi. - Streamlit adapter:
incorta_auth.streamlit.gate()gates a Streamlit app (which has no routes and no response-cookie control) behind Incorta login. Seeexamples/streamlitand the design notes below.
Usage (FastAPI / any ASGI app)
from fastapi import Depends, FastAPI
from incorta_auth import AuthSession
from incorta_auth.asgi import IncortaAuthMiddleware
from incorta_auth.fastapi import get_session
app = FastAPI()
app.add_middleware(IncortaAuthMiddleware) # config from INCORTA_* env vars
@app.get("/api/me")
def me(session: AuthSession = Depends(get_session)):
return {"user": session.user.sub, "roles": session.user.roles}
# session.access_token → Authorization: Bearer for Incorta REST APIs
The middleware serves {base_path}/{login,callback,session,logout,signed-out}
(+ /token when expose_access_token=True), attaches the session to
request.state.incorta_auth, redirects signed-out browser navigations into
Incorta login (relative Location — proxy-safe), 401s signed-out data
requests, refuses signed-out websockets, and answers everything with an
honest 503 while the INCORTA_* env is missing. public_paths=("/api/health",)
exempts liveness probes from both the gate and the 503. For apps embedded in
a cross-site iframe, spread embedded_cookie_config() into the middleware
(SameSite=None + Partitioned).
Embedded (cross-site iframe) sign-in
When the app is previewed inside a cross-site iframe and the Incorta cluster
has iframe inclusion off (the default — Incorta's login is
X-Frame-Options: SAMEORIGIN and its session cookie is SameSite=Lax), a
signed-out user can't complete Incorta login inside the frame. Pass
embedded_sign_in="new-tab" (or INCORTA_EMBEDDED_SIGN_IN=new-tab) and the
gate serves a small interstitial instead of a doomed in-frame redirect: at
the top level it still redirects straight to Incorta; inside a frame it
offers a "Sign in with Incorta" button that opens the app in a new top-level
tab, where login works. The proper fix remains enabling iframe inclusion on
the cluster — this is the graceful fallback when you can't.
Install
uv add incorta-auth # or: pip install incorta-auth
Prerequisites
Same as the TS SDK (see the repo README): enable the authorization server on the cluster, then register an OAuth client. The registration CLI ships with the npm package —
npx incorta-auth register \
--url http://localhost:8080/incorta \
--tenant demo \
--name "My App" \
--redirect http://localhost:4000/auth/callback
— or use the library functions below from Python.
Usage (core)
Configuration comes from INCORTA_* env vars (INCORTA_URL,
INCORTA_TENANT, INCORTA_CLIENT_ID, INCORTA_CLIENT_SECRET,
INCORTA_AUTH_SECRET, optional INCORTA_APP_URL / INCORTA_INTERNAL_URL /
INCORTA_COOKIE_SAMESITE / INCORTA_COOKIE_PARTITIONED) or explicit
keyword arguments:
from incorta_auth import IncortaAuth
auth = IncortaAuth() # reads INCORTA_* env vars
# Send the browser to Incorta login:
login = auth.begin_login(origin="http://localhost:4000", secure=False, redirect_to="/")
# → redirect to login.location, set login.set_cookies
# Complete the callback (?code&state):
result = auth.complete_callback(
origin="http://localhost:4000", secure=False,
cookie_header=request_cookie_header, query=dict(query_params),
)
# → redirect to result.location, set result.set_cookies
# Gate requests / read the user and token:
session = auth.get_session(request_cookie_header)
if session:
session.user.sub, session.user.roles, session.access_token
# Refresh-aware load (rotates the refresh token, slides the session cookie):
loaded = auth.load_session(cookie_header=request_cookie_header, secure=False)
Client registration (RFC 7591/7592):
from incorta_auth import register_client, get_client, update_client_redirect_uris, delete_client
client = register_client(
incorta_url="http://localhost:8080/incorta", tenant="demo",
name="My App", redirect_uris=["http://localhost:4000/auth/callback"],
)
Usage (Streamlit)
import streamlit as st
import incorta_auth.streamlit as incorta
user = incorta.gate() # first line of the entry script; st.stop()s until signed in
st.write(f"Hello {user.name}", user.roles)
incorta.access_token() # Bearer for Incorta REST APIs (auto-refreshed)
if st.button("Sign out"):
incorta.sign_out() # interstitial — SSO would otherwise bounce right back in
The OAuth redirect_uri is the app root ({INCORTA_APP_URL}/) —
register exactly that. Tokens live only in st.session_state; a hard
refresh silently re-authenticates through Incorta SSO. Multi-page entry
scripts can restore the originally requested page with pop_return_path().
The gate also honours the cross-adapter session cookie
(incorta_auth_session, sealed under INCORTA_AUTH_SECRET exactly like the
ASGI/TS adapters seal it): if the websocket's initial request carries a valid
one, it seeds st.session_state once per browser session — no login round
trip. Streamlit never writes that cookie itself; it is how the platform opens
the app signed in as a given user (e.g. the app-card screenshot capture).
Same trust as the other adapters: only the holder of the app secret can mint
it. It never overrides an existing session and never undoes sign_out().
For an app embedded in a cross-site iframe on a cluster with iframe inclusion
off, pass gate(embedded_sign_in="new-tab") (or INCORTA_EMBEDDED_SIGN_IN=new-tab):
signed out, the gate shows a "Sign in with Incorta" button that opens the app
in a new top-level tab (where login works) instead of bouncing the frame to
an Incorta login it can't render — see the ASGI section's note above.
Streamlit design notes (browser-spike findings, 2026-07-20)
Streamlit cannot host a callback route, set response cookies, or (it turns out) navigate from component JS — the flow is built from the three vehicles a real-browser spike (Playwright, Streamlit 1.59) validated:
- Navigation:
components.htmliframes are sandboxedallow-same-origin allow-scripts …withoutallow-top-navigation, so JS cannot redirect the app.st.htmlstrips both scripts and meta tags (DOMPurify). The one silent vehicle is ast.markdown(unsafe_allow_html=True)meta-refresh, which navigates the app frame itself — allowed everywhere, including inside iframes. - State cookie:
components.htmlJS can write cookies on the app origin (allow-same-origin+ srcdoc). The sealed login state (state/nonce/return target, same JWE sealing as the TS SDK's state cookie) is written in two variants —SameSite=Laxfor top-level tabs andSameSite=None; Secure; Partitionedfor cross-site iframes (the Incorta builder preview; verified in a genuine cross-site embed) — and read back after the round-trip viast.context.cookies. - Ordering: the cookie write reliably precedes the meta navigation
(5/5 in the spike) — both are same-turn DOM operations and the srcdoc
iframe has no network dependency. Binding is strict: a missing state
cookie fails the callback with
state_missing(the same failure a cookie-blocked browser gets from the TS SDK) and offers a retry button.
Compatibility note: the meta-refresh passthrough and the component sandbox flags are Streamlit implementation details — the full flow is pinned by a browser e2e and the AppTest suite, so a Streamlit upgrade that changes either fails loudly, not silently.
Development
uv sync
uv run pytest
uv run ruff check . && uv run ruff format --check .
uv run mypy
The test suite runs against an in-process replica of Incorta's authorization
server (tests/mock_incorta.py, a port of the TS suite's mock-incorta.ts)
served through httpx.MockTransport — one-time 60s codes, rotating refresh
tokens, and split-horizon simulation included. Regenerate the cross-language
sealing fixtures with:
node tests/fixtures/generate-fixtures.mjs > tests/fixtures/sealed-fixtures.json
Metadata
Release files for incorta-auth 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| incorta_auth-0.5.0.tar.gz | 217.8 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| incorta_auth-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 262.0 kB
Release files / incorta_auth-0.5.0.tar.gz
| Download URL | incorta_auth-0.5.0.tar.gz |
|---|---|
| Size | 217.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b416f85bbbbe075902a4e3928b5f2e3ca4e103766bbd244cc0c8780c82dbd930
|
|
BLAKE2b-256 checksum How to use checksums |
c471676dfb5cabba05d1d3cdad3620e4f85d40c7354722226e44588c019e8e31
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / incorta_auth-0.5.0-py3-none-any.whl
| Download URL | incorta_auth-0.5.0-py3-none-any.whl |
|---|---|
| Size | 44.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
45d20140105872251b56a07a6b2faa154b5a6e1ad28f5d34e2213619c2fe8b95
|
|
BLAKE2b-256 checksum How to use checksums |
9d6b6f327e6e7b6f2eff5e8198aab78f054eff08423abf3a6adc4806042b96a5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for incorta-auth 0.5.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.