django-bolt 0.1.0__cp310-abi3-win_amd64.whl
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.
Potentially problematic release.
This version of django-bolt might be problematic. Click here for more details.
- django_bolt/__init__.py +147 -0
- django_bolt/_core.pyd +0 -0
- django_bolt/admin/__init__.py +25 -0
- django_bolt/admin/admin_detection.py +179 -0
- django_bolt/admin/asgi_bridge.py +267 -0
- django_bolt/admin/routes.py +91 -0
- django_bolt/admin/static.py +155 -0
- django_bolt/admin/static_routes.py +111 -0
- django_bolt/api.py +1011 -0
- django_bolt/apps.py +7 -0
- django_bolt/async_collector.py +228 -0
- django_bolt/auth/README.md +464 -0
- django_bolt/auth/REVOCATION_EXAMPLE.md +391 -0
- django_bolt/auth/__init__.py +84 -0
- django_bolt/auth/backends.py +236 -0
- django_bolt/auth/guards.py +224 -0
- django_bolt/auth/jwt_utils.py +212 -0
- django_bolt/auth/revocation.py +286 -0
- django_bolt/auth/token.py +335 -0
- django_bolt/binding.py +363 -0
- django_bolt/bootstrap.py +77 -0
- django_bolt/cli.py +133 -0
- django_bolt/compression.py +104 -0
- django_bolt/decorators.py +159 -0
- django_bolt/dependencies.py +128 -0
- django_bolt/error_handlers.py +305 -0
- django_bolt/exceptions.py +294 -0
- django_bolt/health.py +129 -0
- django_bolt/logging/__init__.py +6 -0
- django_bolt/logging/config.py +357 -0
- django_bolt/logging/middleware.py +296 -0
- django_bolt/management/__init__.py +1 -0
- django_bolt/management/commands/__init__.py +0 -0
- django_bolt/management/commands/runbolt.py +427 -0
- django_bolt/middleware/__init__.py +32 -0
- django_bolt/middleware/compiler.py +131 -0
- django_bolt/middleware/middleware.py +247 -0
- django_bolt/openapi/__init__.py +23 -0
- django_bolt/openapi/config.py +196 -0
- django_bolt/openapi/plugins.py +439 -0
- django_bolt/openapi/routes.py +152 -0
- django_bolt/openapi/schema_generator.py +581 -0
- django_bolt/openapi/spec/__init__.py +68 -0
- django_bolt/openapi/spec/base.py +74 -0
- django_bolt/openapi/spec/callback.py +24 -0
- django_bolt/openapi/spec/components.py +72 -0
- django_bolt/openapi/spec/contact.py +21 -0
- django_bolt/openapi/spec/discriminator.py +25 -0
- django_bolt/openapi/spec/encoding.py +67 -0
- django_bolt/openapi/spec/enums.py +41 -0
- django_bolt/openapi/spec/example.py +36 -0
- django_bolt/openapi/spec/external_documentation.py +21 -0
- django_bolt/openapi/spec/header.py +132 -0
- django_bolt/openapi/spec/info.py +50 -0
- django_bolt/openapi/spec/license.py +28 -0
- django_bolt/openapi/spec/link.py +66 -0
- django_bolt/openapi/spec/media_type.py +51 -0
- django_bolt/openapi/spec/oauth_flow.py +36 -0
- django_bolt/openapi/spec/oauth_flows.py +28 -0
- django_bolt/openapi/spec/open_api.py +87 -0
- django_bolt/openapi/spec/operation.py +105 -0
- django_bolt/openapi/spec/parameter.py +147 -0
- django_bolt/openapi/spec/path_item.py +78 -0
- django_bolt/openapi/spec/paths.py +27 -0
- django_bolt/openapi/spec/reference.py +38 -0
- django_bolt/openapi/spec/request_body.py +38 -0
- django_bolt/openapi/spec/response.py +48 -0
- django_bolt/openapi/spec/responses.py +44 -0
- django_bolt/openapi/spec/schema.py +678 -0
- django_bolt/openapi/spec/security_requirement.py +28 -0
- django_bolt/openapi/spec/security_scheme.py +69 -0
- django_bolt/openapi/spec/server.py +34 -0
- django_bolt/openapi/spec/server_variable.py +32 -0
- django_bolt/openapi/spec/tag.py +32 -0
- django_bolt/openapi/spec/xml.py +44 -0
- django_bolt/pagination.py +669 -0
- django_bolt/param_functions.py +49 -0
- django_bolt/params.py +337 -0
- django_bolt/request_parsing.py +128 -0
- django_bolt/responses.py +214 -0
- django_bolt/router.py +48 -0
- django_bolt/serialization.py +193 -0
- django_bolt/status_codes.py +321 -0
- django_bolt/testing/__init__.py +10 -0
- django_bolt/testing/client.py +274 -0
- django_bolt/testing/helpers.py +93 -0
- django_bolt/tests/__init__.py +0 -0
- django_bolt/tests/admin_tests/__init__.py +1 -0
- django_bolt/tests/admin_tests/conftest.py +6 -0
- django_bolt/tests/admin_tests/test_admin_with_django.py +278 -0
- django_bolt/tests/admin_tests/urls.py +9 -0
- django_bolt/tests/cbv/__init__.py +0 -0
- django_bolt/tests/cbv/test_class_views.py +570 -0
- django_bolt/tests/cbv/test_class_views_django_orm.py +703 -0
- django_bolt/tests/cbv/test_class_views_features.py +1173 -0
- django_bolt/tests/cbv/test_class_views_with_client.py +622 -0
- django_bolt/tests/conftest.py +165 -0
- django_bolt/tests/test_action_decorator.py +399 -0
- django_bolt/tests/test_auth_secret_key.py +83 -0
- django_bolt/tests/test_decorator_syntax.py +159 -0
- django_bolt/tests/test_error_handling.py +481 -0
- django_bolt/tests/test_file_response.py +192 -0
- django_bolt/tests/test_global_cors.py +172 -0
- django_bolt/tests/test_guards_auth.py +441 -0
- django_bolt/tests/test_guards_integration.py +303 -0
- django_bolt/tests/test_health.py +283 -0
- django_bolt/tests/test_integration_validation.py +400 -0
- django_bolt/tests/test_json_validation.py +536 -0
- django_bolt/tests/test_jwt_auth.py +327 -0
- django_bolt/tests/test_jwt_token.py +458 -0
- django_bolt/tests/test_logging.py +837 -0
- django_bolt/tests/test_logging_merge.py +419 -0
- django_bolt/tests/test_middleware.py +492 -0
- django_bolt/tests/test_middleware_server.py +230 -0
- django_bolt/tests/test_model_viewset.py +323 -0
- django_bolt/tests/test_models.py +24 -0
- django_bolt/tests/test_pagination.py +1258 -0
- django_bolt/tests/test_parameter_validation.py +178 -0
- django_bolt/tests/test_syntax.py +626 -0
- django_bolt/tests/test_testing_utilities.py +163 -0
- django_bolt/tests/test_testing_utilities_simple.py +123 -0
- django_bolt/tests/test_viewset_unified.py +346 -0
- django_bolt/typing.py +273 -0
- django_bolt/views.py +1110 -0
- django_bolt-0.1.0.dist-info/METADATA +629 -0
- django_bolt-0.1.0.dist-info/RECORD +128 -0
- django_bolt-0.1.0.dist-info/WHEEL +4 -0
- django_bolt-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
# Token Revocation Examples
|
|
2
|
+
|
|
3
|
+
Token revocation is **OPTIONAL** in Django-Bolt. Only use it if you need logout functionality or token invalidation.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### Option 1: In-Memory (Development/Single-Process)
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from django_bolt import BoltAPI, JWTAuthentication, Token, IsAuthenticated
|
|
11
|
+
from django_bolt.auth.revocation import InMemoryRevocation
|
|
12
|
+
from datetime import timedelta
|
|
13
|
+
import uuid
|
|
14
|
+
|
|
15
|
+
# Create revocation store
|
|
16
|
+
revocation = InMemoryRevocation()
|
|
17
|
+
|
|
18
|
+
# Create API with revocation-enabled auth
|
|
19
|
+
api = BoltAPI()
|
|
20
|
+
|
|
21
|
+
auth = JWTAuthentication(
|
|
22
|
+
secret=settings.SECRET_KEY,
|
|
23
|
+
revocation_store=revocation, # ← OPTIONAL: Enables revocation
|
|
24
|
+
require_jti=True, # ← Auto-enabled when revocation_store is provided
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Login endpoint - create tokens with JTI
|
|
28
|
+
@api.post("/login")
|
|
29
|
+
async def login(username: str, password: str):
|
|
30
|
+
user = await authenticate(username=username, password=password)
|
|
31
|
+
|
|
32
|
+
if not user:
|
|
33
|
+
return {"error": "Invalid credentials"}, 401
|
|
34
|
+
|
|
35
|
+
# Create token with JTI (required for revocation)
|
|
36
|
+
token = Token.create(
|
|
37
|
+
sub=str(user.id),
|
|
38
|
+
expires_delta=timedelta(hours=1),
|
|
39
|
+
jti=str(uuid.uuid4()), # ← Unique token ID (required for revocation)
|
|
40
|
+
is_staff=user.is_staff,
|
|
41
|
+
is_admin=user.is_superuser,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
"access_token": token.encode(settings.SECRET_KEY),
|
|
46
|
+
"token_type": "bearer"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Logout endpoint - revoke token
|
|
50
|
+
@api.post("/logout", auth=[auth], guards=[IsAuthenticated()])
|
|
51
|
+
async def logout(request):
|
|
52
|
+
jti = request["context"]["auth_claims"]["jti"]
|
|
53
|
+
|
|
54
|
+
# Revoke the token
|
|
55
|
+
await revocation.revoke(jti, ttl=3600) # TTL = remaining token lifetime
|
|
56
|
+
|
|
57
|
+
return {"message": "Logged out successfully"}
|
|
58
|
+
|
|
59
|
+
# Protected endpoint
|
|
60
|
+
@api.get("/profile", auth=[auth], guards=[IsAuthenticated()])
|
|
61
|
+
async def get_profile(request):
|
|
62
|
+
user_id = request["context"]["user_id"]
|
|
63
|
+
return {"user_id": user_id}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### Option 2: Django Cache (Production - Redis/Memcached)
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from django_bolt import BoltAPI, JWTAuthentication
|
|
72
|
+
from django_bolt.auth.revocation import DjangoCacheRevocation
|
|
73
|
+
|
|
74
|
+
# settings.py
|
|
75
|
+
CACHES = {
|
|
76
|
+
'default': {
|
|
77
|
+
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
|
|
78
|
+
'LOCATION': 'redis://127.0.0.1:6379/1',
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# api.py
|
|
83
|
+
revocation = DjangoCacheRevocation(
|
|
84
|
+
cache_alias='default',
|
|
85
|
+
key_prefix='revoked:', # Keys will be "revoked:{jti}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
auth = JWTAuthentication(
|
|
89
|
+
secret=settings.SECRET_KEY,
|
|
90
|
+
revocation_store=revocation,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@api.post("/logout", auth=[auth])
|
|
94
|
+
async def logout(request):
|
|
95
|
+
jti = request["context"]["auth_claims"]["jti"]
|
|
96
|
+
|
|
97
|
+
# Revoke token - stored in Redis with TTL
|
|
98
|
+
await revocation.revoke(jti, ttl=86400 * 30) # 30 days
|
|
99
|
+
|
|
100
|
+
return {"message": "Token revoked"}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### Option 3: Database (No Cache Infrastructure)
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
# myapp/models.py
|
|
109
|
+
from django.db import models
|
|
110
|
+
|
|
111
|
+
class RevokedToken(models.Model):
|
|
112
|
+
jti = models.CharField(max_length=255, unique=True, db_index=True)
|
|
113
|
+
revoked_at = models.DateTimeField(auto_now_add=True)
|
|
114
|
+
expires_at = models.DateTimeField(db_index=True)
|
|
115
|
+
|
|
116
|
+
class Meta:
|
|
117
|
+
indexes = [
|
|
118
|
+
models.Index(fields=['jti']),
|
|
119
|
+
models.Index(fields=['expires_at']),
|
|
120
|
+
]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
# api.py
|
|
125
|
+
from django_bolt.auth.revocation import DjangoORMRevocation
|
|
126
|
+
|
|
127
|
+
revocation = DjangoORMRevocation(model='myapp.RevokedToken')
|
|
128
|
+
|
|
129
|
+
auth = JWTAuthentication(
|
|
130
|
+
secret=settings.SECRET_KEY,
|
|
131
|
+
revocation_store=revocation,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
@api.post("/logout", auth=[auth])
|
|
135
|
+
async def logout(request):
|
|
136
|
+
jti = request["context"]["auth_claims"]["jti"]
|
|
137
|
+
|
|
138
|
+
# Revoke token - stored in database
|
|
139
|
+
await revocation.revoke(jti, ttl=86400 * 30)
|
|
140
|
+
|
|
141
|
+
return {"message": "Token revoked"}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**⚠️ Important**: Add cleanup task for expired tokens:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
# Celery task or cron job
|
|
148
|
+
from datetime import datetime, timezone
|
|
149
|
+
from myapp.models import RevokedToken
|
|
150
|
+
|
|
151
|
+
async def cleanup_expired_tokens():
|
|
152
|
+
"""Run this periodically (e.g., daily)"""
|
|
153
|
+
await RevokedToken.objects.filter(
|
|
154
|
+
expires_at__lt=datetime.now(timezone.utc)
|
|
155
|
+
).adelete()
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### Option 4: Custom Revocation Handler
|
|
161
|
+
|
|
162
|
+
If you have custom logic or use a different storage backend:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from django_bolt import JWTAuthentication
|
|
166
|
+
import httpx
|
|
167
|
+
|
|
168
|
+
# Custom handler - checks external service
|
|
169
|
+
async def check_token_revoked(jti: str) -> bool:
|
|
170
|
+
"""Custom revocation logic"""
|
|
171
|
+
async with httpx.AsyncClient() as client:
|
|
172
|
+
response = await client.get(f"https://auth-service/check-revoked/{jti}")
|
|
173
|
+
return response.json()["revoked"]
|
|
174
|
+
|
|
175
|
+
auth = JWTAuthentication(
|
|
176
|
+
secret=settings.SECRET_KEY,
|
|
177
|
+
revoked_token_handler=check_token_revoked, # ← Custom function
|
|
178
|
+
require_jti=True,
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Without Revocation (Simpler, Faster)
|
|
185
|
+
|
|
186
|
+
If you don't need logout functionality, just don't provide a revocation handler:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
# NO revocation - tokens valid until expiry
|
|
190
|
+
auth = JWTAuthentication(
|
|
191
|
+
secret=settings.SECRET_KEY,
|
|
192
|
+
# No revocation_store or revoked_token_handler
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Users can't logout - tokens expire naturally
|
|
196
|
+
# ✅ Simpler
|
|
197
|
+
# ✅ Faster (~60k RPS vs ~50k RPS with revocation)
|
|
198
|
+
# ❌ No logout support
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Performance Comparison
|
|
204
|
+
|
|
205
|
+
| Revocation Strategy | RPS | Multi-Process | Notes |
|
|
206
|
+
|---------------------|-----|---------------|-------|
|
|
207
|
+
| **No Revocation** | ~60k | ✅ | Fastest, no logout |
|
|
208
|
+
| **InMemoryRevocation** | ~58k | ❌ | Single process only |
|
|
209
|
+
| **DjangoCacheRevocation (Redis)** | ~50k | ✅ | **Recommended for production** |
|
|
210
|
+
| **DjangoCacheRevocation (Memcached)** | ~48k | ✅ | Good alternative |
|
|
211
|
+
| **DjangoORMRevocation (PostgreSQL)** | ~5k | ✅ | Slowest, use if no cache |
|
|
212
|
+
| **DjangoORMRevocation (SQLite)** | ~2k | ⚠️ | Not for production |
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Complete Login/Logout Example
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from django_bolt import BoltAPI, JWTAuthentication, Token, IsAuthenticated, AllowAny
|
|
220
|
+
from django_bolt.auth.revocation import DjangoCacheRevocation
|
|
221
|
+
from django.contrib.auth import authenticate
|
|
222
|
+
from datetime import timedelta
|
|
223
|
+
import uuid
|
|
224
|
+
|
|
225
|
+
api = BoltAPI()
|
|
226
|
+
|
|
227
|
+
# Setup revocation
|
|
228
|
+
revocation = DjangoCacheRevocation()
|
|
229
|
+
|
|
230
|
+
auth = JWTAuthentication(
|
|
231
|
+
secret=settings.SECRET_KEY,
|
|
232
|
+
revocation_store=revocation,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Public login endpoint
|
|
236
|
+
@api.post("/auth/login", guards=[AllowAny()])
|
|
237
|
+
async def login(username: str, password: str):
|
|
238
|
+
"""Authenticate and return JWT token."""
|
|
239
|
+
user = await authenticate(username=username, password=password)
|
|
240
|
+
|
|
241
|
+
if not user:
|
|
242
|
+
return {"error": "Invalid credentials"}, 401
|
|
243
|
+
|
|
244
|
+
# Create access token (short-lived)
|
|
245
|
+
access_token = Token.create(
|
|
246
|
+
sub=str(user.id),
|
|
247
|
+
expires_delta=timedelta(minutes=15),
|
|
248
|
+
jti=str(uuid.uuid4()), # Required for revocation
|
|
249
|
+
is_staff=user.is_staff,
|
|
250
|
+
is_admin=user.is_superuser,
|
|
251
|
+
permissions=list(user.get_all_permissions()),
|
|
252
|
+
extras={
|
|
253
|
+
"username": user.username,
|
|
254
|
+
"email": user.email,
|
|
255
|
+
}
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Optional: Create refresh token (long-lived)
|
|
259
|
+
refresh_token = Token.create(
|
|
260
|
+
sub=str(user.id),
|
|
261
|
+
expires_delta=timedelta(days=30),
|
|
262
|
+
jti=str(uuid.uuid4()),
|
|
263
|
+
extras={"type": "refresh"}
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
"access_token": access_token.encode(settings.SECRET_KEY),
|
|
268
|
+
"refresh_token": refresh_token.encode(settings.SECRET_KEY),
|
|
269
|
+
"token_type": "bearer",
|
|
270
|
+
"expires_in": 900 # 15 minutes
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# Protected logout endpoint
|
|
274
|
+
@api.post("/auth/logout", auth=[auth], guards=[IsAuthenticated()])
|
|
275
|
+
async def logout(request):
|
|
276
|
+
"""Logout - revoke current token."""
|
|
277
|
+
jti = request["context"]["auth_claims"]["jti"]
|
|
278
|
+
|
|
279
|
+
# Calculate TTL from token expiry
|
|
280
|
+
exp = request["context"]["auth_claims"]["exp"]
|
|
281
|
+
import time
|
|
282
|
+
ttl = max(0, exp - int(time.time()))
|
|
283
|
+
|
|
284
|
+
# Revoke token
|
|
285
|
+
await revocation.revoke(jti, ttl=ttl)
|
|
286
|
+
|
|
287
|
+
return {"message": "Logged out successfully"}
|
|
288
|
+
|
|
289
|
+
# Refresh token endpoint
|
|
290
|
+
@api.post("/auth/refresh", guards=[AllowAny()])
|
|
291
|
+
async def refresh_access_token(refresh_token: str):
|
|
292
|
+
"""Exchange refresh token for new access token."""
|
|
293
|
+
try:
|
|
294
|
+
# Decode refresh token
|
|
295
|
+
token = Token.decode(refresh_token, secret=settings.SECRET_KEY)
|
|
296
|
+
|
|
297
|
+
# Validate it's a refresh token
|
|
298
|
+
if token.extras.get("type") != "refresh":
|
|
299
|
+
return {"error": "Invalid token type"}, 401
|
|
300
|
+
|
|
301
|
+
# Check if revoked
|
|
302
|
+
if await revocation.is_revoked(token.jti):
|
|
303
|
+
return {"error": "Token has been revoked"}, 401
|
|
304
|
+
|
|
305
|
+
# Create new access token
|
|
306
|
+
new_access = Token.create(
|
|
307
|
+
sub=token.sub,
|
|
308
|
+
expires_delta=timedelta(minutes=15),
|
|
309
|
+
jti=str(uuid.uuid4()),
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
"access_token": new_access.encode(settings.SECRET_KEY),
|
|
314
|
+
"token_type": "bearer",
|
|
315
|
+
"expires_in": 900
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
except ValueError as e:
|
|
319
|
+
return {"error": "Invalid token"}, 401
|
|
320
|
+
|
|
321
|
+
# Protected endpoint
|
|
322
|
+
@api.get("/profile", auth=[auth], guards=[IsAuthenticated()])
|
|
323
|
+
async def get_profile(request):
|
|
324
|
+
"""Get current user profile."""
|
|
325
|
+
user_id = request["context"]["user_id"]
|
|
326
|
+
is_admin = request["context"]["is_admin"]
|
|
327
|
+
permissions = request["context"].get("permissions", [])
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
"user_id": user_id,
|
|
331
|
+
"is_admin": is_admin,
|
|
332
|
+
"permissions": permissions,
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Best Practices
|
|
339
|
+
|
|
340
|
+
### 1. Always Use JTI with Revocation
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
# ✅ Good - JTI for revocation
|
|
344
|
+
token = Token.create(
|
|
345
|
+
sub="user123",
|
|
346
|
+
jti=str(uuid.uuid4()), # Unique ID
|
|
347
|
+
expires_delta=timedelta(hours=1)
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# ❌ Bad - No JTI, can't revoke
|
|
351
|
+
token = Token.create(
|
|
352
|
+
sub="user123",
|
|
353
|
+
expires_delta=timedelta(hours=1)
|
|
354
|
+
)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### 2. Set Appropriate TTL
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
# Match revocation TTL to token expiry
|
|
361
|
+
token = Token.create(sub="user123", expires_delta=timedelta(hours=1))
|
|
362
|
+
|
|
363
|
+
# Revoke with same TTL (3600 seconds = 1 hour)
|
|
364
|
+
await revocation.revoke(jti, ttl=3600)
|
|
365
|
+
|
|
366
|
+
# After 1 hour, token expires AND revocation entry is cleaned up
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### 3. Use DjangoCacheRevocation in Production
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
# ✅ Recommended for production
|
|
373
|
+
revocation = DjangoCacheRevocation() # Uses Redis/Memcached
|
|
374
|
+
|
|
375
|
+
# ⚠️ Only for development
|
|
376
|
+
revocation = InMemoryRevocation() # Single-process only
|
|
377
|
+
|
|
378
|
+
# 🐌 Slower but works without cache
|
|
379
|
+
revocation = DjangoORMRevocation(model='myapp.RevokedToken')
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## Summary
|
|
385
|
+
|
|
386
|
+
- **Revocation is OPTIONAL** - only use if you need logout
|
|
387
|
+
- **Multiple storage options** - In-memory, Cache, Database, Custom
|
|
388
|
+
- **No Redis requirement** - Works with Django cache (any backend)
|
|
389
|
+
- **Performance-aware** - Cache-based revocation still does ~50k RPS
|
|
390
|
+
- **JTI auto-required** - When revocation is enabled, JTI becomes mandatory
|
|
391
|
+
- **Flexible** - Bring your own storage with custom handler
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django-Bolt Authentication and Authorization System.
|
|
3
|
+
|
|
4
|
+
High-performance auth system where validation happens in Rust without the GIL.
|
|
5
|
+
Python classes define configuration that gets compiled to Rust types.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Authentication backends
|
|
9
|
+
from .backends import (
|
|
10
|
+
BaseAuthentication,
|
|
11
|
+
JWTAuthentication,
|
|
12
|
+
APIKeyAuthentication,
|
|
13
|
+
SessionAuthentication,
|
|
14
|
+
AuthContext,
|
|
15
|
+
get_default_authentication_classes,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Permission guards
|
|
19
|
+
from .guards import (
|
|
20
|
+
BasePermission,
|
|
21
|
+
AllowAny,
|
|
22
|
+
IsAuthenticated,
|
|
23
|
+
IsAdminUser,
|
|
24
|
+
IsStaff,
|
|
25
|
+
HasPermission,
|
|
26
|
+
HasAnyPermission,
|
|
27
|
+
HasAllPermissions,
|
|
28
|
+
get_default_permission_classes,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# JWT Token handling
|
|
32
|
+
from .token import Token
|
|
33
|
+
|
|
34
|
+
# JWT utilities for Django User integration
|
|
35
|
+
from .jwt_utils import (
|
|
36
|
+
create_jwt_for_user,
|
|
37
|
+
get_current_user,
|
|
38
|
+
extract_user_id_from_context,
|
|
39
|
+
get_auth_context,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Token revocation (optional)
|
|
43
|
+
from .revocation import (
|
|
44
|
+
RevocationStore,
|
|
45
|
+
InMemoryRevocation,
|
|
46
|
+
DjangoCacheRevocation,
|
|
47
|
+
DjangoORMRevocation,
|
|
48
|
+
create_revocation_handler,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
# Authentication
|
|
53
|
+
"BaseAuthentication",
|
|
54
|
+
"JWTAuthentication",
|
|
55
|
+
"APIKeyAuthentication",
|
|
56
|
+
"SessionAuthentication",
|
|
57
|
+
"AuthContext",
|
|
58
|
+
"get_default_authentication_classes",
|
|
59
|
+
|
|
60
|
+
# Guards/Permissions
|
|
61
|
+
"BasePermission",
|
|
62
|
+
"AllowAny",
|
|
63
|
+
"IsAuthenticated",
|
|
64
|
+
"IsAdminUser",
|
|
65
|
+
"IsStaff",
|
|
66
|
+
"HasPermission",
|
|
67
|
+
"HasAnyPermission",
|
|
68
|
+
"HasAllPermissions",
|
|
69
|
+
"get_default_permission_classes",
|
|
70
|
+
|
|
71
|
+
# JWT
|
|
72
|
+
"Token",
|
|
73
|
+
"create_jwt_for_user",
|
|
74
|
+
"get_current_user",
|
|
75
|
+
"extract_user_id_from_context",
|
|
76
|
+
"get_auth_context",
|
|
77
|
+
|
|
78
|
+
# Revocation (optional)
|
|
79
|
+
"RevocationStore",
|
|
80
|
+
"InMemoryRevocation",
|
|
81
|
+
"DjangoCacheRevocation",
|
|
82
|
+
"DjangoORMRevocation",
|
|
83
|
+
"create_revocation_handler",
|
|
84
|
+
]
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication system for Django-Bolt.
|
|
3
|
+
|
|
4
|
+
Provides DRF-inspired authentication classes that are compiled to Rust types
|
|
5
|
+
for zero-GIL performance in the hot path.
|
|
6
|
+
|
|
7
|
+
The authentication flow:
|
|
8
|
+
1. Python defines auth backends (JWT, API key, session)
|
|
9
|
+
2. Backends compile to metadata dicts via to_metadata()
|
|
10
|
+
3. Rust parses metadata at registration time
|
|
11
|
+
4. Rust validates tokens/keys without GIL on each request
|
|
12
|
+
5. AuthContext is populated and passed to Python handlers
|
|
13
|
+
|
|
14
|
+
Performance: ~60k+ RPS with JWT validation happening entirely in Rust.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
from typing import Any, Dict, List, Optional, Set
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class AuthContext:
|
|
24
|
+
"""
|
|
25
|
+
Authentication context returned by authentication backends.
|
|
26
|
+
|
|
27
|
+
This is populated in Rust and passed to Python handlers via request.context.
|
|
28
|
+
"""
|
|
29
|
+
user_id: Optional[str] = None
|
|
30
|
+
is_staff: bool = False
|
|
31
|
+
is_admin: bool = False
|
|
32
|
+
backend: str = "none"
|
|
33
|
+
claims: Optional[Dict[str, Any]] = None
|
|
34
|
+
permissions: Optional[Set[str]] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BaseAuthentication(ABC):
|
|
38
|
+
"""
|
|
39
|
+
Base class for authentication backends.
|
|
40
|
+
|
|
41
|
+
Authentication happens in Rust for performance. These classes compile
|
|
42
|
+
their configuration into metadata that Rust uses to validate tokens/keys.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def scheme_name(self) -> str:
|
|
48
|
+
"""Return the authentication scheme name (e.g., 'jwt', 'api_key')"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def to_metadata(self) -> Dict[str, Any]:
|
|
53
|
+
"""
|
|
54
|
+
Compile this authentication backend into metadata for Rust.
|
|
55
|
+
|
|
56
|
+
Returns a dict that will be parsed by Rust into typed enums.
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class JWTAuthentication(BaseAuthentication):
|
|
62
|
+
"""
|
|
63
|
+
JWT token authentication.
|
|
64
|
+
|
|
65
|
+
Validates JWT tokens using the configured secret and algorithms.
|
|
66
|
+
Tokens should be provided in the Authorization header as "Bearer <token>".
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
secret: Secret key for JWT validation. If None, uses Django's SECRET_KEY.
|
|
70
|
+
algorithms: List of allowed JWT algorithms (default: ["HS256"])
|
|
71
|
+
header: Header name to extract token from (default: "authorization")
|
|
72
|
+
audience: Optional JWT audience claim to validate
|
|
73
|
+
issuer: Optional JWT issuer claim to validate
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
secret: Optional[str] = None,
|
|
79
|
+
algorithms: Optional[List[str]] = None,
|
|
80
|
+
header: str = "authorization",
|
|
81
|
+
audience: Optional[str] = None,
|
|
82
|
+
issuer: Optional[str] = None,
|
|
83
|
+
revoked_token_handler: Optional[callable] = None,
|
|
84
|
+
revocation_store: Optional[Any] = None,
|
|
85
|
+
require_jti: bool = False,
|
|
86
|
+
):
|
|
87
|
+
self.secret = secret
|
|
88
|
+
self.algorithms = algorithms or ["HS256"]
|
|
89
|
+
self.header = header
|
|
90
|
+
self.audience = audience
|
|
91
|
+
self.issuer = issuer
|
|
92
|
+
|
|
93
|
+
# If no secret provided, try to get Django's SECRET_KEY
|
|
94
|
+
if self.secret is None:
|
|
95
|
+
try:
|
|
96
|
+
from django.conf import settings
|
|
97
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
98
|
+
|
|
99
|
+
if not hasattr(settings, 'SECRET_KEY'):
|
|
100
|
+
raise ImproperlyConfigured(
|
|
101
|
+
"JWTAuthentication requires a 'secret' parameter or Django's SECRET_KEY setting. "
|
|
102
|
+
"Neither was provided."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
self.secret = settings.SECRET_KEY
|
|
106
|
+
|
|
107
|
+
if not self.secret or self.secret == '':
|
|
108
|
+
raise ImproperlyConfigured(
|
|
109
|
+
"JWTAuthentication secret cannot be empty. "
|
|
110
|
+
"Please provide a non-empty 'secret' parameter or set Django's SECRET_KEY."
|
|
111
|
+
)
|
|
112
|
+
except ImportError:
|
|
113
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
114
|
+
raise ImproperlyConfigured(
|
|
115
|
+
"JWTAuthentication requires Django to be installed and configured, "
|
|
116
|
+
"or a 'secret' parameter must be explicitly provided."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Revocation support (OPTIONAL - only checked if provided)
|
|
120
|
+
self.revoked_token_handler = revoked_token_handler
|
|
121
|
+
self.revocation_store = revocation_store
|
|
122
|
+
|
|
123
|
+
# Auto-enable require_jti if revocation is configured
|
|
124
|
+
if (revoked_token_handler or revocation_store) and not require_jti:
|
|
125
|
+
require_jti = True
|
|
126
|
+
self.require_jti = require_jti
|
|
127
|
+
|
|
128
|
+
# If revocation_store provided, create handler from it
|
|
129
|
+
if revocation_store and not revoked_token_handler:
|
|
130
|
+
from .revocation import create_revocation_handler
|
|
131
|
+
self.revoked_token_handler = create_revocation_handler(revocation_store)
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def scheme_name(self) -> str:
|
|
135
|
+
return "jwt"
|
|
136
|
+
|
|
137
|
+
def to_metadata(self) -> Dict[str, Any]:
|
|
138
|
+
metadata = {
|
|
139
|
+
"type": "jwt",
|
|
140
|
+
"secret": self.secret,
|
|
141
|
+
"algorithms": self.algorithms,
|
|
142
|
+
"header": self.header.lower(),
|
|
143
|
+
"audience": self.audience,
|
|
144
|
+
"issuer": self.issuer,
|
|
145
|
+
"require_jti": self.require_jti,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Add revocation handler reference (will be called from Rust if present)
|
|
149
|
+
if self.revoked_token_handler:
|
|
150
|
+
metadata["has_revocation_handler"] = True
|
|
151
|
+
|
|
152
|
+
return metadata
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class APIKeyAuthentication(BaseAuthentication):
|
|
156
|
+
"""
|
|
157
|
+
API key authentication.
|
|
158
|
+
|
|
159
|
+
Validates API keys against a configured set of valid keys.
|
|
160
|
+
Keys should be provided in the configured header (default: X-API-Key).
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
api_keys: Set of valid API keys
|
|
164
|
+
header: Header name to extract API key from (default: "x-api-key")
|
|
165
|
+
key_permissions: Optional mapping of API keys to permission sets
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def __init__(
|
|
169
|
+
self,
|
|
170
|
+
api_keys: Optional[Set[str]] = None,
|
|
171
|
+
header: str = "x-api-key",
|
|
172
|
+
key_permissions: Optional[Dict[str, Set[str]]] = None,
|
|
173
|
+
):
|
|
174
|
+
self.api_keys = api_keys or set()
|
|
175
|
+
self.header = header
|
|
176
|
+
self.key_permissions = key_permissions or {}
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def scheme_name(self) -> str:
|
|
180
|
+
return "api_key"
|
|
181
|
+
|
|
182
|
+
def to_metadata(self) -> Dict[str, Any]:
|
|
183
|
+
return {
|
|
184
|
+
"type": "api_key",
|
|
185
|
+
"api_keys": list(self.api_keys),
|
|
186
|
+
"header": self.header.lower(),
|
|
187
|
+
"key_permissions": {
|
|
188
|
+
k: list(v) for k, v in self.key_permissions.items()
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class SessionAuthentication(BaseAuthentication):
|
|
194
|
+
"""
|
|
195
|
+
Django session authentication.
|
|
196
|
+
|
|
197
|
+
Uses Django's session framework to authenticate users.
|
|
198
|
+
This requires Django to be configured and session middleware enabled.
|
|
199
|
+
|
|
200
|
+
Note: This has higher overhead than JWT/API key auth as it requires
|
|
201
|
+
Python execution for every request.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
def __init__(self):
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def scheme_name(self) -> str:
|
|
209
|
+
return "session"
|
|
210
|
+
|
|
211
|
+
def to_metadata(self) -> Dict[str, Any]:
|
|
212
|
+
return {
|
|
213
|
+
"type": "session",
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def get_default_authentication_classes() -> List[BaseAuthentication]:
|
|
218
|
+
"""
|
|
219
|
+
Get default authentication classes from Django settings.
|
|
220
|
+
|
|
221
|
+
Looks for BOLT_AUTHENTICATION_CLASSES in settings. If not found,
|
|
222
|
+
returns an empty list (no authentication by default).
|
|
223
|
+
"""
|
|
224
|
+
try:
|
|
225
|
+
from django.conf import settings
|
|
226
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
227
|
+
try:
|
|
228
|
+
if hasattr(settings, 'BOLT_AUTHENTICATION_CLASSES'):
|
|
229
|
+
return settings.BOLT_AUTHENTICATION_CLASSES
|
|
230
|
+
except ImproperlyConfigured:
|
|
231
|
+
# Settings not configured, return empty list
|
|
232
|
+
pass
|
|
233
|
+
except (ImportError, AttributeError):
|
|
234
|
+
pass
|
|
235
|
+
|
|
236
|
+
return []
|