wristband-python-jwt 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.
- wristband_python_jwt-0.1.0/LICENSE +21 -0
- wristband_python_jwt-0.1.0/PKG-INFO +74 -0
- wristband_python_jwt-0.1.0/README.md +405 -0
- wristband_python_jwt-0.1.0/README_PYPI.md +29 -0
- wristband_python_jwt-0.1.0/pyproject.toml +115 -0
- wristband_python_jwt-0.1.0/setup.cfg +4 -0
- wristband_python_jwt-0.1.0/src/wristband/py.typed +0 -0
- wristband_python_jwt-0.1.0/src/wristband/python_jwt/__init__.py +46 -0
- wristband_python_jwt-0.1.0/src/wristband/python_jwt/jwks_client.py +307 -0
- wristband_python_jwt-0.1.0/src/wristband/python_jwt/models.py +351 -0
- wristband_python_jwt-0.1.0/src/wristband/python_jwt/utils/__init__.py +15 -0
- wristband_python_jwt-0.1.0/src/wristband/python_jwt/utils/cache.py +323 -0
- wristband_python_jwt-0.1.0/src/wristband/python_jwt/utils/crypto.py +288 -0
- wristband_python_jwt-0.1.0/src/wristband/python_jwt/validator.py +233 -0
- wristband_python_jwt-0.1.0/src/wristband_python_jwt.egg-info/PKG-INFO +74 -0
- wristband_python_jwt-0.1.0/src/wristband_python_jwt.egg-info/SOURCES.txt +20 -0
- wristband_python_jwt-0.1.0/src/wristband_python_jwt.egg-info/dependency_links.txt +1 -0
- wristband_python_jwt-0.1.0/src/wristband_python_jwt.egg-info/requires.txt +17 -0
- wristband_python_jwt-0.1.0/src/wristband_python_jwt.egg-info/top_level.txt +1 -0
- wristband_python_jwt-0.1.0/tests/test_jwks_client.py +849 -0
- wristband_python_jwt-0.1.0/tests/test_models.py +688 -0
- wristband_python_jwt-0.1.0/tests/test_validator.py +438 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Apitopia, Inc. (dba Wristband)
|
|
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,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wristband-python-jwt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Framework-agnostic Python SDK for validating JWT access tokens issued by Wristband.
|
|
5
|
+
Author-email: Wristband <support@wristband.dev>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://wristband.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/wristband-dev/python-jwt
|
|
9
|
+
Project-URL: Documentation, https://docs.wristband.dev
|
|
10
|
+
Keywords: api,auth,authentication,authorization,fastapi,flask,django,jwt,multi-tenant,multi-tenancy,oauth,oidc,sdk,secure,security,sso,validation,wristband
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: Security
|
|
21
|
+
Classifier: Development Status :: 4 - Beta
|
|
22
|
+
Classifier: Framework :: FastAPI
|
|
23
|
+
Classifier: Framework :: Flask
|
|
24
|
+
Classifier: Framework :: Django
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: cryptography<45.0.0,>=41.0.0
|
|
29
|
+
Requires-Dist: httpx>=0.24.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: setuptools>=61; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest<9.0.0,>=8.2.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-cov<6.0.0,>=5.0.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-httpx>=0.21.0; extra == "dev"
|
|
35
|
+
Requires-Dist: mypy>=1.10.0; extra == "dev"
|
|
36
|
+
Requires-Dist: flake8<7.0.0,>=6.0.0; extra == "dev"
|
|
37
|
+
Requires-Dist: flake8-pyproject>=1.2.0; extra == "dev"
|
|
38
|
+
Requires-Dist: pip-audit>=2.0.0; extra == "dev"
|
|
39
|
+
Requires-Dist: bandit>=1.7.0; extra == "dev"
|
|
40
|
+
Requires-Dist: build>=0.10.0; extra == "dev"
|
|
41
|
+
Requires-Dist: twine>=4.0.0; extra == "dev"
|
|
42
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
43
|
+
Requires-Dist: isort>=5.12.0; extra == "dev"
|
|
44
|
+
Dynamic: license-file
|
|
45
|
+
|
|
46
|
+
# Wristband Framework-Agnostic JWT Validation SDK for Python
|
|
47
|
+
|
|
48
|
+
Wristband provides enterprise-ready auth that is secure by default, truly multi-tenant, and ungated for small businesses.
|
|
49
|
+
|
|
50
|
+
- Website: [Wristband Website](https://wristband.dev)
|
|
51
|
+
- Documentation: [Wristband Docs](https://docs.wristband.dev/)
|
|
52
|
+
|
|
53
|
+
For detailed setup instructions and usage guidelines, visit the project's GitHub repository.
|
|
54
|
+
|
|
55
|
+
- [Python JWT SDK - GitHub](https://github.com/wristband-dev/python-jwt)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
## Details
|
|
59
|
+
|
|
60
|
+
This SDK provides secure JWT validation capabilities for applications using Wristband authentication. It is framework-agnostic and works with FastAPI, Flask, Django, and other Python web frameworks. The SDK follows OWASP security best practices and is supported for Python 3.9+. Key functionalities include:
|
|
61
|
+
|
|
62
|
+
- Extracting Bearer tokens from HTTP Authorization headers.
|
|
63
|
+
- Validating JWT signatures using Wristband's JWKS endpoint.
|
|
64
|
+
- Verifying token claims including issuer, expiration, and algorithm allowlisting to prevent common JWT vulnerabilities.
|
|
65
|
+
- Automatic JWKS key caching and rotation for optimal performance.
|
|
66
|
+
- Comprehensive error handling with detailed validation messages.
|
|
67
|
+
|
|
68
|
+
You can learn more about JWTs in Wristband in our documentation:
|
|
69
|
+
|
|
70
|
+
- [JWTs and Signing Keys](https://docs.wristband.dev/docs/json-web-tokens-jwts-and-signing-keys)
|
|
71
|
+
|
|
72
|
+
## Questions
|
|
73
|
+
|
|
74
|
+
Reach out to the Wristband team at <support@wristband.dev> for any questions regarding this SDK.
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://wristband.dev">
|
|
3
|
+
<picture>
|
|
4
|
+
<img src="https://assets.wristband.dev/images/email_branding_logo_v1.png" alt="Wristband" width="297" height="64">
|
|
5
|
+
</picture>
|
|
6
|
+
</a>
|
|
7
|
+
<p align="center">
|
|
8
|
+
Enterprise-ready auth that is secure by default, truly multi-tenant, and ungated for small businesses.
|
|
9
|
+
</p>
|
|
10
|
+
<p align="center">
|
|
11
|
+
<b>
|
|
12
|
+
<a href="https://wristband.dev">Website</a> •
|
|
13
|
+
<a href="https://docs.wristband.dev/">Documentation</a>
|
|
14
|
+
</b>
|
|
15
|
+
</p>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<br/>
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<br/>
|
|
23
|
+
|
|
24
|
+
# Wristband JWT Validation SDK for Python
|
|
25
|
+
|
|
26
|
+
This framework-agnostic Python SDK validates JWT access tokens issued by Wristband for user or machine authentication. It uses the Wristband JWKS endpoint to resolve signing keys and verify RS256 signatures. Validation includes issuer verification, lifetime checks, and signature validation using cached keys. Developers should use this to protect routes and ensure that only valid, Wristband-issued access tokens can access secured APIs.
|
|
27
|
+
|
|
28
|
+
You can learn more about JWTs in Wristband in our documentation:
|
|
29
|
+
|
|
30
|
+
- [JWTs and Signing Keys](https://docs.wristband.dev/docs/json-web-tokens-jwts-and-signing-keys)
|
|
31
|
+
|
|
32
|
+
<br/>
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
This SDK is designed to work with Python 3.9+ and any Python framework (FastAPI, Django, Flask, etc.). It uses minimal dependencies for maximum compatibility and security.
|
|
37
|
+
|
|
38
|
+
<br/>
|
|
39
|
+
|
|
40
|
+
## 1) Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install wristband-python-jwt
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
or
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
poetry add wristband-python-jwt
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
or
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pipenv install wristband-python-jwt
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
You should see the dependency added to your `requirements.txt` file:
|
|
59
|
+
|
|
60
|
+
```txt
|
|
61
|
+
wristband-python-jwt==0.1.0
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or in your `pyproject.toml`:
|
|
65
|
+
|
|
66
|
+
```txt
|
|
67
|
+
dependencies = [
|
|
68
|
+
"wristband-python-jwt==0.1.0",
|
|
69
|
+
# ...other dependencies...
|
|
70
|
+
]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Or in your `Pipfile`:
|
|
74
|
+
```txt
|
|
75
|
+
[packages]
|
|
76
|
+
wristband-python-jwt = "==0.1.0"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
<br/>
|
|
80
|
+
|
|
81
|
+
## 2) Wristband Configuration
|
|
82
|
+
|
|
83
|
+
First, you'll need to make sure you have an Application in your Wristband Dashboard account. If you haven't done so yet, refer to our docs on [Creating an Application](https://docs.wristband.dev/docs/quick-start-create-a-wristband-application).
|
|
84
|
+
|
|
85
|
+
**Make sure to copy the Application Vanity Domain for next steps, which can be found in "Application Settings" for your Wristband Application.**
|
|
86
|
+
|
|
87
|
+
<br/>
|
|
88
|
+
|
|
89
|
+
## 3) SDK Configuration
|
|
90
|
+
|
|
91
|
+
First, create an instance of `WristbandJwtValidator` in your server's directory structure in any location of your choice (i.e.: `src/wristband.py`). Then, you can export this instance and use it across your project. When creating an instance, you provide all necessary configurations for your application to correlate with how you've set it up in Wristband.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# src/wristband.py
|
|
95
|
+
from wristband.python_jwt import create_wristband_jwt_validator, WristbandJwtValidatorConfig
|
|
96
|
+
|
|
97
|
+
wristband_jwt_validator = create_wristband_jwt_validator(
|
|
98
|
+
WristbandJwtValidatorConfig(
|
|
99
|
+
wristband_application_vanity_domain='auth.yourapp.io'
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
<br/>
|
|
105
|
+
|
|
106
|
+
## 4) Extract and Validate JWT Tokens
|
|
107
|
+
|
|
108
|
+
The SDK provides methods to extract Bearer tokens from Authorization headers and validate them. Here are examples for a few frameworks:
|
|
109
|
+
|
|
110
|
+
### FastAPI
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
# jwt_auth_middleware.py
|
|
114
|
+
from fastapi import Request, Response, status
|
|
115
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
116
|
+
from .wristband import wristband_jwt_validator
|
|
117
|
+
|
|
118
|
+
class JwtAuthMiddleware(BaseHTTPMiddleware):
|
|
119
|
+
async def dispatch(self, request: Request, call_next):
|
|
120
|
+
# Adjust paths as needed
|
|
121
|
+
if not request.url.path.startswith('/api/protected/'):
|
|
122
|
+
return await call_next(request)
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
auth_header = request.headers.get("authorization")
|
|
126
|
+
token = wristband_jwt_validator.extract_bearer_token(auth_header)
|
|
127
|
+
result = wristband_jwt_validator.validate(token)
|
|
128
|
+
|
|
129
|
+
if not result.is_valid:
|
|
130
|
+
print(f"JWT validation middleware error: {result.error_message}")
|
|
131
|
+
return Response(status_code=status.HTTP_401_UNAUTHORIZED)
|
|
132
|
+
|
|
133
|
+
return await call_next(request)
|
|
134
|
+
|
|
135
|
+
except Exception as error:
|
|
136
|
+
print(f"JWT validation middleware error: {error}")
|
|
137
|
+
return Response(status_code=status.HTTP_401_UNAUTHORIZED)
|
|
138
|
+
|
|
139
|
+
# main.py
|
|
140
|
+
from fastapi import FastAPI, Request
|
|
141
|
+
from .middleware import JwtAuthMiddleware
|
|
142
|
+
|
|
143
|
+
app = FastAPI()
|
|
144
|
+
|
|
145
|
+
# Add JWT authentication middleware
|
|
146
|
+
app.add_middleware(JwtAuthMiddleware)
|
|
147
|
+
|
|
148
|
+
@app.get("/api/protected/data")
|
|
149
|
+
async def protected_data(request: Request):
|
|
150
|
+
return { "message": "Hello from protected API!" }
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
<br/>
|
|
154
|
+
|
|
155
|
+
### Django
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
# your_app/jwt_auth_middleware.py
|
|
159
|
+
from django.http import HttpResponse
|
|
160
|
+
from django.utils.deprecation import MiddlewareMixin
|
|
161
|
+
from .wristband import wristband_jwt_validator
|
|
162
|
+
|
|
163
|
+
class JwtAuthMiddleware(MiddlewareMixin):
|
|
164
|
+
def process_request(self, request):
|
|
165
|
+
# Adjust paths as needed
|
|
166
|
+
if not request.path.startswith('/api/protected/'):
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
auth_header = request.META.get('HTTP_AUTHORIZATION')
|
|
171
|
+
token = wristband_jwt_validator.extract_bearer_token(auth_header)
|
|
172
|
+
result = wristband_jwt_validator.validate(token)
|
|
173
|
+
|
|
174
|
+
if not result.is_valid:
|
|
175
|
+
print(f"JWT validation middleware error: {result.error_message}")
|
|
176
|
+
return HttpResponse(status=401)
|
|
177
|
+
|
|
178
|
+
except Exception as error:
|
|
179
|
+
print(f"JWT validation middleware error: {error}")
|
|
180
|
+
return HttpResponse(status=401)
|
|
181
|
+
|
|
182
|
+
# your_project/settings.py
|
|
183
|
+
MIDDLEWARE = [
|
|
184
|
+
# ...other middlewares...
|
|
185
|
+
'your_app.middleware.JwtAuthMiddleware', # Add your JWT middleware
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
# your_project/urls.py
|
|
189
|
+
from django.contrib import admin
|
|
190
|
+
from django.urls import path
|
|
191
|
+
from your_app import views
|
|
192
|
+
|
|
193
|
+
urlpatterns = [
|
|
194
|
+
# The JWT middleware will execute before the business logic occurs.
|
|
195
|
+
path('api/protected/data/', views.protected_view, name='protected_data'),
|
|
196
|
+
# ...other URLs...
|
|
197
|
+
]
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
<br/>
|
|
201
|
+
|
|
202
|
+
### Flask
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
# jwt_auth_middleware.py
|
|
206
|
+
from flask import request, jsonify, g
|
|
207
|
+
from functools import wraps
|
|
208
|
+
from .wristband import wristband_jwt_validator
|
|
209
|
+
|
|
210
|
+
def jwt_auth_middleware():
|
|
211
|
+
# Adjust paths as needed
|
|
212
|
+
if not request.path.startswith('/api/protected/'):
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
auth_header = request.headers.get('Authorization')
|
|
217
|
+
token = wristband_jwt_validator.extract_bearer_token(auth_header)
|
|
218
|
+
result = wristband_jwt_validator.validate(token)
|
|
219
|
+
|
|
220
|
+
if not result.is_valid:
|
|
221
|
+
print(f"JWT validation middleware error: {result.error_message}")
|
|
222
|
+
return '', 401
|
|
223
|
+
|
|
224
|
+
except Exception as error:
|
|
225
|
+
print(f"JWT validation middleware error: {error}")
|
|
226
|
+
return '', 401
|
|
227
|
+
|
|
228
|
+
# app.py
|
|
229
|
+
from flask import Flask, jsonify
|
|
230
|
+
from .middleware import jwt_auth_middleware
|
|
231
|
+
|
|
232
|
+
app = Flask(__name__)
|
|
233
|
+
|
|
234
|
+
# Register JWT middleware to run before each request
|
|
235
|
+
app.before_request(jwt_auth_middleware)
|
|
236
|
+
|
|
237
|
+
@app.route('/api/protected/data', methods=['GET'])
|
|
238
|
+
def protected_data():
|
|
239
|
+
return jsonify({"message": "Hello from protected API!"})
|
|
240
|
+
|
|
241
|
+
if __name__ == '__main__':
|
|
242
|
+
app.run(debug=True)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
<br/>
|
|
246
|
+
|
|
247
|
+
## JWKS Caching and Expiration
|
|
248
|
+
|
|
249
|
+
The SDK automatically retrieves and caches JSON Web Key Sets (JWKS) from your Wristband application's domain to validate incoming access tokens. By default, keys are cached in memory and reused across requests to avoid unnecessary network calls.
|
|
250
|
+
|
|
251
|
+
You can control how the SDK handles this caching behavior using two optional configuration values: `jwks_cache_max_size` and `jwks_cache_ttl`.
|
|
252
|
+
|
|
253
|
+
**Set a limit on how many keys to keep in memory:**
|
|
254
|
+
```python
|
|
255
|
+
validator = create_wristband_jwt_validator(
|
|
256
|
+
WristbandJwtValidatorConfig(
|
|
257
|
+
wristband_application_vanity_domain='auth.yourapp.io',
|
|
258
|
+
jwks_cache_max_size=10 # Keep at most 10 keys in cache
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Set a time-to-live duration for each key:**
|
|
264
|
+
```python
|
|
265
|
+
validator = create_wristband_jwt_validator(
|
|
266
|
+
WristbandJwtValidatorConfig(
|
|
267
|
+
wristband_application_vanity_domain='auth.yourapp.io',
|
|
268
|
+
jwks_cache_ttl=2629746000 # Expire keys from cache after 1 month (in milliseconds)
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
If `jwks_cache_ttl` is not set, cached keys remain available until evicted by the cache size limit.
|
|
274
|
+
|
|
275
|
+
<br>
|
|
276
|
+
|
|
277
|
+
## SDK Configuration Options
|
|
278
|
+
|
|
279
|
+
| JWT Validation Option | Type | Required | Description |
|
|
280
|
+
| --------------------- | ---- | -------- | ----------- |
|
|
281
|
+
| jwks_cache_max_size | int | No | Maximum number of JWKs to cache in memory. When exceeded, the least recently used keys are evicted. Defaults to 20. |
|
|
282
|
+
| jwks_cache_ttl | int | No | Time-to-live for cached JWKs, in milliseconds. If not set, keys remain in cache until eviction by size limit. |
|
|
283
|
+
| wristband_application_vanity_domain | str | Yes | The Wristband vanity domain used to construct the JWKS endpoint URL for verifying tokens. Example: `myapp.wristband.dev`. |
|
|
284
|
+
|
|
285
|
+
<br/>
|
|
286
|
+
|
|
287
|
+
## API Reference
|
|
288
|
+
|
|
289
|
+
### `create_wristband_jwt_validator(config)`
|
|
290
|
+
|
|
291
|
+
This is a factory function that creates a configured JWT validator instance.
|
|
292
|
+
|
|
293
|
+
**Parameters:**
|
|
294
|
+
| Name | Type | Required | Description |
|
|
295
|
+
| ---- | ---- | -------- | ----------- |
|
|
296
|
+
| config | `WristbandJwtValidatorConfig` | Yes | Configuration options (see [SDK Configuration Options](#sdk-configuration-options)) |
|
|
297
|
+
|
|
298
|
+
**Returns:**
|
|
299
|
+
- The configured `WristbandJwtValidator` instance
|
|
300
|
+
|
|
301
|
+
**Example:**
|
|
302
|
+
```python
|
|
303
|
+
validator = create_wristband_jwt_validator(
|
|
304
|
+
WristbandJwtValidatorConfig(
|
|
305
|
+
wristband_application_vanity_domain='myapp.wristband.dev',
|
|
306
|
+
jwks_cache_max_size=20,
|
|
307
|
+
jwks_cache_ttl=3600000
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
<br/>
|
|
313
|
+
|
|
314
|
+
### `extract_bearer_token(authorization_header)`
|
|
315
|
+
|
|
316
|
+
This is used to extract the raw Bearer token from an HTTP Authorization header. It can handle various input formats and validates the Bearer scheme according to [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750).
|
|
317
|
+
|
|
318
|
+
The function will raise an error for the following cases:
|
|
319
|
+
- The Authorization header is missing
|
|
320
|
+
- The Authorization header is malformed
|
|
321
|
+
- The Authorization header contains multiple entries
|
|
322
|
+
- The Authorization header uses wrong scheme (i.e. not using `Bearer`)
|
|
323
|
+
- The Authorization header is missing the token value
|
|
324
|
+
|
|
325
|
+
**Parameters:**
|
|
326
|
+
| Name | Type | Required | Description |
|
|
327
|
+
| ---- | ---- | -------- | ----------- |
|
|
328
|
+
| authorization_header | str or list[str] | Yes | The Authorization header value(s) of the current request. |
|
|
329
|
+
|
|
330
|
+
**Returns:**
|
|
331
|
+
| Type | Description |
|
|
332
|
+
| ---- | ----------- |
|
|
333
|
+
| str | The extracted Bearer token |
|
|
334
|
+
|
|
335
|
+
**Valid usage examples:**
|
|
336
|
+
```python
|
|
337
|
+
token1 = wristband_jwt_validator.extract_bearer_token('Bearer abc123')
|
|
338
|
+
token2 = wristband_jwt_validator.extract_bearer_token(['Bearer abc123'])
|
|
339
|
+
# From FastAPI request
|
|
340
|
+
token3 = wristband_jwt_validator.extract_bearer_token(request.headers.get('authorization'))
|
|
341
|
+
# From Django request
|
|
342
|
+
token4 = wristband_jwt_validator.extract_bearer_token(request.META.get('HTTP_AUTHORIZATION'))
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**Invalid cases that raise errors:**
|
|
346
|
+
```python
|
|
347
|
+
wristband_jwt_validator.extract_bearer_token(['Bearer abc', 'Bearer xyz'])
|
|
348
|
+
wristband_jwt_validator.extract_bearer_token([])
|
|
349
|
+
wristband_jwt_validator.extract_bearer_token('Basic abc123')
|
|
350
|
+
wristband_jwt_validator.extract_bearer_token('Bearer ')
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### `validate(token)`
|
|
354
|
+
|
|
355
|
+
Validates a JWT access token issued by Wristband. Performs comprehensive validation including format checking, signature verification, issuer validation, and expiration checks.
|
|
356
|
+
|
|
357
|
+
**Parameters:**
|
|
358
|
+
| Name | Type | Required | Description |
|
|
359
|
+
| ---- | ---- | -------- | ----------- |
|
|
360
|
+
| token | str | Yes | The Wristband JWT token to validate. |
|
|
361
|
+
|
|
362
|
+
**Returns:**
|
|
363
|
+
| Type | Description |
|
|
364
|
+
| ---- | ----------- |
|
|
365
|
+
| `JwtValidationResult` | Validation result object. |
|
|
366
|
+
|
|
367
|
+
JwtValidationResult attributes:
|
|
368
|
+
```python
|
|
369
|
+
class JwtValidationResult:
|
|
370
|
+
is_valid: bool
|
|
371
|
+
payload: Optional[JWTPayload] # Present when is_valid is True
|
|
372
|
+
error_message: Optional[str] # Present when is_valid is False
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
JWTPayload properties:
|
|
376
|
+
```python
|
|
377
|
+
class JWTPayload:
|
|
378
|
+
iss: Optional[str] # Issuer
|
|
379
|
+
sub: Optional[str] # Subject (user ID)
|
|
380
|
+
aud: Optional[Union[str, List[str]]] # Audience
|
|
381
|
+
exp: Optional[int] # Expiration time (Unix timestamp)
|
|
382
|
+
nbf: Optional[int] # Not before (Unix timestamp)
|
|
383
|
+
iat: Optional[int] # Issued at (Unix timestamp)
|
|
384
|
+
jti: Optional[str] # JWT ID
|
|
385
|
+
# ... plus any additional Wristband custom claims...
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**Valid usage examples:**
|
|
389
|
+
```python
|
|
390
|
+
result = validator.validate(token)
|
|
391
|
+
|
|
392
|
+
if result.is_valid:
|
|
393
|
+
print('User ID:', result.payload.sub)
|
|
394
|
+
print('Expires at:', datetime.fromtimestamp(result.payload.exp))
|
|
395
|
+
else:
|
|
396
|
+
print('Validation failed:', result.error_message)
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
<br/>
|
|
400
|
+
|
|
401
|
+
## Questions
|
|
402
|
+
|
|
403
|
+
Reach out to the Wristband team at <support@wristband.dev> for any questions regarding this SDK.
|
|
404
|
+
|
|
405
|
+
<br/>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Wristband Framework-Agnostic JWT Validation SDK for Python
|
|
2
|
+
|
|
3
|
+
Wristband provides enterprise-ready auth that is secure by default, truly multi-tenant, and ungated for small businesses.
|
|
4
|
+
|
|
5
|
+
- Website: [Wristband Website](https://wristband.dev)
|
|
6
|
+
- Documentation: [Wristband Docs](https://docs.wristband.dev/)
|
|
7
|
+
|
|
8
|
+
For detailed setup instructions and usage guidelines, visit the project's GitHub repository.
|
|
9
|
+
|
|
10
|
+
- [Python JWT SDK - GitHub](https://github.com/wristband-dev/python-jwt)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## Details
|
|
14
|
+
|
|
15
|
+
This SDK provides secure JWT validation capabilities for applications using Wristband authentication. It is framework-agnostic and works with FastAPI, Flask, Django, and other Python web frameworks. The SDK follows OWASP security best practices and is supported for Python 3.9+. Key functionalities include:
|
|
16
|
+
|
|
17
|
+
- Extracting Bearer tokens from HTTP Authorization headers.
|
|
18
|
+
- Validating JWT signatures using Wristband's JWKS endpoint.
|
|
19
|
+
- Verifying token claims including issuer, expiration, and algorithm allowlisting to prevent common JWT vulnerabilities.
|
|
20
|
+
- Automatic JWKS key caching and rotation for optimal performance.
|
|
21
|
+
- Comprehensive error handling with detailed validation messages.
|
|
22
|
+
|
|
23
|
+
You can learn more about JWTs in Wristband in our documentation:
|
|
24
|
+
|
|
25
|
+
- [JWTs and Signing Keys](https://docs.wristband.dev/docs/json-web-tokens-jwts-and-signing-keys)
|
|
26
|
+
|
|
27
|
+
## Questions
|
|
28
|
+
|
|
29
|
+
Reach out to the Wristband team at <support@wristband.dev> for any questions regarding this SDK.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wristband-python-jwt"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Framework-agnostic Python SDK for validating JWT access tokens issued by Wristband."
|
|
9
|
+
readme = "README_PYPI.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Wristband", email = "support@wristband.dev"},
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.9"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"cryptography>=41.0.0,<45.0.0",
|
|
17
|
+
"httpx>=0.24.0",
|
|
18
|
+
]
|
|
19
|
+
keywords = [
|
|
20
|
+
"api",
|
|
21
|
+
"auth",
|
|
22
|
+
"authentication",
|
|
23
|
+
"authorization",
|
|
24
|
+
"fastapi",
|
|
25
|
+
"flask",
|
|
26
|
+
"django",
|
|
27
|
+
"jwt",
|
|
28
|
+
"multi-tenant",
|
|
29
|
+
"multi-tenancy",
|
|
30
|
+
"oauth",
|
|
31
|
+
"oidc",
|
|
32
|
+
"sdk",
|
|
33
|
+
"secure",
|
|
34
|
+
"security",
|
|
35
|
+
"sso",
|
|
36
|
+
"validation",
|
|
37
|
+
"wristband"
|
|
38
|
+
]
|
|
39
|
+
classifiers = [
|
|
40
|
+
"Programming Language :: Python :: 3",
|
|
41
|
+
"Programming Language :: Python :: 3.9",
|
|
42
|
+
"Programming Language :: Python :: 3.10",
|
|
43
|
+
"Programming Language :: Python :: 3.11",
|
|
44
|
+
"Programming Language :: Python :: 3.12",
|
|
45
|
+
"Programming Language :: Python :: 3.13",
|
|
46
|
+
"Operating System :: OS Independent",
|
|
47
|
+
"Intended Audience :: Developers",
|
|
48
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
49
|
+
"Topic :: Security",
|
|
50
|
+
"Development Status :: 4 - Beta",
|
|
51
|
+
"Framework :: FastAPI",
|
|
52
|
+
"Framework :: Flask",
|
|
53
|
+
"Framework :: Django",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[project.optional-dependencies]
|
|
57
|
+
dev = [
|
|
58
|
+
"setuptools>=61",
|
|
59
|
+
"pytest>=8.2.0,<9.0.0",
|
|
60
|
+
"pytest-cov>=5.0.0,<6.0.0",
|
|
61
|
+
"pytest-httpx>=0.21.0",
|
|
62
|
+
"mypy>=1.10.0",
|
|
63
|
+
"flake8>=6.0.0,<7.0.0",
|
|
64
|
+
"flake8-pyproject>=1.2.0",
|
|
65
|
+
"pip-audit>=2.0.0",
|
|
66
|
+
"bandit>=1.7.0",
|
|
67
|
+
"build>=0.10.0",
|
|
68
|
+
"twine>=4.0.0",
|
|
69
|
+
"black>=23.0.0",
|
|
70
|
+
"isort>=5.12.0",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[project.urls]
|
|
74
|
+
"Homepage" = "https://wristband.dev"
|
|
75
|
+
"Repository" = "https://github.com/wristband-dev/python-jwt"
|
|
76
|
+
"Documentation" = "https://docs.wristband.dev"
|
|
77
|
+
|
|
78
|
+
[tool.setuptools.packages.find]
|
|
79
|
+
where = ["src"]
|
|
80
|
+
|
|
81
|
+
[tool.setuptools.package-dir]
|
|
82
|
+
"" = "src"
|
|
83
|
+
|
|
84
|
+
[tool.setuptools.package-data]
|
|
85
|
+
"wristband" = ["py.typed"]
|
|
86
|
+
|
|
87
|
+
[tool.pytest.ini_options]
|
|
88
|
+
pythonpath = ["src"]
|
|
89
|
+
minversion = "8.0"
|
|
90
|
+
addopts = "-ra -q --strict-markers --strict-config"
|
|
91
|
+
testpaths = ["tests"]
|
|
92
|
+
|
|
93
|
+
[tool.flake8]
|
|
94
|
+
max-line-length = 120
|
|
95
|
+
extend-ignore = ["E203", "W503"]
|
|
96
|
+
|
|
97
|
+
[tool.mypy]
|
|
98
|
+
python_version = "3.9"
|
|
99
|
+
strict = true
|
|
100
|
+
warn_return_any = true
|
|
101
|
+
warn_unused_configs = true
|
|
102
|
+
disallow_untyped_defs = true
|
|
103
|
+
exclude = ["tests/"]
|
|
104
|
+
|
|
105
|
+
[tool.black]
|
|
106
|
+
line-length = 120
|
|
107
|
+
target-version = ['py39']
|
|
108
|
+
|
|
109
|
+
[tool.isort]
|
|
110
|
+
profile = "black"
|
|
111
|
+
line_length = 120
|
|
112
|
+
|
|
113
|
+
[tool.coverage.run]
|
|
114
|
+
source = ["src"]
|
|
115
|
+
omit = ["tests/*"]
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wristband JWT Validation SDK for Python.
|
|
3
|
+
|
|
4
|
+
Framework-agnostic Python SDK for validating JWT access tokens issued by Wristband
|
|
5
|
+
for user or machine authentication. Uses the Wristband JWKS endpoint to resolve
|
|
6
|
+
signing keys and verify RS256 signatures.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
```python
|
|
10
|
+
from wristband.python_jwt import create_wristband_jwt_validator, WristbandJwtValidatorConfig
|
|
11
|
+
|
|
12
|
+
# Create validator instance (reuse across requests)
|
|
13
|
+
validator = create_wristband_jwt_validator(
|
|
14
|
+
WristbandJwtValidatorConfig(wristband_application_vanity_domain='myapp.wristband.dev')
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Extract and validate token
|
|
18
|
+
def verify_token(authorization_header):
|
|
19
|
+
try:
|
|
20
|
+
token = validator.extract_bearer_token(authorization_header)
|
|
21
|
+
result = await validator.validate(token)
|
|
22
|
+
|
|
23
|
+
if result.is_valid:
|
|
24
|
+
return result.payload
|
|
25
|
+
else:
|
|
26
|
+
raise ValueError(result.error_message)
|
|
27
|
+
except Exception as error:
|
|
28
|
+
raise ValueError(f"Authentication failed: {error}")
|
|
29
|
+
```
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from .models import (
|
|
33
|
+
JWTPayload,
|
|
34
|
+
JwtValidationResult,
|
|
35
|
+
WristbandJwtValidator,
|
|
36
|
+
WristbandJwtValidatorConfig,
|
|
37
|
+
)
|
|
38
|
+
from .validator import create_wristband_jwt_validator
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"WristbandJwtValidator",
|
|
42
|
+
"WristbandJwtValidatorConfig",
|
|
43
|
+
"JWTPayload",
|
|
44
|
+
"JwtValidationResult",
|
|
45
|
+
"create_wristband_jwt_validator",
|
|
46
|
+
]
|