leap-oauth-sdk 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.
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ .coverage
5
+ htmlcov/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ .venv/
10
+ .ruff_cache/
11
+ .pyright/
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (unreleased)
4
+
5
+ - `LeapAuthConfig` with explicit issuer, audience, JWKS URL and verification policy.
6
+ - `AccessTokenVerifier` with blocking and async verification sharing one JWKS cache.
7
+ - Error categories `Retryable`, `ReauthRequired`, `Forbidden`, `InvalidResponse`.
8
+ - `BrowserSSO`: code exchange, refresh, online session check and logout for one browser client,
9
+ blocking and async; upstream responses classified into the four error categories.
10
+ - `CookieCipher` and `CookieSessionManager`: encrypted HttpOnly cookie sessions with in-process
11
+ single-flight refresh, secret rotation and one-time migration of an old cookie format.
12
+ - `leap-sso.js`: browser module for automatic SSO, deep links, refresh, duplicated tabs and logout.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Llugaes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,241 @@
1
+ Metadata-Version: 2.5
2
+ Name: leap-oauth-sdk
3
+ Version: 0.1.0
4
+ Summary: Business-side SDK for services that accept Leap Auth OAuth access tokens
5
+ Project-URL: Homepage, https://github.com/Llugaes/leap-oauth-sdk
6
+ Project-URL: Repository, https://github.com/Llugaes/leap-oauth-sdk
7
+ Project-URL: Issues, https://github.com/Llugaes/leap-oauth-sdk/issues
8
+ Author-email: Llugaes <249094896@qq.com>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Llugaes
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: access-token,jwks,jwt,oauth,sso
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3 :: Only
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Classifier: Programming Language :: Python :: 3.14
41
+ Classifier: Topic :: Security
42
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
43
+ Classifier: Typing :: Typed
44
+ Requires-Python: >=3.12
45
+ Requires-Dist: httpx<1,>=0.27
46
+ Requires-Dist: pyjwt[crypto]<3,>=2.10
47
+ Provides-Extra: dev
48
+ Requires-Dist: pyright; extra == 'dev'
49
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
50
+ Requires-Dist: pytest-cov; extra == 'dev'
51
+ Requires-Dist: pytest>=8; extra == 'dev'
52
+ Requires-Dist: respx>=0.21; extra == 'dev'
53
+ Requires-Dist: ruff; extra == 'dev'
54
+ Description-Content-Type: text/markdown
55
+
56
+ # leap-oauth-sdk
57
+
58
+ Business-side SDK for services that accept access tokens issued by Leap Auth, an OAuth 2.1
59
+ authorization server. It verifies tokens locally against the server's JWKS and reports every
60
+ failure as one of four error categories, so each service handles retries, re-login and
61
+ permission errors the same way.
62
+
63
+ Status: alpha. This release contains access token verification, the server side of browser
64
+ sign-in (code exchange, refresh, online session check, logout), encrypted cookie sessions and
65
+ `leap-sso.js`, the matching browser module. Directory lookup and member management are planned.
66
+
67
+ ## Install
68
+
69
+ ```bash
70
+ uv add leap-oauth-sdk
71
+ ```
72
+
73
+ Python 3.12 or newer.
74
+
75
+ ## Verify an access token
76
+
77
+ Create one verifier per process and reuse it, so the JWKS cache is shared by all requests.
78
+
79
+ ```python
80
+ from leap_oauth_sdk import AccessTokenVerifier, LeapAuthConfig, OAuthSDKError
81
+
82
+ verifier = AccessTokenVerifier(
83
+ LeapAuthConfig(
84
+ issuer="https://auth.example.com", # must equal the token's iss claim exactly
85
+ audience="example-service", # this service's own audience
86
+ )
87
+ )
88
+
89
+ async def current_subject(bearer_token: str) -> str:
90
+ try:
91
+ context = await verifier.verify_async(bearer_token)
92
+ except OAuthSDKError as exc:
93
+ # exc.status is the suggested local HTTP status; exc.code is stable for logs.
94
+ raise HTTPException(status_code=exc.status, detail={"error": exc.code})
95
+ return context.subject_id
96
+ ```
97
+
98
+ `verify()` is the blocking variant. Both share the same cache.
99
+
100
+ The SDK does not read environment variables and has no default hosts. Load settings in your
101
+ own configuration layer and pass them to `LeapAuthConfig`.
102
+
103
+ ## Browser sign-in
104
+
105
+ One browser client is registered per service and environment. The browser runs authorization
106
+ code + PKCE with a top-level redirect; your backend exchanges the code. Tokens never appear in
107
+ URLs. Two ways to hold credentials:
108
+
109
+ - **bearer**: the page keeps the access and refresh token in `sessionStorage` and sends
110
+ `Authorization: Bearer`. Your API verifies with `check_session()`.
111
+ - **cookie**: the backend keeps both tokens in an encrypted HttpOnly cookie and the page never
112
+ sees them. Use `CookieSessionManager`.
113
+
114
+ ```python
115
+ from leap_oauth_sdk import BrowserClientConfig, BrowserSSO, LeapAuthConfig, OAuthSDKError
116
+
117
+ sso = BrowserSSO(
118
+ LeapAuthConfig(issuer="https://auth.example.com", audience="example-service"),
119
+ BrowserClientConfig(
120
+ client_id="web-example",
121
+ redirect_uri="https://app.example.com/auth/callback", # exact registered callback
122
+ resource="https://app.example.com/",
123
+ ),
124
+ )
125
+ ```
126
+
127
+ Serve these routes under one base path (bearer mode shown). On `OAuthSDKError` every route
128
+ responds with `exc.status`, `exc.headers` and the body `exc.to_dict()`:
129
+
130
+ | Route | Handler |
131
+ | --- | --- |
132
+ | `GET {base}/config` | `sso.browser_config()` |
133
+ | `POST {base}/exchange` `{code, code_verifier}` | `sso.exchange_code(...).to_browser_payload()` |
134
+ | `POST {base}/refresh` `{refresh_token}` | `sso.refresh(...).to_browser_payload()` |
135
+ | `POST {base}/logout` `{scope}` + Bearer | `sso.logout(token, scope)` |
136
+
137
+ Protected API routes call `sso.check_session(bearer_token)` on every request and must not
138
+ cache the result, so that signing out in one service takes effect everywhere on the next
139
+ request. An already-ended sign-in counts as a successful logout.
140
+
141
+ ### Browser module
142
+
143
+ `leap-sso.js` ships inside the package; `leap_oauth_sdk.static_dir()` returns its directory.
144
+
145
+ ```html
146
+ <script src="/static/leap-sso.js"></script>
147
+ <script>
148
+ const sso = LeapSSO.create({
149
+ endpointBase: "/api/auth/browser",
150
+ credentialMode: "bearer", // or "cookie"
151
+ storagePrefix: "example.admin", // sessionStorage keys; storageKeys can keep old names
152
+ defaultNext: "/admin",
153
+ hooks: {
154
+ onLoading: () => showSpinner(),
155
+ onLogin: (message, retryable) => showSignIn(message, retryable),
156
+ onForbidden: () => showNoAccess(),
157
+ },
158
+ });
159
+ const state = await sso.boot(); // "ready" | "redirecting" | "login" | "forbidden" | "disabled"
160
+ if (state === "ready") render(await (await sso.fetch("/api/me")).json());
161
+ signInButton.onclick = () => sso.login();
162
+ signOutButton.onclick = () => sso.logout().catch(showRetry);
163
+ </script>
164
+ ```
165
+
166
+ - `boot()` finishes a callback, restores the deep link, or starts one automatic sign-in that
167
+ only reuses an existing sign-in. Every page load makes at most one automatic attempt; a
168
+ failed callback shows sign-in instead of looping.
169
+ - `fetch()` adds the bearer token and refreshes 60 seconds before expiry, one refresh at a time.
170
+ A 401 drops local credentials and makes the one automatic attempt; 403 and other statuses
171
+ are returned to you untouched.
172
+ - A duplicated tab copies `sessionStorage` and with it a refresh token the other tab may
173
+ already have spent. The server then answers `refresh_rotation_in_progress`; the module drops
174
+ the copy and signs in again through SSO instead of replaying it. In cookie mode the same
175
+ answer comes from two replicas refreshing at once, so the request is retried briefly.
176
+ - `logout()` sends `scope=browser`, which ends the sign-in for every service in this browser.
177
+ Credentials are cleared only when the backend confirms; the next page load shows sign-in once.
178
+ - Messages default to Chinese; pass `messages` to override.
179
+
180
+ ### Cookie sessions
181
+
182
+ ```python
183
+ from leap_oauth_sdk import CookieCipher, CookieSessionManager
184
+
185
+ cookies = CookieSessionManager(sso, CookieCipher(secret)) # secret: 32+ characters
186
+
187
+ # callback: value = cookies.issue(sso.exchange_code(code, verifier), extra={"csrf": csrf})
188
+ # per request:
189
+ result = cookies.resolve(request.cookies.get("example_session"))
190
+ if result.cookie is not None: # refreshed or migrated: write it back
191
+ response.set_cookie(
192
+ "example_session", result.cookie, max_age=cookies.max_age(result.session),
193
+ httponly=True, secure=True, samesite="lax",
194
+ )
195
+ # logout: cookies.logout(value), then delete the cookie
196
+ ```
197
+
198
+ `resolve()` refreshes shortly before expiry and checks the sign-in online. Requests carrying
199
+ the same old cookie share one refresh within the process, and the result is reused for ten
200
+ seconds so a late request never replays the spent refresh token. `CookieCipher` accepts
201
+ `previous_secrets` for rotation and a `context` string; `legacy_loader` lets a service read its
202
+ old cookie format once and rewrite it.
203
+
204
+ ## Verification rules
205
+
206
+ - Signature algorithm must be in `allowed_algorithms` (default `EdDSA` only); the token must
207
+ carry a `kid` that exists in the JWKS.
208
+ - Registered claims `exp`, `iat`, `nbf`, `iss`, `sub`, `aud`, `jti` are required; `aud` must be a
209
+ single string equal to `audience`; `iss` must equal `issuer`.
210
+ - `ver` must equal `token_version`; `status` must be `active`; `source.channel` must be present;
211
+ `purpose` must be in `allowed_purposes` (a token without `purpose` counts as `access`).
212
+ - The JWKS is cached for `jwks_cache_seconds` (300). An unknown `kid` forces at most one refetch
213
+ per `jwks_refresh_cooldown_seconds` (10). A failed or malformed fetch is never cached and never
214
+ falls back to expired keys. Redirects are not followed.
215
+
216
+ Business authorization (roles, resources, fields, actions) stays in your service. A verified
217
+ token only proves who the caller is.
218
+
219
+ ## Error categories
220
+
221
+ | Exception | Status | Meaning | What to do |
222
+ | --- | --- | --- | --- |
223
+ | `Retryable` | 503 | Authorization server or JWKS unavailable, timeout, 5xx or 429, `temporarily_unavailable` (including `refresh_rotation_in_progress`) | Keep credentials, retry later |
224
+ | `ReauthRequired` | 401 | Token invalid, expired, revoked or unknown key; upstream 401 or `invalid_grant`; sign-in no longer active | Clear credentials, sign in again |
225
+ | `Forbidden` | 403 | Principal inactive, token purpose not accepted, upstream 403 | Show a permission error, do not re-login |
226
+ | `InvalidResponse` | 502 | Malformed upstream response, redirect, `invalid_client`, token not bound to a browser sign-in | Alert; do not loop sign-in |
227
+
228
+ All four inherit from `OAuthSDKError` and expose `code`, `message`, `status` and `retryable`;
229
+ `to_dict()` and `headers` (`Retry-After` when retryable) build your error response. Messages
230
+ never include tokens or upstream response bodies.
231
+
232
+ ## Development
233
+
234
+ ```bash
235
+ uv sync --extra dev
236
+ bash scripts/check.sh # also runs the browser module tests (Node 20+)
237
+ ```
238
+
239
+ ## License
240
+
241
+ MIT
@@ -0,0 +1,186 @@
1
+ # leap-oauth-sdk
2
+
3
+ Business-side SDK for services that accept access tokens issued by Leap Auth, an OAuth 2.1
4
+ authorization server. It verifies tokens locally against the server's JWKS and reports every
5
+ failure as one of four error categories, so each service handles retries, re-login and
6
+ permission errors the same way.
7
+
8
+ Status: alpha. This release contains access token verification, the server side of browser
9
+ sign-in (code exchange, refresh, online session check, logout), encrypted cookie sessions and
10
+ `leap-sso.js`, the matching browser module. Directory lookup and member management are planned.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ uv add leap-oauth-sdk
16
+ ```
17
+
18
+ Python 3.12 or newer.
19
+
20
+ ## Verify an access token
21
+
22
+ Create one verifier per process and reuse it, so the JWKS cache is shared by all requests.
23
+
24
+ ```python
25
+ from leap_oauth_sdk import AccessTokenVerifier, LeapAuthConfig, OAuthSDKError
26
+
27
+ verifier = AccessTokenVerifier(
28
+ LeapAuthConfig(
29
+ issuer="https://auth.example.com", # must equal the token's iss claim exactly
30
+ audience="example-service", # this service's own audience
31
+ )
32
+ )
33
+
34
+ async def current_subject(bearer_token: str) -> str:
35
+ try:
36
+ context = await verifier.verify_async(bearer_token)
37
+ except OAuthSDKError as exc:
38
+ # exc.status is the suggested local HTTP status; exc.code is stable for logs.
39
+ raise HTTPException(status_code=exc.status, detail={"error": exc.code})
40
+ return context.subject_id
41
+ ```
42
+
43
+ `verify()` is the blocking variant. Both share the same cache.
44
+
45
+ The SDK does not read environment variables and has no default hosts. Load settings in your
46
+ own configuration layer and pass them to `LeapAuthConfig`.
47
+
48
+ ## Browser sign-in
49
+
50
+ One browser client is registered per service and environment. The browser runs authorization
51
+ code + PKCE with a top-level redirect; your backend exchanges the code. Tokens never appear in
52
+ URLs. Two ways to hold credentials:
53
+
54
+ - **bearer**: the page keeps the access and refresh token in `sessionStorage` and sends
55
+ `Authorization: Bearer`. Your API verifies with `check_session()`.
56
+ - **cookie**: the backend keeps both tokens in an encrypted HttpOnly cookie and the page never
57
+ sees them. Use `CookieSessionManager`.
58
+
59
+ ```python
60
+ from leap_oauth_sdk import BrowserClientConfig, BrowserSSO, LeapAuthConfig, OAuthSDKError
61
+
62
+ sso = BrowserSSO(
63
+ LeapAuthConfig(issuer="https://auth.example.com", audience="example-service"),
64
+ BrowserClientConfig(
65
+ client_id="web-example",
66
+ redirect_uri="https://app.example.com/auth/callback", # exact registered callback
67
+ resource="https://app.example.com/",
68
+ ),
69
+ )
70
+ ```
71
+
72
+ Serve these routes under one base path (bearer mode shown). On `OAuthSDKError` every route
73
+ responds with `exc.status`, `exc.headers` and the body `exc.to_dict()`:
74
+
75
+ | Route | Handler |
76
+ | --- | --- |
77
+ | `GET {base}/config` | `sso.browser_config()` |
78
+ | `POST {base}/exchange` `{code, code_verifier}` | `sso.exchange_code(...).to_browser_payload()` |
79
+ | `POST {base}/refresh` `{refresh_token}` | `sso.refresh(...).to_browser_payload()` |
80
+ | `POST {base}/logout` `{scope}` + Bearer | `sso.logout(token, scope)` |
81
+
82
+ Protected API routes call `sso.check_session(bearer_token)` on every request and must not
83
+ cache the result, so that signing out in one service takes effect everywhere on the next
84
+ request. An already-ended sign-in counts as a successful logout.
85
+
86
+ ### Browser module
87
+
88
+ `leap-sso.js` ships inside the package; `leap_oauth_sdk.static_dir()` returns its directory.
89
+
90
+ ```html
91
+ <script src="/static/leap-sso.js"></script>
92
+ <script>
93
+ const sso = LeapSSO.create({
94
+ endpointBase: "/api/auth/browser",
95
+ credentialMode: "bearer", // or "cookie"
96
+ storagePrefix: "example.admin", // sessionStorage keys; storageKeys can keep old names
97
+ defaultNext: "/admin",
98
+ hooks: {
99
+ onLoading: () => showSpinner(),
100
+ onLogin: (message, retryable) => showSignIn(message, retryable),
101
+ onForbidden: () => showNoAccess(),
102
+ },
103
+ });
104
+ const state = await sso.boot(); // "ready" | "redirecting" | "login" | "forbidden" | "disabled"
105
+ if (state === "ready") render(await (await sso.fetch("/api/me")).json());
106
+ signInButton.onclick = () => sso.login();
107
+ signOutButton.onclick = () => sso.logout().catch(showRetry);
108
+ </script>
109
+ ```
110
+
111
+ - `boot()` finishes a callback, restores the deep link, or starts one automatic sign-in that
112
+ only reuses an existing sign-in. Every page load makes at most one automatic attempt; a
113
+ failed callback shows sign-in instead of looping.
114
+ - `fetch()` adds the bearer token and refreshes 60 seconds before expiry, one refresh at a time.
115
+ A 401 drops local credentials and makes the one automatic attempt; 403 and other statuses
116
+ are returned to you untouched.
117
+ - A duplicated tab copies `sessionStorage` and with it a refresh token the other tab may
118
+ already have spent. The server then answers `refresh_rotation_in_progress`; the module drops
119
+ the copy and signs in again through SSO instead of replaying it. In cookie mode the same
120
+ answer comes from two replicas refreshing at once, so the request is retried briefly.
121
+ - `logout()` sends `scope=browser`, which ends the sign-in for every service in this browser.
122
+ Credentials are cleared only when the backend confirms; the next page load shows sign-in once.
123
+ - Messages default to Chinese; pass `messages` to override.
124
+
125
+ ### Cookie sessions
126
+
127
+ ```python
128
+ from leap_oauth_sdk import CookieCipher, CookieSessionManager
129
+
130
+ cookies = CookieSessionManager(sso, CookieCipher(secret)) # secret: 32+ characters
131
+
132
+ # callback: value = cookies.issue(sso.exchange_code(code, verifier), extra={"csrf": csrf})
133
+ # per request:
134
+ result = cookies.resolve(request.cookies.get("example_session"))
135
+ if result.cookie is not None: # refreshed or migrated: write it back
136
+ response.set_cookie(
137
+ "example_session", result.cookie, max_age=cookies.max_age(result.session),
138
+ httponly=True, secure=True, samesite="lax",
139
+ )
140
+ # logout: cookies.logout(value), then delete the cookie
141
+ ```
142
+
143
+ `resolve()` refreshes shortly before expiry and checks the sign-in online. Requests carrying
144
+ the same old cookie share one refresh within the process, and the result is reused for ten
145
+ seconds so a late request never replays the spent refresh token. `CookieCipher` accepts
146
+ `previous_secrets` for rotation and a `context` string; `legacy_loader` lets a service read its
147
+ old cookie format once and rewrite it.
148
+
149
+ ## Verification rules
150
+
151
+ - Signature algorithm must be in `allowed_algorithms` (default `EdDSA` only); the token must
152
+ carry a `kid` that exists in the JWKS.
153
+ - Registered claims `exp`, `iat`, `nbf`, `iss`, `sub`, `aud`, `jti` are required; `aud` must be a
154
+ single string equal to `audience`; `iss` must equal `issuer`.
155
+ - `ver` must equal `token_version`; `status` must be `active`; `source.channel` must be present;
156
+ `purpose` must be in `allowed_purposes` (a token without `purpose` counts as `access`).
157
+ - The JWKS is cached for `jwks_cache_seconds` (300). An unknown `kid` forces at most one refetch
158
+ per `jwks_refresh_cooldown_seconds` (10). A failed or malformed fetch is never cached and never
159
+ falls back to expired keys. Redirects are not followed.
160
+
161
+ Business authorization (roles, resources, fields, actions) stays in your service. A verified
162
+ token only proves who the caller is.
163
+
164
+ ## Error categories
165
+
166
+ | Exception | Status | Meaning | What to do |
167
+ | --- | --- | --- | --- |
168
+ | `Retryable` | 503 | Authorization server or JWKS unavailable, timeout, 5xx or 429, `temporarily_unavailable` (including `refresh_rotation_in_progress`) | Keep credentials, retry later |
169
+ | `ReauthRequired` | 401 | Token invalid, expired, revoked or unknown key; upstream 401 or `invalid_grant`; sign-in no longer active | Clear credentials, sign in again |
170
+ | `Forbidden` | 403 | Principal inactive, token purpose not accepted, upstream 403 | Show a permission error, do not re-login |
171
+ | `InvalidResponse` | 502 | Malformed upstream response, redirect, `invalid_client`, token not bound to a browser sign-in | Alert; do not loop sign-in |
172
+
173
+ All four inherit from `OAuthSDKError` and expose `code`, `message`, `status` and `retryable`;
174
+ `to_dict()` and `headers` (`Retry-After` when retryable) build your error response. Messages
175
+ never include tokens or upstream response bodies.
176
+
177
+ ## Development
178
+
179
+ ```bash
180
+ uv sync --extra dev
181
+ bash scripts/check.sh # also runs the browser module tests (Node 20+)
182
+ ```
183
+
184
+ ## License
185
+
186
+ MIT
@@ -0,0 +1,68 @@
1
+ [project]
2
+ name = "leap-oauth-sdk"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.12"
5
+ description = "Business-side SDK for services that accept Leap Auth OAuth access tokens"
6
+ authors = [{name = "Llugaes", email = "249094896@qq.com"}]
7
+ license = {file = "LICENSE"}
8
+ readme = "README.md"
9
+ keywords = ["oauth", "jwt", "jwks", "access-token", "sso"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Operating System :: OS Independent",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3 :: Only",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3.14",
20
+ "Topic :: Security",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ "Typing :: Typed",
23
+ ]
24
+ dependencies = [
25
+ "httpx>=0.27,<1",
26
+ "pyjwt[crypto]>=2.10,<3",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/Llugaes/leap-oauth-sdk"
31
+ Repository = "https://github.com/Llugaes/leap-oauth-sdk"
32
+ Issues = "https://github.com/Llugaes/leap-oauth-sdk/issues"
33
+
34
+ [project.optional-dependencies]
35
+ dev = [
36
+ "pytest>=8",
37
+ "pytest-asyncio>=0.23",
38
+ "pytest-cov",
39
+ "respx>=0.21",
40
+ "pyright",
41
+ "ruff",
42
+ ]
43
+
44
+ [build-system]
45
+ requires = ["hatchling"]
46
+ build-backend = "hatchling.build"
47
+
48
+ [tool.hatch.build.targets.wheel]
49
+ packages = ["src/leap_oauth_sdk"]
50
+
51
+ [tool.hatch.build.targets.sdist]
52
+ only-include = ["src/leap_oauth_sdk", "README.md", "LICENSE", "CHANGELOG.md", "pyproject.toml"]
53
+
54
+ [tool.pytest.ini_options]
55
+ asyncio_mode = "auto"
56
+ testpaths = ["tests"]
57
+
58
+ [tool.pyright]
59
+ include = ["src"]
60
+ strict = ["src"]
61
+ pythonVersion = "3.12"
62
+
63
+ [tool.ruff]
64
+ target-version = "py312"
65
+ line-length = 100
66
+
67
+ [tool.ruff.lint]
68
+ select = ["E", "F", "W", "I", "B", "UP", "ASYNC"]
@@ -0,0 +1,54 @@
1
+ """Business-side SDK for services that accept Leap Auth OAuth access tokens."""
2
+
3
+ from pathlib import Path
4
+
5
+ from .browser import (
6
+ ROTATION_IN_PROGRESS,
7
+ AuthorizationRequest,
8
+ BrowserSession,
9
+ BrowserSSO,
10
+ LogoutScope,
11
+ classify_response,
12
+ )
13
+ from .config import DEFAULT_TOKEN_VERSION, BrowserClientConfig, LeapAuthConfig
14
+ from .cookie_session import (
15
+ CookieCipher,
16
+ CookieResolution,
17
+ CookieSession,
18
+ CookieSessionManager,
19
+ )
20
+ from .errors import Forbidden, InvalidResponse, OAuthSDKError, ReauthRequired, Retryable
21
+ from .tokens import AccessTokenContext, AccessTokenVerifier
22
+
23
+ __version__ = "0.1.0"
24
+
25
+
26
+ def static_dir() -> Path:
27
+ """Directory holding ``leap-sso.js``; mount it as static files in your web framework."""
28
+ return Path(__file__).with_name("static")
29
+
30
+
31
+ __all__ = [
32
+ "DEFAULT_TOKEN_VERSION",
33
+ "ROTATION_IN_PROGRESS",
34
+ "AccessTokenContext",
35
+ "AccessTokenVerifier",
36
+ "AuthorizationRequest",
37
+ "BrowserClientConfig",
38
+ "BrowserSSO",
39
+ "BrowserSession",
40
+ "CookieCipher",
41
+ "CookieResolution",
42
+ "CookieSession",
43
+ "CookieSessionManager",
44
+ "Forbidden",
45
+ "InvalidResponse",
46
+ "LeapAuthConfig",
47
+ "LogoutScope",
48
+ "OAuthSDKError",
49
+ "ReauthRequired",
50
+ "Retryable",
51
+ "__version__",
52
+ "classify_response",
53
+ "static_dir",
54
+ ]