Metadata-Version: 2.4
Name: incorta-auth
Version: 0.1.0
Summary: Authenticate Incorta users in Python apps via Incorta's built-in OAuth 2.0 / OIDC authorization server
Project-URL: Repository, https://github.com/Incorta/IncortaAuthSDK
Author: Incorta
License-Expression: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: jwcrypto>=1.5.6
Requires-Dist: pyjwt[crypto]>=2.8
Description-Content-Type: text/markdown

# incorta-auth (Python)

[![PyPI version](https://img.shields.io/pypi/v/incorta-auth)](https://pypi.org/project/incorta-auth/)
[![license](https://img.shields.io/badge/license-MIT-blue)](../../LICENSE)

Authenticate Incorta users in Python apps. The Python port of
[`@incorta/auth`](https://www.npmjs.com/package/@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.

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**: `IncortaAuthMiddleware` gates any ASGI app
  behind Incorta login and serves `/auth/*`; `incorta_auth.fastapi` provides
  the `get_session` dependency. See `examples/fastapi`.
- **Streamlit adapter**: `incorta_auth.streamlit.gate()` gates a Streamlit
  app (which has no routes and no response-cookie control) behind Incorta
  login. See `examples/streamlit` and the design notes below.

## Usage (FastAPI / any ASGI app)

```python
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).

## Install

```bash
uv add incorta-auth        # or: pip install incorta-auth
```

## Prerequisites

Same as the TS SDK (see the [repo README](../../README.md)): enable the
authorization server on the cluster, then register an OAuth client. The
registration CLI ships with the npm package —

```bash
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:

```python
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):

```python
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)

```python
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()`.

### 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:

1. **Navigation**: `components.html` iframes are sandboxed
   `allow-same-origin allow-scripts …` **without** `allow-top-navigation`,
   so JS cannot redirect the app. `st.html` strips both scripts and meta
   tags (DOMPurify). The one silent vehicle is a
   `st.markdown(unsafe_allow_html=True)` **meta-refresh**, which navigates
   the app frame itself — allowed everywhere, including inside iframes.
2. **State cookie**: `components.html` JS **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=Lax` for top-level tabs
   and `SameSite=None; Secure; Partitioned` for cross-site iframes (the
   Incorta builder preview; verified in a genuine cross-site embed) — and
   read back after the round-trip via `st.context.cookies`.
3. **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

```bash
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:

```bash
node tests/fixtures/generate-fixtures.mjs > tests/fixtures/sealed-fixtures.json
```
