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.
- gait_sdk-0.5.0/LICENSE +21 -0
- gait_sdk-0.5.0/PKG-INFO +187 -0
- gait_sdk-0.5.0/README.md +144 -0
- gait_sdk-0.5.0/auth_integration/__init__.py +64 -0
- gait_sdk-0.5.0/gait_sdk/__init__.py +31 -0
- gait_sdk-0.5.0/gait_sdk/application.py +220 -0
- gait_sdk-0.5.0/gait_sdk/apps.py +19 -0
- gait_sdk-0.5.0/gait_sdk/authentication.py +21 -0
- gait_sdk-0.5.0/gait_sdk/client.py +145 -0
- gait_sdk-0.5.0/gait_sdk/context.py +160 -0
- gait_sdk-0.5.0/gait_sdk/django/__init__.py +0 -0
- gait_sdk-0.5.0/gait_sdk/django/authentication.py +480 -0
- gait_sdk-0.5.0/gait_sdk/exceptions.py +100 -0
- gait_sdk-0.5.0/gait_sdk/fastapi/__init__.py +0 -0
- gait_sdk-0.5.0/gait_sdk/fastapi/dependencies.py +228 -0
- gait_sdk-0.5.0/gait_sdk/permissions.py +214 -0
- gait_sdk-0.5.0/gait_sdk/security.py +310 -0
- gait_sdk-0.5.0/gait_sdk/session.py +106 -0
- gait_sdk-0.5.0/gait_sdk/settings.py +126 -0
- gait_sdk-0.5.0/gait_sdk/utils.py +97 -0
- gait_sdk-0.5.0/gait_sdk/verification.py +568 -0
- gait_sdk-0.5.0/gait_sdk.egg-info/PKG-INFO +187 -0
- gait_sdk-0.5.0/gait_sdk.egg-info/SOURCES.txt +46 -0
- gait_sdk-0.5.0/gait_sdk.egg-info/dependency_links.txt +1 -0
- gait_sdk-0.5.0/gait_sdk.egg-info/requires.txt +17 -0
- gait_sdk-0.5.0/gait_sdk.egg-info/top_level.txt +2 -0
- gait_sdk-0.5.0/pyproject.toml +114 -0
- gait_sdk-0.5.0/setup.cfg +4 -0
- gait_sdk-0.5.0/tests/test_application.py +432 -0
- gait_sdk-0.5.0/tests/test_client.py +127 -0
- gait_sdk-0.5.0/tests/test_context.py +247 -0
- gait_sdk-0.5.0/tests/test_dependencies.py +194 -0
- gait_sdk-0.5.0/tests/test_django_authentication.py +400 -0
- gait_sdk-0.5.0/tests/test_exceptions.py +61 -0
- gait_sdk-0.5.0/tests/test_integration_auth.py +0 -0
- gait_sdk-0.5.0/tests/test_jwks_adapters.py +325 -0
- gait_sdk-0.5.0/tests/test_jwks_verifier.py +366 -0
- gait_sdk-0.5.0/tests/test_permissions.py +83 -0
- gait_sdk-0.5.0/tests/test_permissions_fastapi_fallback.py +170 -0
- gait_sdk-0.5.0/tests/test_rename_compat_shim.py +101 -0
- gait_sdk-0.5.0/tests/test_role_dependency_status_codes.py +109 -0
- gait_sdk-0.5.0/tests/test_security_hardening_050.py +271 -0
- gait_sdk-0.5.0/tests/test_security_signal.py +539 -0
- gait_sdk-0.5.0/tests/test_session_check.py +115 -0
- gait_sdk-0.5.0/tests/test_settings.py +106 -0
- gait_sdk-0.5.0/tests/test_settings_decouple_isolation.py +34 -0
- gait_sdk-0.5.0/tests/test_settings_django_unconfigured.py +20 -0
- 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.
|
gait_sdk-0.5.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/gait-sdk/)
|
|
47
|
+
[](https://pypi.org/project/gait-sdk/)
|
|
48
|
+
[](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.
|
gait_sdk-0.5.0/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# gait-sdk
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/gait-sdk/)
|
|
4
|
+
[](https://pypi.org/project/gait-sdk/)
|
|
5
|
+
[](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}")
|