gait-sdk 0.5.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 (48) hide show
  1. gait_sdk-0.5.0/LICENSE +21 -0
  2. gait_sdk-0.5.0/PKG-INFO +187 -0
  3. gait_sdk-0.5.0/README.md +144 -0
  4. gait_sdk-0.5.0/auth_integration/__init__.py +64 -0
  5. gait_sdk-0.5.0/gait_sdk/__init__.py +31 -0
  6. gait_sdk-0.5.0/gait_sdk/application.py +220 -0
  7. gait_sdk-0.5.0/gait_sdk/apps.py +19 -0
  8. gait_sdk-0.5.0/gait_sdk/authentication.py +21 -0
  9. gait_sdk-0.5.0/gait_sdk/client.py +145 -0
  10. gait_sdk-0.5.0/gait_sdk/context.py +160 -0
  11. gait_sdk-0.5.0/gait_sdk/django/__init__.py +0 -0
  12. gait_sdk-0.5.0/gait_sdk/django/authentication.py +480 -0
  13. gait_sdk-0.5.0/gait_sdk/exceptions.py +100 -0
  14. gait_sdk-0.5.0/gait_sdk/fastapi/__init__.py +0 -0
  15. gait_sdk-0.5.0/gait_sdk/fastapi/dependencies.py +228 -0
  16. gait_sdk-0.5.0/gait_sdk/permissions.py +214 -0
  17. gait_sdk-0.5.0/gait_sdk/security.py +310 -0
  18. gait_sdk-0.5.0/gait_sdk/session.py +106 -0
  19. gait_sdk-0.5.0/gait_sdk/settings.py +126 -0
  20. gait_sdk-0.5.0/gait_sdk/utils.py +97 -0
  21. gait_sdk-0.5.0/gait_sdk/verification.py +568 -0
  22. gait_sdk-0.5.0/gait_sdk.egg-info/PKG-INFO +187 -0
  23. gait_sdk-0.5.0/gait_sdk.egg-info/SOURCES.txt +46 -0
  24. gait_sdk-0.5.0/gait_sdk.egg-info/dependency_links.txt +1 -0
  25. gait_sdk-0.5.0/gait_sdk.egg-info/requires.txt +17 -0
  26. gait_sdk-0.5.0/gait_sdk.egg-info/top_level.txt +2 -0
  27. gait_sdk-0.5.0/pyproject.toml +114 -0
  28. gait_sdk-0.5.0/setup.cfg +4 -0
  29. gait_sdk-0.5.0/tests/test_application.py +432 -0
  30. gait_sdk-0.5.0/tests/test_client.py +127 -0
  31. gait_sdk-0.5.0/tests/test_context.py +247 -0
  32. gait_sdk-0.5.0/tests/test_dependencies.py +194 -0
  33. gait_sdk-0.5.0/tests/test_django_authentication.py +400 -0
  34. gait_sdk-0.5.0/tests/test_exceptions.py +61 -0
  35. gait_sdk-0.5.0/tests/test_integration_auth.py +0 -0
  36. gait_sdk-0.5.0/tests/test_jwks_adapters.py +325 -0
  37. gait_sdk-0.5.0/tests/test_jwks_verifier.py +366 -0
  38. gait_sdk-0.5.0/tests/test_permissions.py +83 -0
  39. gait_sdk-0.5.0/tests/test_permissions_fastapi_fallback.py +170 -0
  40. gait_sdk-0.5.0/tests/test_rename_compat_shim.py +101 -0
  41. gait_sdk-0.5.0/tests/test_role_dependency_status_codes.py +109 -0
  42. gait_sdk-0.5.0/tests/test_security_hardening_050.py +271 -0
  43. gait_sdk-0.5.0/tests/test_security_signal.py +539 -0
  44. gait_sdk-0.5.0/tests/test_session_check.py +115 -0
  45. gait_sdk-0.5.0/tests/test_settings.py +106 -0
  46. gait_sdk-0.5.0/tests/test_settings_decouple_isolation.py +34 -0
  47. gait_sdk-0.5.0/tests/test_settings_django_unconfigured.py +20 -0
  48. gait_sdk-0.5.0/tests/test_utils.py +92 -0
