incorta-auth 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. incorta_auth-0.1.0/.gitignore +6 -0
  2. incorta_auth-0.1.0/.python-version +1 -0
  3. incorta_auth-0.1.0/PKG-INFO +210 -0
  4. incorta_auth-0.1.0/README.md +189 -0
  5. incorta_auth-0.1.0/pyproject.toml +73 -0
  6. incorta_auth-0.1.0/src/incorta_auth/__init__.py +77 -0
  7. incorta_auth-0.1.0/src/incorta_auth/_time.py +11 -0
  8. incorta_auth-0.1.0/src/incorta_auth/asgi.py +438 -0
  9. incorta_auth-0.1.0/src/incorta_auth/auth.py +69 -0
  10. incorta_auth-0.1.0/src/incorta_auth/config.py +226 -0
  11. incorta_auth-0.1.0/src/incorta_auth/cookies.py +79 -0
  12. incorta_auth-0.1.0/src/incorta_auth/discovery.py +80 -0
  13. incorta_auth-0.1.0/src/incorta_auth/errors.py +6 -0
  14. incorta_auth-0.1.0/src/incorta_auth/fastapi.py +37 -0
  15. incorta_auth-0.1.0/src/incorta_auth/flow.py +289 -0
  16. incorta_auth-0.1.0/src/incorta_auth/jwks.py +73 -0
  17. incorta_auth-0.1.0/src/incorta_auth/py.typed +0 -0
  18. incorta_auth-0.1.0/src/incorta_auth/register.py +232 -0
  19. incorta_auth-0.1.0/src/incorta_auth/session.py +195 -0
  20. incorta_auth-0.1.0/src/incorta_auth/streamlit.py +386 -0
  21. incorta_auth-0.1.0/src/incorta_auth/tokens.py +165 -0
  22. incorta_auth-0.1.0/tests/__init__.py +0 -0
  23. incorta_auth-0.1.0/tests/conftest.py +33 -0
  24. incorta_auth-0.1.0/tests/fixtures/generate-fixtures.mjs +71 -0
  25. incorta_auth-0.1.0/tests/fixtures/sealed-fixtures.json +30 -0
  26. incorta_auth-0.1.0/tests/mock_incorta.py +434 -0
  27. incorta_auth-0.1.0/tests/test_asgi.py +231 -0
  28. incorta_auth-0.1.0/tests/test_config.py +103 -0
  29. incorta_auth-0.1.0/tests/test_flow.py +315 -0
  30. incorta_auth-0.1.0/tests/test_mock_quirks.py +83 -0
  31. incorta_auth-0.1.0/tests/test_register.py +169 -0
  32. incorta_auth-0.1.0/tests/test_session.py +164 -0
  33. incorta_auth-0.1.0/tests/test_split_horizon.py +143 -0
  34. incorta_auth-0.1.0/tests/test_streamlit.py +282 -0
  35. incorta_auth-0.1.0/tests/util.py +68 -0
  36. incorta_auth-0.1.0/uv.lock +2339 -0
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.4
2
+ Name: incorta-auth
3
+ Version: 0.1.0
4
+ Summary: Authenticate Incorta users in Python apps via Incorta's built-in OAuth 2.0 / OIDC authorization server
5
+ Project-URL: Repository, https://github.com/Incorta/IncortaAuthSDK
6
+ Author: Incorta
7
+ License-Expression: MIT
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Security
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: httpx>=0.27
18
+ Requires-Dist: jwcrypto>=1.5.6
19
+ Requires-Dist: pyjwt[crypto]>=2.8
20
+ Description-Content-Type: text/markdown
21
+
22
+ # incorta-auth (Python)
23
+
24
+ [![PyPI version](https://img.shields.io/pypi/v/incorta-auth)](https://pypi.org/project/incorta-auth/)
25
+ [![license](https://img.shields.io/badge/license-MIT-blue)](../../LICENSE)
26
+
27
+ Authenticate Incorta users in Python apps. The Python port of
28
+ [`@incorta/auth`](https://www.npmjs.com/package/@incorta/auth) — it gates your
29
+ app behind the **real Incorta login** (no custom login page) using the OAuth
30
+ 2.0 / OIDC authorization server built into Incorta.
31
+
32
+ Both SDKs implement one spec: the same OAuth authorization-code flow against
33
+ `{INCORTA_URL}/oauth/{tenant}`, the same `INCORTA_*` configuration, and the
34
+ same encrypted-cookie sealing (compact JWE, `dir` + A256GCM, key derived
35
+ `SHA-256("incorta-auth:" + secret)`) — cookies sealed by either language
36
+ unseal in the other, verified by fixtures generated with the TS
37
+ implementation (`tests/fixtures/`).
38
+
39
+ ## Status
40
+
41
+ - **Core protocol layer**: configuration, endpoint discovery with
42
+ split-horizon support, authorization-code exchange, silent refresh with
43
+ rotation, RS256/JWKS token verification, session sealing, and RFC 7591/7592
44
+ client registration as library functions.
45
+ - **ASGI / FastAPI adapter**: `IncortaAuthMiddleware` gates any ASGI app
46
+ behind Incorta login and serves `/auth/*`; `incorta_auth.fastapi` provides
47
+ the `get_session` dependency. See `examples/fastapi`.
48
+ - **Streamlit adapter**: `incorta_auth.streamlit.gate()` gates a Streamlit
49
+ app (which has no routes and no response-cookie control) behind Incorta
50
+ login. See `examples/streamlit` and the design notes below.
51
+
52
+ ## Usage (FastAPI / any ASGI app)
53
+
54
+ ```python
55
+ from fastapi import Depends, FastAPI
56
+ from incorta_auth import AuthSession
57
+ from incorta_auth.asgi import IncortaAuthMiddleware
58
+ from incorta_auth.fastapi import get_session
59
+
60
+ app = FastAPI()
61
+ app.add_middleware(IncortaAuthMiddleware) # config from INCORTA_* env vars
62
+
63
+ @app.get("/api/me")
64
+ def me(session: AuthSession = Depends(get_session)):
65
+ return {"user": session.user.sub, "roles": session.user.roles}
66
+ # session.access_token → Authorization: Bearer for Incorta REST APIs
67
+ ```
68
+
69
+ The middleware serves `{base_path}/{login,callback,session,logout,signed-out}`
70
+ (+ `/token` when `expose_access_token=True`), attaches the session to
71
+ `request.state.incorta_auth`, redirects signed-out browser navigations into
72
+ Incorta login (relative `Location` — proxy-safe), 401s signed-out data
73
+ requests, refuses signed-out websockets, and answers everything with an
74
+ honest 503 while the `INCORTA_*` env is missing. `public_paths=("/api/health",)`
75
+ exempts liveness probes from both the gate and the 503. For apps embedded in
76
+ a cross-site iframe, spread `embedded_cookie_config()` into the middleware
77
+ (SameSite=None + Partitioned).
78
+
79
+ ## Install
80
+
81
+ ```bash
82
+ uv add incorta-auth # or: pip install incorta-auth
83
+ ```
84
+
85
+ ## Prerequisites
86
+
87
+ Same as the TS SDK (see the [repo README](../../README.md)): enable the
88
+ authorization server on the cluster, then register an OAuth client. The
89
+ registration CLI ships with the npm package —
90
+
91
+ ```bash
92
+ npx incorta-auth register \
93
+ --url http://localhost:8080/incorta \
94
+ --tenant demo \
95
+ --name "My App" \
96
+ --redirect http://localhost:4000/auth/callback
97
+ ```
98
+
99
+ — or use the library functions below from Python.
100
+
101
+ ## Usage (core)
102
+
103
+ Configuration comes from `INCORTA_*` env vars (`INCORTA_URL`,
104
+ `INCORTA_TENANT`, `INCORTA_CLIENT_ID`, `INCORTA_CLIENT_SECRET`,
105
+ `INCORTA_AUTH_SECRET`, optional `INCORTA_APP_URL` / `INCORTA_INTERNAL_URL` /
106
+ `INCORTA_COOKIE_SAMESITE` / `INCORTA_COOKIE_PARTITIONED`) or explicit
107
+ keyword arguments:
108
+
109
+ ```python
110
+ from incorta_auth import IncortaAuth
111
+
112
+ auth = IncortaAuth() # reads INCORTA_* env vars
113
+
114
+ # Send the browser to Incorta login:
115
+ login = auth.begin_login(origin="http://localhost:4000", secure=False, redirect_to="/")
116
+ # → redirect to login.location, set login.set_cookies
117
+
118
+ # Complete the callback (?code&state):
119
+ result = auth.complete_callback(
120
+ origin="http://localhost:4000", secure=False,
121
+ cookie_header=request_cookie_header, query=dict(query_params),
122
+ )
123
+ # → redirect to result.location, set result.set_cookies
124
+
125
+ # Gate requests / read the user and token:
126
+ session = auth.get_session(request_cookie_header)
127
+ if session:
128
+ session.user.sub, session.user.roles, session.access_token
129
+
130
+ # Refresh-aware load (rotates the refresh token, slides the session cookie):
131
+ loaded = auth.load_session(cookie_header=request_cookie_header, secure=False)
132
+ ```
133
+
134
+ Client registration (RFC 7591/7592):
135
+
136
+ ```python
137
+ from incorta_auth import register_client, get_client, update_client_redirect_uris, delete_client
138
+
139
+ client = register_client(
140
+ incorta_url="http://localhost:8080/incorta", tenant="demo",
141
+ name="My App", redirect_uris=["http://localhost:4000/auth/callback"],
142
+ )
143
+ ```
144
+
145
+ ## Usage (Streamlit)
146
+
147
+ ```python
148
+ import streamlit as st
149
+ import incorta_auth.streamlit as incorta
150
+
151
+ user = incorta.gate() # first line of the entry script; st.stop()s until signed in
152
+ st.write(f"Hello {user.name}", user.roles)
153
+ incorta.access_token() # Bearer for Incorta REST APIs (auto-refreshed)
154
+ if st.button("Sign out"):
155
+ incorta.sign_out() # interstitial — SSO would otherwise bounce right back in
156
+ ```
157
+
158
+ The OAuth `redirect_uri` is the app **root** (`{INCORTA_APP_URL}/`) —
159
+ register exactly that. Tokens live only in `st.session_state`; a hard
160
+ refresh silently re-authenticates through Incorta SSO. Multi-page entry
161
+ scripts can restore the originally requested page with `pop_return_path()`.
162
+
163
+ ### Streamlit design notes (browser-spike findings, 2026-07-20)
164
+
165
+ Streamlit cannot host a callback route, set response cookies, or (it turns
166
+ out) navigate from component JS — the flow is built from the three vehicles
167
+ a real-browser spike (Playwright, Streamlit 1.59) validated:
168
+
169
+ 1. **Navigation**: `components.html` iframes are sandboxed
170
+ `allow-same-origin allow-scripts …` **without** `allow-top-navigation`,
171
+ so JS cannot redirect the app. `st.html` strips both scripts and meta
172
+ tags (DOMPurify). The one silent vehicle is a
173
+ `st.markdown(unsafe_allow_html=True)` **meta-refresh**, which navigates
174
+ the app frame itself — allowed everywhere, including inside iframes.
175
+ 2. **State cookie**: `components.html` JS **can** write cookies on the app
176
+ origin (`allow-same-origin` + srcdoc). The sealed login state
177
+ (state/nonce/return target, same JWE sealing as the TS SDK's state
178
+ cookie) is written in two variants — `SameSite=Lax` for top-level tabs
179
+ and `SameSite=None; Secure; Partitioned` for cross-site iframes (the
180
+ Incorta builder preview; verified in a genuine cross-site embed) — and
181
+ read back after the round-trip via `st.context.cookies`.
182
+ 3. **Ordering**: the cookie write reliably precedes the meta navigation
183
+ (5/5 in the spike) — both are same-turn DOM operations and the srcdoc
184
+ iframe has no network dependency. Binding is **strict**: a missing state
185
+ cookie fails the callback with `state_missing` (the same failure a
186
+ cookie-blocked browser gets from the TS SDK) and offers a retry button.
187
+
188
+ Compatibility note: the meta-refresh passthrough and the component sandbox
189
+ flags are Streamlit implementation details — the full flow is pinned by a
190
+ browser e2e and the AppTest suite, so a Streamlit upgrade that changes
191
+ either fails loudly, not silently.
192
+
193
+ ## Development
194
+
195
+ ```bash
196
+ uv sync
197
+ uv run pytest
198
+ uv run ruff check . && uv run ruff format --check .
199
+ uv run mypy
200
+ ```
201
+
202
+ The test suite runs against an in-process replica of Incorta's authorization
203
+ server (`tests/mock_incorta.py`, a port of the TS suite's `mock-incorta.ts`)
204
+ served through `httpx.MockTransport` — one-time 60s codes, rotating refresh
205
+ tokens, and split-horizon simulation included. Regenerate the cross-language
206
+ sealing fixtures with:
207
+
208
+ ```bash
209
+ node tests/fixtures/generate-fixtures.mjs > tests/fixtures/sealed-fixtures.json
210
+ ```
@@ -0,0 +1,189 @@
1
+ # incorta-auth (Python)
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/incorta-auth)](https://pypi.org/project/incorta-auth/)
4
+ [![license](https://img.shields.io/badge/license-MIT-blue)](../../LICENSE)
5
+
6
+ Authenticate Incorta users in Python apps. The Python port of
7
+ [`@incorta/auth`](https://www.npmjs.com/package/@incorta/auth) — it gates your
8
+ app behind the **real Incorta login** (no custom login page) using the OAuth
9
+ 2.0 / OIDC authorization server built into Incorta.
10
+
11
+ Both SDKs implement one spec: the same OAuth authorization-code flow against
12
+ `{INCORTA_URL}/oauth/{tenant}`, the same `INCORTA_*` configuration, and the
13
+ same encrypted-cookie sealing (compact JWE, `dir` + A256GCM, key derived
14
+ `SHA-256("incorta-auth:" + secret)`) — cookies sealed by either language
15
+ unseal in the other, verified by fixtures generated with the TS
16
+ implementation (`tests/fixtures/`).
17
+
18
+ ## Status
19
+
20
+ - **Core protocol layer**: configuration, endpoint discovery with
21
+ split-horizon support, authorization-code exchange, silent refresh with
22
+ rotation, RS256/JWKS token verification, session sealing, and RFC 7591/7592
23
+ client registration as library functions.
24
+ - **ASGI / FastAPI adapter**: `IncortaAuthMiddleware` gates any ASGI app
25
+ behind Incorta login and serves `/auth/*`; `incorta_auth.fastapi` provides
26
+ the `get_session` dependency. See `examples/fastapi`.
27
+ - **Streamlit adapter**: `incorta_auth.streamlit.gate()` gates a Streamlit
28
+ app (which has no routes and no response-cookie control) behind Incorta
29
+ login. See `examples/streamlit` and the design notes below.
30
+
31
+ ## Usage (FastAPI / any ASGI app)
32
+
33
+ ```python
34
+ from fastapi import Depends, FastAPI
35
+ from incorta_auth import AuthSession
36
+ from incorta_auth.asgi import IncortaAuthMiddleware
37
+ from incorta_auth.fastapi import get_session
38
+
39
+ app = FastAPI()
40
+ app.add_middleware(IncortaAuthMiddleware) # config from INCORTA_* env vars
41
+
42
+ @app.get("/api/me")
43
+ def me(session: AuthSession = Depends(get_session)):
44
+ return {"user": session.user.sub, "roles": session.user.roles}
45
+ # session.access_token → Authorization: Bearer for Incorta REST APIs
46
+ ```
47
+
48
+ The middleware serves `{base_path}/{login,callback,session,logout,signed-out}`
49
+ (+ `/token` when `expose_access_token=True`), attaches the session to
50
+ `request.state.incorta_auth`, redirects signed-out browser navigations into
51
+ Incorta login (relative `Location` — proxy-safe), 401s signed-out data
52
+ requests, refuses signed-out websockets, and answers everything with an
53
+ honest 503 while the `INCORTA_*` env is missing. `public_paths=("/api/health",)`
54
+ exempts liveness probes from both the gate and the 503. For apps embedded in
55
+ a cross-site iframe, spread `embedded_cookie_config()` into the middleware
56
+ (SameSite=None + Partitioned).
57
+
58
+ ## Install
59
+
60
+ ```bash
61
+ uv add incorta-auth # or: pip install incorta-auth
62
+ ```
63
+
64
+ ## Prerequisites
65
+
66
+ Same as the TS SDK (see the [repo README](../../README.md)): enable the
67
+ authorization server on the cluster, then register an OAuth client. The
68
+ registration CLI ships with the npm package —
69
+
70
+ ```bash
71
+ npx incorta-auth register \
72
+ --url http://localhost:8080/incorta \
73
+ --tenant demo \
74
+ --name "My App" \
75
+ --redirect http://localhost:4000/auth/callback
76
+ ```
77
+
78
+ — or use the library functions below from Python.
79
+
80
+ ## Usage (core)
81
+
82
+ Configuration comes from `INCORTA_*` env vars (`INCORTA_URL`,
83
+ `INCORTA_TENANT`, `INCORTA_CLIENT_ID`, `INCORTA_CLIENT_SECRET`,
84
+ `INCORTA_AUTH_SECRET`, optional `INCORTA_APP_URL` / `INCORTA_INTERNAL_URL` /
85
+ `INCORTA_COOKIE_SAMESITE` / `INCORTA_COOKIE_PARTITIONED`) or explicit
86
+ keyword arguments:
87
+
88
+ ```python
89
+ from incorta_auth import IncortaAuth
90
+
91
+ auth = IncortaAuth() # reads INCORTA_* env vars
92
+
93
+ # Send the browser to Incorta login:
94
+ login = auth.begin_login(origin="http://localhost:4000", secure=False, redirect_to="/")
95
+ # → redirect to login.location, set login.set_cookies
96
+
97
+ # Complete the callback (?code&state):
98
+ result = auth.complete_callback(
99
+ origin="http://localhost:4000", secure=False,
100
+ cookie_header=request_cookie_header, query=dict(query_params),
101
+ )
102
+ # → redirect to result.location, set result.set_cookies
103
+
104
+ # Gate requests / read the user and token:
105
+ session = auth.get_session(request_cookie_header)
106
+ if session:
107
+ session.user.sub, session.user.roles, session.access_token
108
+
109
+ # Refresh-aware load (rotates the refresh token, slides the session cookie):
110
+ loaded = auth.load_session(cookie_header=request_cookie_header, secure=False)
111
+ ```
112
+
113
+ Client registration (RFC 7591/7592):
114
+
115
+ ```python
116
+ from incorta_auth import register_client, get_client, update_client_redirect_uris, delete_client
117
+
118
+ client = register_client(
119
+ incorta_url="http://localhost:8080/incorta", tenant="demo",
120
+ name="My App", redirect_uris=["http://localhost:4000/auth/callback"],
121
+ )
122
+ ```
123
+
124
+ ## Usage (Streamlit)
125
+
126
+ ```python
127
+ import streamlit as st
128
+ import incorta_auth.streamlit as incorta
129
+
130
+ user = incorta.gate() # first line of the entry script; st.stop()s until signed in
131
+ st.write(f"Hello {user.name}", user.roles)
132
+ incorta.access_token() # Bearer for Incorta REST APIs (auto-refreshed)
133
+ if st.button("Sign out"):
134
+ incorta.sign_out() # interstitial — SSO would otherwise bounce right back in
135
+ ```
136
+
137
+ The OAuth `redirect_uri` is the app **root** (`{INCORTA_APP_URL}/`) —
138
+ register exactly that. Tokens live only in `st.session_state`; a hard
139
+ refresh silently re-authenticates through Incorta SSO. Multi-page entry
140
+ scripts can restore the originally requested page with `pop_return_path()`.
141
+
142
+ ### Streamlit design notes (browser-spike findings, 2026-07-20)
143
+
144
+ Streamlit cannot host a callback route, set response cookies, or (it turns
145
+ out) navigate from component JS — the flow is built from the three vehicles
146
+ a real-browser spike (Playwright, Streamlit 1.59) validated:
147
+
148
+ 1. **Navigation**: `components.html` iframes are sandboxed
149
+ `allow-same-origin allow-scripts …` **without** `allow-top-navigation`,
150
+ so JS cannot redirect the app. `st.html` strips both scripts and meta
151
+ tags (DOMPurify). The one silent vehicle is a
152
+ `st.markdown(unsafe_allow_html=True)` **meta-refresh**, which navigates
153
+ the app frame itself — allowed everywhere, including inside iframes.
154
+ 2. **State cookie**: `components.html` JS **can** write cookies on the app
155
+ origin (`allow-same-origin` + srcdoc). The sealed login state
156
+ (state/nonce/return target, same JWE sealing as the TS SDK's state
157
+ cookie) is written in two variants — `SameSite=Lax` for top-level tabs
158
+ and `SameSite=None; Secure; Partitioned` for cross-site iframes (the
159
+ Incorta builder preview; verified in a genuine cross-site embed) — and
160
+ read back after the round-trip via `st.context.cookies`.
161
+ 3. **Ordering**: the cookie write reliably precedes the meta navigation
162
+ (5/5 in the spike) — both are same-turn DOM operations and the srcdoc
163
+ iframe has no network dependency. Binding is **strict**: a missing state
164
+ cookie fails the callback with `state_missing` (the same failure a
165
+ cookie-blocked browser gets from the TS SDK) and offers a retry button.
166
+
167
+ Compatibility note: the meta-refresh passthrough and the component sandbox
168
+ flags are Streamlit implementation details — the full flow is pinned by a
169
+ browser e2e and the AppTest suite, so a Streamlit upgrade that changes
170
+ either fails loudly, not silently.
171
+
172
+ ## Development
173
+
174
+ ```bash
175
+ uv sync
176
+ uv run pytest
177
+ uv run ruff check . && uv run ruff format --check .
178
+ uv run mypy
179
+ ```
180
+
181
+ The test suite runs against an in-process replica of Incorta's authorization
182
+ server (`tests/mock_incorta.py`, a port of the TS suite's `mock-incorta.ts`)
183
+ served through `httpx.MockTransport` — one-time 60s codes, rotating refresh
184
+ tokens, and split-horizon simulation included. Regenerate the cross-language
185
+ sealing fixtures with:
186
+
187
+ ```bash
188
+ node tests/fixtures/generate-fixtures.mjs > tests/fixtures/sealed-fixtures.json
189
+ ```
@@ -0,0 +1,73 @@
1
+ [project]
2
+ name = "incorta-auth"
3
+ # Placeholder — the real version is computed from git history by
4
+ # semantic-release and stamped here (uv version) only in CI at publish time.
5
+ # See release.python.config.cjs; release tags are py-v{version}.
6
+ version = "0.1.0"
7
+ description = "Authenticate Incorta users in Python apps via Incorta's built-in OAuth 2.0 / OIDC authorization server"
8
+ readme = "README.md"
9
+ license = "MIT"
10
+ authors = [{ name = "Incorta" }]
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "httpx>=0.27",
14
+ "pyjwt[crypto]>=2.8",
15
+ "jwcrypto>=1.5.6",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Developers",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Security",
26
+ ]
27
+
28
+ [project.urls]
29
+ Repository = "https://github.com/Incorta/IncortaAuthSDK"
30
+
31
+ [dependency-groups]
32
+ dev = [
33
+ "pytest>=8.3",
34
+ "ruff>=0.8",
35
+ "mypy>=1.13",
36
+ # For the adapter tests (TestClient incl. websockets) and typing the
37
+ # incorta_auth.fastapi module; the published package does NOT depend on it.
38
+ "fastapi>=0.115",
39
+ "streamlit>=1.44",
40
+ ]
41
+
42
+ [build-system]
43
+ requires = ["hatchling"]
44
+ build-backend = "hatchling.build"
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/incorta_auth"]
48
+
49
+ [tool.pytest.ini_options]
50
+ testpaths = ["tests"]
51
+
52
+ [tool.ruff]
53
+ line-length = 100
54
+ target-version = "py310"
55
+
56
+ [tool.ruff.lint]
57
+ select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF"]
58
+
59
+ [tool.mypy]
60
+ python_version = "3.10"
61
+ strict = true
62
+ files = ["src"]
63
+
64
+ [[tool.mypy.overrides]]
65
+ module = "jwcrypto.*"
66
+ ignore_missing_imports = true
67
+
68
+ # Streamlit's dependency tree (numpy etc.) ships 3.12-only stub syntax that
69
+ # breaks analysis under our 3.10 floor — treat the framework as untyped.
70
+ [[tool.mypy.overrides]]
71
+ module = "streamlit.*"
72
+ follow_imports = "skip"
73
+ ignore_missing_imports = true
@@ -0,0 +1,77 @@
1
+ """Authenticate Incorta users in Python apps.
2
+
3
+ Python port of ``@incorta/auth`` — OAuth 2.0 / OIDC against the authorization
4
+ server built into Incorta, with sessions sealed in encrypted cookies that
5
+ interop with the TS SDK.
6
+ """
7
+
8
+ from importlib.metadata import PackageNotFoundError
9
+ from importlib.metadata import version as _metadata_version
10
+
11
+ from .asgi import IncortaAuthMiddleware, is_auth_configured
12
+ from .auth import IncortaAuth
13
+ from .config import (
14
+ AuthSession,
15
+ IncortaUser,
16
+ ResolvedConfig,
17
+ SessionData,
18
+ embedded_cookie_config,
19
+ resolve_config,
20
+ )
21
+ from .errors import IncortaAuthError
22
+ from .flow import (
23
+ ACCESS_TOKEN_REFRESH_LEEWAY_MS,
24
+ CallbackResult,
25
+ LoadedSession,
26
+ LoginResult,
27
+ begin_login,
28
+ clear_session_cookie,
29
+ complete_callback,
30
+ get_session,
31
+ load_session,
32
+ validate_return_to,
33
+ )
34
+ from .register import (
35
+ ClientInfo,
36
+ RegisteredClient,
37
+ delete_client,
38
+ get_client,
39
+ register_client,
40
+ update_client_redirect_uris,
41
+ )
42
+
43
+ # The distribution's version is stamped into package metadata at publish time
44
+ # (see release.python.config.cjs) — read it back rather than hardcoding.
45
+ try:
46
+ __version__ = _metadata_version("incorta-auth")
47
+ except PackageNotFoundError: # bare checkout without an installed dist
48
+ __version__ = "0.0.0"
49
+
50
+ __all__ = [
51
+ "ACCESS_TOKEN_REFRESH_LEEWAY_MS",
52
+ "AuthSession",
53
+ "CallbackResult",
54
+ "ClientInfo",
55
+ "IncortaAuth",
56
+ "IncortaAuthError",
57
+ "IncortaAuthMiddleware",
58
+ "IncortaUser",
59
+ "LoadedSession",
60
+ "LoginResult",
61
+ "RegisteredClient",
62
+ "ResolvedConfig",
63
+ "SessionData",
64
+ "begin_login",
65
+ "clear_session_cookie",
66
+ "complete_callback",
67
+ "delete_client",
68
+ "embedded_cookie_config",
69
+ "get_client",
70
+ "get_session",
71
+ "is_auth_configured",
72
+ "load_session",
73
+ "register_client",
74
+ "resolve_config",
75
+ "update_client_redirect_uris",
76
+ "validate_return_to",
77
+ ]
@@ -0,0 +1,11 @@
1
+ """Clock indirection so tests can shift time without sleeping.
2
+
3
+ Call sites use ``_time.now_ms()`` (module attribute lookup) rather than
4
+ importing the function, so a monkeypatch here is seen everywhere.
5
+ """
6
+
7
+ import time
8
+
9
+
10
+ def now_ms() -> int:
11
+ return int(time.time() * 1000)