gait_sdk-0.5.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Anthony Narine
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,187 @@
1
+ Metadata-Version: 2.4
2
+ Name: gait-sdk
3
+ Version: 0.5.0
4
+ Summary: Official Python SDK for Gait: verify Gait identities (local JWKS or introspection) in Django and FastAPI, with live session checks, application identity, and security signals.
5
+ Author: Anthony Narine
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/anthonynarine/gait-sdk
8
+ Project-URL: Documentation, https://github.com/anthonynarine/gait-sdk#readme
9
+ Project-URL: Changelog, https://github.com/anthonynarine/gait-sdk/blob/main/docs/CHANGELOG.md
10
+ Project-URL: Security, https://github.com/anthonynarine/gait-sdk/blob/main/docs/SECURITY.md
11
+ Keywords: gait,authentication,jwt,jwks,django,fastapi,sdk,identity
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Framework :: Django
21
+ Classifier: Framework :: FastAPI
22
+ Classifier: Topic :: Security
23
+ Classifier: Topic :: Internet :: WWW/HTTP :: Session
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: httpx>=0.25
29
+ Requires-Dist: python-decouple>=3.6
30
+ Requires-Dist: requests>=2.31.0
31
+ Requires-Dist: PyJWT[crypto]>=2.10.1
32
+ Provides-Extra: django
33
+ Requires-Dist: Django>=4.2; extra == "django"
34
+ Requires-Dist: djangorestframework>=3.14; extra == "django"
35
+ Requires-Dist: asgiref>=3.6; extra == "django"
36
+ Provides-Extra: fastapi
37
+ Requires-Dist: fastapi>=0.100; extra == "fastapi"
38
+ Requires-Dist: starlette>=0.27; extra == "fastapi"
39
+ Provides-Extra: test
40
+ Requires-Dist: pytest>=7; extra == "test"
41
+ Requires-Dist: pytest-asyncio>=0.21; extra == "test"
42
+ Dynamic: license-file
43
+
44
+ # gait-sdk
45
+
46
+ [![PyPI](https://img.shields.io/pypi/v/gait-sdk.svg)](https://pypi.org/project/gait-sdk/)
47
+ [![Python](https://img.shields.io/pypi/pyversions/gait-sdk.svg)](https://pypi.org/project/gait-sdk/)
48
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
49
+
50
+ The official Python SDK for **Gait**, an identity and security platform. Drop it into a **Django REST Framework** or **FastAPI** service to verify who is calling you, using identities Gait issues. It never issues tokens, stores passwords, or makes authorization decisions.
51
+
52
+ ```
53
+ Gait authenticates — who are you? (login, 2FA, sessions, signed tokens)
54
+ gait-sdk verifies — is this token genuine, and whose is it?
55
+ your application authorizes — what may this person do here?
56
+ ```
57
+
58
+ That boundary is the core design rule. The SDK hands your code a verified **identity** (`subject`, `email`, session, token id, issuer). Roles, organizations and permissions belong to your application. See [Architecture](docs/ARCHITECTURE.md).
59
+
60
+ ---
61
+
62
+ ## Install
63
+
64
+ ```bash
65
+ pip install "gait-sdk[django]" # Django REST Framework services
66
+ pip install "gait-sdk[fastapi]" # FastAPI services
67
+ pip install gait-sdk # core only (verification, sessions, app identity)
68
+ ```
69
+
70
+ Requires Python 3.10+. Pin exact versions in production (`gait-sdk==0.5.0`). See [Supply chain](docs/PUBLISHING.md#consuming-safely).
71
+
72
+ ---
73
+
74
+ ## Quick start: Django REST Framework
75
+
76
+ ```python
77
+ # settings.py
78
+ INSTALLED_APPS = [..., "gait_sdk"] # validates configuration at startup
79
+
80
+ REST_FRAMEWORK = {
81
+ "DEFAULT_AUTHENTICATION_CLASSES": ["gait_sdk.authentication.ExternalJWTAuthentication"],
82
+ }
83
+
84
+ GAIT_TOKEN_VERIFIER = "jwks" # verify locally (recommended)
85
+ GAIT_JWKS_URL = "https://auth.example.com/.well-known/jwks.json"
86
+ GAIT_ISSUER = "https://auth.example.com"
87
+ GAIT_AUDIENCE = "urn:gait:your-app"
88
+ GAIT_AUTH_URL = "https://auth.example.com/api" # used for live session checks
89
+ ```
90
+
91
+ ```python
92
+ # views.py
93
+ from gait_sdk.django.authentication import require_live_session
94
+
95
+ class FinalizeReport(APIView):
96
+ def post(self, request, pk):
97
+ identity = request.verified_identity # subject, email, session_id, token_id, issuer
98
+ ... # YOUR authorization check first
99
+ require_live_session(request) # sensitive action: confirm the session live
100
+ ... # then mutate
101
+ ```
102
+
103
+ ## Quick start: FastAPI
104
+
105
+ ```python
106
+ from fastapi import Depends, FastAPI
107
+ from gait_sdk.fastapi.dependencies import require_live_session, validate_configuration, verify_token
108
+
109
+ app = FastAPI()
110
+ validate_configuration() # fail at startup, not on the first request
111
+
112
+ @app.get("/me")
113
+ async def me(claims: dict = Depends(verify_token)):
114
+ return {"subject": claims["id"], "email": claims["email"]} # "id" works in both verifier modes
115
+
116
+ @app.post("/danger")
117
+ async def danger(claims: dict = Depends(require_live_session)):
118
+ ...
119
+ ```
120
+
121
+ ---
122
+
123
+ ## Configuration
124
+
125
+ | Setting | Default | Purpose |
126
+ |---|---|---|
127
+ | `GAIT_TOKEN_VERIFIER` | `introspection` | `jwks` = verify tokens locally against Gait's published keys (recommended). `introspection` = ask Gait `/whoami/` on every request (legacy). Chosen explicitly, with **no automatic fallback**. |
128
+ | `GAIT_JWKS_URL` | — | Required for `jwks`. Must be `https` (plain `http` only for `localhost`). |
129
+ | `GAIT_ISSUER` | — | Required for `jwks`. Must equal Gait's `JWT_ISSUER` exactly. |
130
+ | `GAIT_AUDIENCE` | — | Required for `jwks`. Must equal Gait's `JWT_AUDIENCE`. |
131
+ | `GAIT_AUTH_URL` | — | Gait's API base (`…/api`), used by live session checks, introspection, application identity and signals. `https` required (http only for `localhost`). |
132
+ | `GAIT_TIMEOUT` | `5` | Seconds for calls to Gait. |
133
+ | `GAIT_APPLICATION_CREDENTIAL` | — | Only for application identity / security signals. A secret: keep it in the environment. |
134
+ | `GAIT_ALLOW_COOKIE_AUTH` | `False` | Deprecated legacy cookie mode (introspection only). Leave off. See [Security](docs/SECURITY.md). |
135
+
136
+ Settings come from Django settings first, then environment variables / `.env`. An invalid or incomplete configuration **stops the service at startup**.
137
+
138
+ ---
139
+
140
+ ## What you get
141
+
142
+ | Module | For |
143
+ |---|---|
144
+ | `gait_sdk.verification` | Token verification (`JwksVerifier`, `IntrospectionVerifier`) → `VerifiedIdentity` |
145
+ | `gait_sdk.session` | `check_session_live()`: live revocation check for sensitive actions |
146
+ | `gait_sdk.authentication` / `gait_sdk.django` | DRF authentication class, `require_live_session(request)` |
147
+ | `gait_sdk.fastapi.dependencies` | `verify_token`, `require_live_session`, `validate_configuration` |
148
+ | `gait_sdk.application` | Verify your *service's* own Gait credential (machine identity) |
149
+ | `gait_sdk.context` | `SecurityContext`: human identity + application identity together |
150
+ | `gait_sdk.security` | Send tenant security signals to Gait |
151
+
152
+ ## Security, in one screen
153
+
154
+ - **RS256 only.** `alg=none`, HS256 key-confusion and unknown algorithms are rejected. `iss`, `aud`, `exp`, `iat`, `sub`, `sid`, `jti` and `token_use="access"` are all required.
155
+ - **The SDK holds no secrets for verification.** It only ever has Gait's *public* keys, so it cannot mint tokens even if compromised.
156
+ - **Fails closed:** an invalid token → **401**; Gait unreachable → **503**. It never falls back to a weaker check.
157
+ - **Real 401s** (not DRF's silent 403), so clients' refresh-on-401 logic works.
158
+ - **Revocation:** local verification sees a revoked session only when its token expires (≤15 min). Protect sensitive actions with `require_live_session`.
159
+ - **No token, cookie or credential value is ever logged.**
160
+
161
+ Full threat model, guarantees, limits and audit history: [docs/SECURITY.md](docs/SECURITY.md). To report a vulnerability, see the same file.
162
+
163
+ ---
164
+
165
+ ## Upgrading from `auth_integration`
166
+
167
+ The package was renamed in **0.5.0**. The old import name still works as a deprecated alias until 0.6.0, returning the *same* modules, so nothing breaks while you migrate:
168
+
169
+ 1. `pip install gait-sdk` (replacing the old git URL pin).
170
+ 2. Replace `auth_integration` with `gait_sdk` in imports, `INSTALLED_APPS`, and DRF settings strings.
171
+
172
+ Details: [Integration guide](docs/INTEGRATION_GUIDE.md#upgrading-from-auth_integration).
173
+
174
+ ## Documentation
175
+
176
+ | | |
177
+ |---|---|
178
+ | [Architecture](docs/ARCHITECTURE.md) | The boundary, components, verification & caching, trust model |
179
+ | [Integration guide](docs/INTEGRATION_GUIDE.md) | Wiring into Django/FastAPI, JWKS cut-over runbook, sensitive actions, testing |
180
+ | [Security](docs/SECURITY.md) | Threat model, guarantees, known limits, hardening checklist, audit log, reporting |
181
+ | [Publishing](docs/PUBLISHING.md) | How releases reach PyPI (a step-by-step tutorial), and consuming safely |
182
+ | [Changelog](docs/CHANGELOG.md) | Version history |
183
+ | Module references | [`gait_sdk/docs/`](gait_sdk/docs/) |
184
+
185
+ ## License
186
+
187
+ MIT, © Anthony Narine.
@@ -0,0 +1,144 @@
1
+ # gait-sdk
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/gait-sdk.svg)](https://pypi.org/project/gait-sdk/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/gait-sdk.svg)](https://pypi.org/project/gait-sdk/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+
7
+ The official Python SDK for **Gait**, an identity and security platform. Drop it into a **Django REST Framework** or **FastAPI** service to verify who is calling you, using identities Gait issues. It never issues tokens, stores passwords, or makes authorization decisions.
8
+
9
+ ```
10
+ Gait authenticates — who are you? (login, 2FA, sessions, signed tokens)
11
+ gait-sdk verifies — is this token genuine, and whose is it?
12
+ your application authorizes — what may this person do here?
13
+ ```
14
+
15
+ That boundary is the core design rule. The SDK hands your code a verified **identity** (`subject`, `email`, session, token id, issuer). Roles, organizations and permissions belong to your application. See [Architecture](docs/ARCHITECTURE.md).
16
+
17
+ ---
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install "gait-sdk[django]" # Django REST Framework services
23
+ pip install "gait-sdk[fastapi]" # FastAPI services
24
+ pip install gait-sdk # core only (verification, sessions, app identity)
25
+ ```
26
+
27
+ Requires Python 3.10+. Pin exact versions in production (`gait-sdk==0.5.0`). See [Supply chain](docs/PUBLISHING.md#consuming-safely).
28
+
29
+ ---
30
+
31
+ ## Quick start: Django REST Framework
32
+
33
+ ```python
34
+ # settings.py
35
+ INSTALLED_APPS = [..., "gait_sdk"] # validates configuration at startup
36
+
37
+ REST_FRAMEWORK = {
38
+ "DEFAULT_AUTHENTICATION_CLASSES": ["gait_sdk.authentication.ExternalJWTAuthentication"],
39
+ }
40
+
41
+ GAIT_TOKEN_VERIFIER = "jwks" # verify locally (recommended)
42
+ GAIT_JWKS_URL = "https://auth.example.com/.well-known/jwks.json"
43
+ GAIT_ISSUER = "https://auth.example.com"
44
+ GAIT_AUDIENCE = "urn:gait:your-app"
45
+ GAIT_AUTH_URL = "https://auth.example.com/api" # used for live session checks
46
+ ```
47
+
48
+ ```python
49
+ # views.py
50
+ from gait_sdk.django.authentication import require_live_session
51
+
52
+ class FinalizeReport(APIView):
53
+ def post(self, request, pk):
54
+ identity = request.verified_identity # subject, email, session_id, token_id, issuer
55
+ ... # YOUR authorization check first
56
+ require_live_session(request) # sensitive action: confirm the session live
57
+ ... # then mutate
58
+ ```
59
+
60
+ ## Quick start: FastAPI
61
+
62
+ ```python
63
+ from fastapi import Depends, FastAPI
64
+ from gait_sdk.fastapi.dependencies import require_live_session, validate_configuration, verify_token
65
+
66
+ app = FastAPI()
67
+ validate_configuration() # fail at startup, not on the first request
68
+
69
+ @app.get("/me")
70
+ async def me(claims: dict = Depends(verify_token)):
71
+ return {"subject": claims["id"], "email": claims["email"]} # "id" works in both verifier modes
72
+
73
+ @app.post("/danger")
74
+ async def danger(claims: dict = Depends(require_live_session)):
75
+ ...
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Configuration
81
+
82
+ | Setting | Default | Purpose |
83
+ |---|---|---|
84
+ | `GAIT_TOKEN_VERIFIER` | `introspection` | `jwks` = verify tokens locally against Gait's published keys (recommended). `introspection` = ask Gait `/whoami/` on every request (legacy). Chosen explicitly, with **no automatic fallback**. |
85
+ | `GAIT_JWKS_URL` | — | Required for `jwks`. Must be `https` (plain `http` only for `localhost`). |
86
+ | `GAIT_ISSUER` | — | Required for `jwks`. Must equal Gait's `JWT_ISSUER` exactly. |
87
+ | `GAIT_AUDIENCE` | — | Required for `jwks`. Must equal Gait's `JWT_AUDIENCE`. |
88
+ | `GAIT_AUTH_URL` | — | Gait's API base (`…/api`), used by live session checks, introspection, application identity and signals. `https` required (http only for `localhost`). |
89
+ | `GAIT_TIMEOUT` | `5` | Seconds for calls to Gait. |
90
+ | `GAIT_APPLICATION_CREDENTIAL` | — | Only for application identity / security signals. A secret: keep it in the environment. |
91
+ | `GAIT_ALLOW_COOKIE_AUTH` | `False` | Deprecated legacy cookie mode (introspection only). Leave off. See [Security](docs/SECURITY.md). |
92
+
93
+ Settings come from Django settings first, then environment variables / `.env`. An invalid or incomplete configuration **stops the service at startup**.
94
+
95
+ ---
96
+
97
+ ## What you get
98
+
99
+ | Module | For |
100
+ |---|---|
101
+ | `gait_sdk.verification` | Token verification (`JwksVerifier`, `IntrospectionVerifier`) → `VerifiedIdentity` |
102
+ | `gait_sdk.session` | `check_session_live()`: live revocation check for sensitive actions |
103
+ | `gait_sdk.authentication` / `gait_sdk.django` | DRF authentication class, `require_live_session(request)` |
104
+ | `gait_sdk.fastapi.dependencies` | `verify_token`, `require_live_session`, `validate_configuration` |
105
+ | `gait_sdk.application` | Verify your *service's* own Gait credential (machine identity) |
106
+ | `gait_sdk.context` | `SecurityContext`: human identity + application identity together |
107
+ | `gait_sdk.security` | Send tenant security signals to Gait |
108
+
109
+ ## Security, in one screen
110
+
111
+ - **RS256 only.** `alg=none`, HS256 key-confusion and unknown algorithms are rejected. `iss`, `aud`, `exp`, `iat`, `sub`, `sid`, `jti` and `token_use="access"` are all required.
112
+ - **The SDK holds no secrets for verification.** It only ever has Gait's *public* keys, so it cannot mint tokens even if compromised.
113
+ - **Fails closed:** an invalid token → **401**; Gait unreachable → **503**. It never falls back to a weaker check.
114
+ - **Real 401s** (not DRF's silent 403), so clients' refresh-on-401 logic works.
115
+ - **Revocation:** local verification sees a revoked session only when its token expires (≤15 min). Protect sensitive actions with `require_live_session`.
116
+ - **No token, cookie or credential value is ever logged.**
117
+
118
+ Full threat model, guarantees, limits and audit history: [docs/SECURITY.md](docs/SECURITY.md). To report a vulnerability, see the same file.
119
+
120
+ ---
121
+
122
+ ## Upgrading from `auth_integration`
123
+
124
+ The package was renamed in **0.5.0**. The old import name still works as a deprecated alias until 0.6.0, returning the *same* modules, so nothing breaks while you migrate:
125
+
126
+ 1. `pip install gait-sdk` (replacing the old git URL pin).
127
+ 2. Replace `auth_integration` with `gait_sdk` in imports, `INSTALLED_APPS`, and DRF settings strings.
128
+
129
+ Details: [Integration guide](docs/INTEGRATION_GUIDE.md#upgrading-from-auth_integration).
130
+
131
+ ## Documentation
132
+
133
+ | | |
134
+ |---|---|
135
+ | [Architecture](docs/ARCHITECTURE.md) | The boundary, components, verification & caching, trust model |
136
+ | [Integration guide](docs/INTEGRATION_GUIDE.md) | Wiring into Django/FastAPI, JWKS cut-over runbook, sensitive actions, testing |
137
+ | [Security](docs/SECURITY.md) | Threat model, guarantees, known limits, hardening checklist, audit log, reporting |
138
+ | [Publishing](docs/PUBLISHING.md) | How releases reach PyPI (a step-by-step tutorial), and consuming safely |
139
+ | [Changelog](docs/CHANGELOG.md) | Version history |
140
+ | Module references | [`gait_sdk/docs/`](gait_sdk/docs/) |
141
+
142
+ ## License
143
+
144
+ MIT, © Anthony Narine.
@@ -0,0 +1,64 @@
1
+ """
2
+ auth_integration -- DEPRECATED compatibility alias for `gait_sdk`
3
+ =================================================================
4
+
5
+ The package was renamed to `gait_sdk` in 0.5.0. This shim keeps every old
6
+ import path working for one release so consumers can migrate in their own
7
+ commit:
8
+
9
+ import auth_integration.verification -> gait_sdk.verification
10
+ "auth_integration.authentication.ExternalJWTAuthentication" (DRF setting)
11
+ INSTALLED_APPS = [..., "auth_integration"]
12
+
13
+ Old names resolve to the SAME module objects as the new ones (not copies),
14
+ so process-wide state -- the configured token verifier and its JWKS cache,
15
+ the Django bearer cache -- is shared no matter which name a caller used.
16
+
17
+ Scheduled for removal in 0.6.0. Migrate: replace `auth_integration` with
18
+ `gait_sdk` in imports, settings strings, and INSTALLED_APPS.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import importlib
24
+ import importlib.abc
25
+ import importlib.util
26
+ import sys
27
+ import warnings
28
+
29
+ _OLD = "auth_integration"
30
+ _NEW = "gait_sdk"
31
+
32
+ warnings.warn(
33
+ "The 'auth_integration' package was renamed to 'gait_sdk' (0.5.0). "
34
+ "The old name is a deprecated alias and will be removed in 0.6.0.",
35
+ DeprecationWarning,
36
+ stacklevel=2,
37
+ )
38
+
39
+
40
+ class _GaitSdkAliasFinder(importlib.abc.MetaPathFinder, importlib.abc.Loader):
41
+ """Resolve `auth_integration.<x>` to the already-importable `gait_sdk.<x>` module object."""
42
+
43
+ def find_spec(self, fullname, path=None, target=None):
44
+ if fullname.startswith(_OLD + "."):
45
+ if importlib.util.find_spec(_NEW + fullname[len(_OLD):]) is None:
46
+ return None
47
+ return importlib.util.spec_from_loader(fullname, self)
48
+ return None
49
+
50
+ def create_module(self, spec):
51
+ # Return the real gait_sdk module; the import system registers it in
52
+ # sys.modules under the old name too, so both names share one object.
53
+ return importlib.import_module(_NEW + spec.name[len(_OLD):])
54
+
55
+ def exec_module(self, module):
56
+ # Already executed when imported under its real name.
57
+ pass
58
+
59
+
60
+ if not any(isinstance(f, _GaitSdkAliasFinder) for f in sys.meta_path):
61
+ sys.meta_path.insert(0, _GaitSdkAliasFinder())
62
+
63
+ from gait_sdk import * # noqa: E402,F401,F403
64
+ from gait_sdk import __version__ # noqa: E402,F401
@@ -0,0 +1,31 @@
1
+ # Filename: gait_sdk/__init__.py
2
+ """
3
+ gait_sdk
4
+ ----------------
5
+ Cross-framework authentication integration for Django + FastAPI.
6
+
7
+ Design rules:
8
+ -------------
9
+ - Keep package import side-effect free.
10
+ - Do NOT import framework-specific modules (Django/DRF/FastAPI) here.
11
+ - Downstream services should import adapters from stable entrypoints:
12
+ - DRF: gait_sdk.authentication.ExternalJWTAuthentication
13
+ - FastAPI: gait_sdk.fastapi.dependencies.verify_token
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from importlib.metadata import PackageNotFoundError, version
19
+
20
+
21
+ # ---------------------------------------------------------------------
22
+ # 📌 Package Version
23
+ # ---------------------------------------------------------------------
24
+ # Step 1: Resolve installed version from package metadata (no heavy imports).
25
+ try:
26
+ __version__ = version("gait-sdk")
27
+ except PackageNotFoundError:
28
+ __version__ = "0.0.0"
29
+
30
+
31
+ __all__ = ["__version__"]
@@ -0,0 +1,220 @@
1
+ # Filename: gait_sdk/application.py
2
+ """
3
+ gait_sdk.application — Gait Application (machine) identity
4
+ ====================================================================
5
+
6
+ SDK2: adds Gait's machine/software identity to gait_sdk, kept
7
+ strictly independent of — and never merged with — human identity.
8
+
9
+ Framework-agnostic, deliberately mirroring `gait_sdk.client`'s own
10
+ structure and style:
11
+
12
+ - `ApplicationPrincipal` — an immutable value object representing a
13
+ Gait-verified Application/Organization/environment identity.
14
+ - `verify_application()` — the one function that turns a raw
15
+ `ApplicationCredential` secret into a verified `ApplicationPrincipal` by
16
+ calling Gait's dedicated application-verification endpoint.
17
+
18
+ Identity separation (do not blur this):
19
+ -----------------------------------------------------------------------
20
+ - `ClaimsUser` / user claims (`gait_sdk.django.authentication`,
21
+ `gait_sdk.fastapi.dependencies`) = HUMAN identity, derived from
22
+ a user JWT via Gait's `/whoami/`.
23
+ - `ApplicationPrincipal` (this module) = SOFTWARE identity, derived from
24
+ an `ApplicationCredential` secret via Gait's `/applications/verify/`.
25
+
26
+ Verifying one never establishes, requires, or invalidates the other — see
27
+ `tests/test_application.py`'s independence tests. Do not add an
28
+ organization/environment/application field to `ClaimsUser`, and do not add
29
+ a human role to `ApplicationPrincipal`.
30
+
31
+ Verified read-only against Gait's frozen backend contract
32
+ (`applications/views.py`, `applications/serializers.py`,
33
+ `applications/services.py`, `applications/test_verification_api.py` in the
34
+ Gait backend repo) — not invented. Endpoint: `POST /api/applications/verify/`
35
+ (same `GAIT_AUTH_URL` base as `/whoami/`). Credential transport: a
36
+ dedicated header, never the human `Authorization: Bearer` header — a
37
+ single request may need to carry both a user token and an application
38
+ credential at once. Every failure (missing/unknown/revoked/expired
39
+ credential, or a suspended/revoked owning Application) is a uniform 401
40
+ with no distinguishing detail — Gait deliberately does not give an
41
+ unauthenticated caller an oracle for probing credential/application state,
42
+ and this SDK does not attempt to recover a finer-grained reason either.
43
+
44
+ Security:
45
+ ---------
46
+ - The raw ApplicationCredential is never logged, never stored on
47
+ `ApplicationPrincipal`, and never echoed into any exception message.
48
+ - `organization_id`, `organization_slug`, and `environment` are exactly
49
+ what Gait's verification response says — there is no parameter on
50
+ `verify_application()` through which a caller can select, override, or
51
+ broaden any of them.
52
+ """
53
+
54
+ from __future__ import annotations
55
+
56
+ import logging
57
+ from dataclasses import dataclass
58
+ from typing import Any, Optional
59
+
60
+ import httpx
61
+
62
+ from gait_sdk.exceptions import AuthServiceUnavailable, InvalidApplicationCredentialError
63
+ from gait_sdk.settings import GAIT_APPLICATION_CREDENTIAL, GAIT_AUTH_URL, GAIT_TIMEOUT
64
+
65
+ # -----------------------------------------------------------------------------
66
+ # ⚙️ Logger (never logs the raw credential — see verify_application)
67
+ # -----------------------------------------------------------------------------
68
+ logger = logging.getLogger("gait_sdk.application")
69
+ logger.setLevel(logging.INFO)
70
+
71
+ # -----------------------------------------------------------------------------
72
+ # 🔑 Dedicated application-credential transport
73
+ # -----------------------------------------------------------------------------
74
+ # Must match Gait's own applications/views.py::APPLICATION_CREDENTIAL_HEADER
75
+ # exactly — confirmed by read-only inspection of the frozen backend, not
76
+ # guessed. Deliberately never Authorization: Bearer, which is reserved for
77
+ # human tokens (see module docstring).
78
+ APPLICATION_CREDENTIAL_HEADER = "Gait-Application-Credential"
79
+
80
+ # Gait's own Application.Environment choices (applications/models.py),
81
+ # mirrored here only so this SDK can fail closed on an unrecognized value.
82
+ # This module has no opinion about what any of these environments *mean* —
83
+ # it only refuses to build an ApplicationPrincipal around a value Gait
84
+ # itself wouldn't have produced.
85
+ _VALID_ENVIRONMENTS = frozenset({"local", "test", "ci", "staging", "production"})
86
+
87
+ _REQUIRED_STRING_FIELDS = (
88
+ "application_id",
89
+ "application_slug",
90
+ "organization_id",
91
+ "organization_slug",
92
+ "environment",
93
+ )
94
+
95
+
96
+ # -----------------------------------------------------------------------------
97
+ # ✅ ApplicationPrincipal — verified machine identity
98
+ # -----------------------------------------------------------------------------
99
+ @dataclass(frozen=True)
100
+ class ApplicationPrincipal:
101
+ """A Gait-verified machine/software identity.
102
+
103
+ Represents "Gait has verified this software identity" — nothing about
104
+ what that software is authorized to do, and nothing about any human.
105
+ Every field comes directly from Gait's verification response; this SDK
106
+ never fills in, defaults, or overrides any of them.
107
+
108
+ Deliberately excludes: the credential itself, any credential hash/id,
109
+ a human role, business-domain permissions, and a Lumen Facility or
110
+ Lumen Organization. See module docstring for the identity-separation
111
+ rationale this reflects.
112
+ """
113
+
114
+ application_id: str
115
+ application_slug: str
116
+ organization_id: str
117
+ organization_slug: str
118
+ environment: str
119
+
120
+
121
+ def _build_application_principal(raw: Any) -> ApplicationPrincipal:
122
+ """Validate Gait's verification response shape before trusting it (fail closed).
123
+
124
+ Authoritative but not blindly trusted: every required field must be a
125
+ non-empty string, and `environment` must be one of Gait's own known
126
+ values. A response missing, mistyping, or emptying any of these never
127
+ produces a partially-populated ApplicationPrincipal — it raises
128
+ AuthServiceUnavailable instead, the same failure class already used
129
+ for "Gait's response can't be trusted" elsewhere in this package (see
130
+ gait_sdk.client.validate_token's own malformed-JSON handling).
131
+ """
132
+ if not isinstance(raw, dict):
133
+ raise AuthServiceUnavailable("Malformed response from authentication service.")
134
+
135
+ for key in _REQUIRED_STRING_FIELDS:
136
+ value = raw.get(key)
137
+ if not isinstance(value, str) or not value:
138
+ raise AuthServiceUnavailable("Malformed response from authentication service.")
139
+
140
+ if raw["environment"] not in _VALID_ENVIRONMENTS:
141
+ raise AuthServiceUnavailable("Malformed response from authentication service.")
142
+
143
+ return ApplicationPrincipal(
144
+ application_id=raw["application_id"],
145
+ application_slug=raw["application_slug"],
146
+ organization_id=raw["organization_id"],
147
+ organization_slug=raw["organization_slug"],
148
+ environment=raw["environment"],
149
+ )
150
+
151
+
152
+ # -----------------------------------------------------------------------------
153
+ # 🔐 Public API — verify_application
154
+ # -----------------------------------------------------------------------------
155
+ async def verify_application(credential: Optional[str] = None) -> ApplicationPrincipal:
156
+ """
157
+ Verify a Gait ApplicationCredential and return the resulting ApplicationPrincipal.
158
+
159
+ Args:
160
+ credential: The raw ApplicationCredential secret. If omitted, falls
161
+ back to the server-side `GAIT_APPLICATION_CREDENTIAL` setting.
162
+ This value is a backend secret — never accept it from a
163
+ browser/frontend request, and never pass a value a caller
164
+ supplied over an untrusted channel.
165
+
166
+ Returns:
167
+ ApplicationPrincipal: the Gait-verified machine identity. Its
168
+ `organization_id`, `organization_slug`, and `environment` are
169
+ exactly what Gait returned — there is no argument here through
170
+ which a caller can select or broaden any of them (see PART 9/10 of
171
+ the SDK2 milestone this implements).
172
+
173
+ Raises:
174
+ InvalidApplicationCredentialError: the credential is missing (not
175
+ configured/provided) or Gait rejected it (unknown, revoked,
176
+ expired, or belongs to a suspended/revoked Application) — Gait
177
+ itself does not distinguish these reasons in its own response,
178
+ and this SDK does not attempt to re-derive a finer-grained one.
179
+ AuthServiceUnavailable: Gait is unreachable, times out, or returns
180
+ a response this SDK cannot parse or trust (malformed JSON, an
181
+ unexpected status code, or a structurally invalid identity
182
+ payload).
183
+ """
184
+ # Step 1: resolve the credential — explicit failure if none exists,
185
+ # never a silently manufactured or fallback identity.
186
+ raw_credential = credential if credential is not None else GAIT_APPLICATION_CREDENTIAL
187
+ if not raw_credential:
188
+ logger.error("No application credential configured or provided - cannot verify.")
189
+ raise InvalidApplicationCredentialError("Application credential is not configured.")
190
+
191
+ if not GAIT_AUTH_URL:
192
+ logger.error("Missing GAIT_AUTH_URL - cannot verify application identity.")
193
+ raise AuthServiceUnavailable("Authentication service misconfigured.")
194
+
195
+ url = f"{GAIT_AUTH_URL.rstrip('/')}/applications/verify/"
196
+ headers = {APPLICATION_CREDENTIAL_HEADER: raw_credential}
197
+
198
+ # Step 2: call Gait. Never log `headers` or `raw_credential`.
199
+ try:
200
+ async with httpx.AsyncClient(timeout=GAIT_TIMEOUT) as client:
201
+ response = await client.post(url, headers=headers)
202
+ except httpx.RequestError:
203
+ logger.error("Gait application-verification API unreachable.")
204
+ raise AuthServiceUnavailable("Authentication service unreachable.")
205
+
206
+ # Step 3: interpret the response.
207
+ if response.status_code == 200:
208
+ try:
209
+ raw = response.json()
210
+ except Exception:
211
+ logger.error("Malformed JSON from Gait during application verification.")
212
+ raise AuthServiceUnavailable("Malformed response from authentication service.")
213
+ return _build_application_principal(raw)
214
+
215
+ if response.status_code == 401:
216
+ logger.warning("Application credential rejected by Gait.")
217
+ raise InvalidApplicationCredentialError("Invalid application credential.")
218
+
219
+ logger.error("Unexpected status %s from Gait during application verification.", response.status_code)
220
+ raise AuthServiceUnavailable(f"Unexpected response: {response.status_code}")