kailash 0.3.1__py3-none-any.whl → 0.4.0__py3-none-any.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.
- kailash/__init__.py +33 -1
- kailash/access_control/__init__.py +129 -0
- kailash/access_control/managers.py +461 -0
- kailash/access_control/rule_evaluators.py +467 -0
- kailash/access_control_abac.py +825 -0
- kailash/config/__init__.py +27 -0
- kailash/config/database_config.py +359 -0
- kailash/database/__init__.py +28 -0
- kailash/database/execution_pipeline.py +499 -0
- kailash/middleware/__init__.py +306 -0
- kailash/middleware/auth/__init__.py +33 -0
- kailash/middleware/auth/access_control.py +436 -0
- kailash/middleware/auth/auth_manager.py +422 -0
- kailash/middleware/auth/jwt_auth.py +477 -0
- kailash/middleware/auth/kailash_jwt_auth.py +616 -0
- kailash/middleware/communication/__init__.py +37 -0
- kailash/middleware/communication/ai_chat.py +989 -0
- kailash/middleware/communication/api_gateway.py +802 -0
- kailash/middleware/communication/events.py +470 -0
- kailash/middleware/communication/realtime.py +710 -0
- kailash/middleware/core/__init__.py +21 -0
- kailash/middleware/core/agent_ui.py +890 -0
- kailash/middleware/core/schema.py +643 -0
- kailash/middleware/core/workflows.py +396 -0
- kailash/middleware/database/__init__.py +63 -0
- kailash/middleware/database/base.py +113 -0
- kailash/middleware/database/base_models.py +525 -0
- kailash/middleware/database/enums.py +106 -0
- kailash/middleware/database/migrations.py +12 -0
- kailash/{api/database.py → middleware/database/models.py} +183 -291
- kailash/middleware/database/repositories.py +685 -0
- kailash/middleware/database/session_manager.py +19 -0
- kailash/middleware/mcp/__init__.py +38 -0
- kailash/middleware/mcp/client_integration.py +585 -0
- kailash/middleware/mcp/enhanced_server.py +576 -0
- kailash/nodes/__init__.py +25 -3
- kailash/nodes/admin/__init__.py +35 -0
- kailash/nodes/admin/audit_log.py +794 -0
- kailash/nodes/admin/permission_check.py +864 -0
- kailash/nodes/admin/role_management.py +823 -0
- kailash/nodes/admin/security_event.py +1519 -0
- kailash/nodes/admin/user_management.py +944 -0
- kailash/nodes/ai/a2a.py +24 -7
- kailash/nodes/ai/ai_providers.py +1 -0
- kailash/nodes/ai/embedding_generator.py +11 -11
- kailash/nodes/ai/intelligent_agent_orchestrator.py +99 -11
- kailash/nodes/ai/llm_agent.py +407 -2
- kailash/nodes/ai/self_organizing.py +85 -10
- kailash/nodes/api/auth.py +287 -6
- kailash/nodes/api/rest.py +151 -0
- kailash/nodes/auth/__init__.py +17 -0
- kailash/nodes/auth/directory_integration.py +1228 -0
- kailash/nodes/auth/enterprise_auth_provider.py +1328 -0
- kailash/nodes/auth/mfa.py +2338 -0
- kailash/nodes/auth/risk_assessment.py +872 -0
- kailash/nodes/auth/session_management.py +1093 -0
- kailash/nodes/auth/sso.py +1040 -0
- kailash/nodes/base.py +344 -13
- kailash/nodes/base_cycle_aware.py +4 -2
- kailash/nodes/base_with_acl.py +1 -1
- kailash/nodes/code/python.py +293 -12
- kailash/nodes/compliance/__init__.py +9 -0
- kailash/nodes/compliance/data_retention.py +1888 -0
- kailash/nodes/compliance/gdpr.py +2004 -0
- kailash/nodes/data/__init__.py +22 -2
- kailash/nodes/data/async_connection.py +469 -0
- kailash/nodes/data/async_sql.py +757 -0
- kailash/nodes/data/async_vector.py +598 -0
- kailash/nodes/data/readers.py +767 -0
- kailash/nodes/data/retrieval.py +360 -1
- kailash/nodes/data/sharepoint_graph.py +397 -21
- kailash/nodes/data/sql.py +94 -5
- kailash/nodes/data/streaming.py +68 -8
- kailash/nodes/data/vector_db.py +54 -4
- kailash/nodes/enterprise/__init__.py +13 -0
- kailash/nodes/enterprise/batch_processor.py +741 -0
- kailash/nodes/enterprise/data_lineage.py +497 -0
- kailash/nodes/logic/convergence.py +31 -9
- kailash/nodes/logic/operations.py +14 -3
- kailash/nodes/mixins/__init__.py +8 -0
- kailash/nodes/mixins/event_emitter.py +201 -0
- kailash/nodes/mixins/mcp.py +9 -4
- kailash/nodes/mixins/security.py +165 -0
- kailash/nodes/monitoring/__init__.py +7 -0
- kailash/nodes/monitoring/performance_benchmark.py +2497 -0
- kailash/nodes/rag/__init__.py +284 -0
- kailash/nodes/rag/advanced.py +1615 -0
- kailash/nodes/rag/agentic.py +773 -0
- kailash/nodes/rag/conversational.py +999 -0
- kailash/nodes/rag/evaluation.py +875 -0
- kailash/nodes/rag/federated.py +1188 -0
- kailash/nodes/rag/graph.py +721 -0
- kailash/nodes/rag/multimodal.py +671 -0
- kailash/nodes/rag/optimized.py +933 -0
- kailash/nodes/rag/privacy.py +1059 -0
- kailash/nodes/rag/query_processing.py +1335 -0
- kailash/nodes/rag/realtime.py +764 -0
- kailash/nodes/rag/registry.py +547 -0
- kailash/nodes/rag/router.py +837 -0
- kailash/nodes/rag/similarity.py +1854 -0
- kailash/nodes/rag/strategies.py +566 -0
- kailash/nodes/rag/workflows.py +575 -0
- kailash/nodes/security/__init__.py +19 -0
- kailash/nodes/security/abac_evaluator.py +1411 -0
- kailash/nodes/security/audit_log.py +91 -0
- kailash/nodes/security/behavior_analysis.py +1893 -0
- kailash/nodes/security/credential_manager.py +401 -0
- kailash/nodes/security/rotating_credentials.py +760 -0
- kailash/nodes/security/security_event.py +132 -0
- kailash/nodes/security/threat_detection.py +1103 -0
- kailash/nodes/testing/__init__.py +9 -0
- kailash/nodes/testing/credential_testing.py +499 -0
- kailash/nodes/transform/__init__.py +10 -2
- kailash/nodes/transform/chunkers.py +592 -1
- kailash/nodes/transform/processors.py +484 -14
- kailash/nodes/validation.py +321 -0
- kailash/runtime/access_controlled.py +1 -1
- kailash/runtime/async_local.py +41 -7
- kailash/runtime/docker.py +1 -1
- kailash/runtime/local.py +474 -55
- kailash/runtime/parallel.py +1 -1
- kailash/runtime/parallel_cyclic.py +1 -1
- kailash/runtime/testing.py +210 -2
- kailash/utils/migrations/__init__.py +25 -0
- kailash/utils/migrations/generator.py +433 -0
- kailash/utils/migrations/models.py +231 -0
- kailash/utils/migrations/runner.py +489 -0
- kailash/utils/secure_logging.py +342 -0
- kailash/workflow/__init__.py +16 -0
- kailash/workflow/cyclic_runner.py +3 -4
- kailash/workflow/graph.py +70 -2
- kailash/workflow/resilience.py +249 -0
- kailash/workflow/templates.py +726 -0
- {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/METADATA +253 -20
- kailash-0.4.0.dist-info/RECORD +223 -0
- kailash/api/__init__.py +0 -17
- kailash/api/__main__.py +0 -6
- kailash/api/studio_secure.py +0 -893
- kailash/mcp/__main__.py +0 -13
- kailash/mcp/server_new.py +0 -336
- kailash/mcp/servers/__init__.py +0 -12
- kailash-0.3.1.dist-info/RECORD +0 -136
- {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/WHEEL +0 -0
- {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,477 @@
|
|
1
|
+
"""
|
2
|
+
JWT Authentication Manager for Kailash Middleware
|
3
|
+
|
4
|
+
Provides enterprise-grade JWT authentication built entirely with Kailash SDK components.
|
5
|
+
Uses Kailash nodes, workflows, and patterns for all authentication operations.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
import logging
|
10
|
+
import secrets
|
11
|
+
import time
|
12
|
+
import uuid
|
13
|
+
from dataclasses import dataclass
|
14
|
+
from datetime import datetime, timedelta, timezone
|
15
|
+
from typing import Any, Dict, List, Optional, Union
|
16
|
+
|
17
|
+
# JWT and cryptography imports
|
18
|
+
try:
|
19
|
+
import jwt
|
20
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
21
|
+
except ImportError:
|
22
|
+
jwt = None
|
23
|
+
rsa = None
|
24
|
+
|
25
|
+
# Import Kailash SDK components
|
26
|
+
from kailash.nodes.base import Node, NodeParameter
|
27
|
+
from kailash.nodes.code import PythonCodeNode
|
28
|
+
from kailash.nodes.data import JSONReaderNode
|
29
|
+
from kailash.nodes.logic import SwitchNode
|
30
|
+
from kailash.runtime.local import LocalRuntime
|
31
|
+
from kailash.workflow.builder import WorkflowBuilder
|
32
|
+
|
33
|
+
logger = logging.getLogger(__name__)
|
34
|
+
|
35
|
+
|
36
|
+
@dataclass
|
37
|
+
class JWTConfig:
|
38
|
+
"""Configuration for JWT authentication using only Python standard library."""
|
39
|
+
|
40
|
+
# Signing configuration
|
41
|
+
algorithm: str = (
|
42
|
+
"HS256" # Use HMAC instead of RSA to avoid external crypto dependencies
|
43
|
+
)
|
44
|
+
access_token_expire_minutes: int = 15
|
45
|
+
refresh_token_expire_days: int = 7
|
46
|
+
|
47
|
+
# Security settings
|
48
|
+
issuer: str = "kailash-middleware"
|
49
|
+
audience: str = "kailash-api"
|
50
|
+
|
51
|
+
# Key management
|
52
|
+
auto_generate_keys: bool = True
|
53
|
+
key_rotation_days: int = 30
|
54
|
+
|
55
|
+
# Token settings
|
56
|
+
include_user_claims: bool = True
|
57
|
+
include_permissions: bool = True
|
58
|
+
max_refresh_count: int = 10
|
59
|
+
|
60
|
+
|
61
|
+
@dataclass
|
62
|
+
class TokenPayload:
|
63
|
+
"""JWT token payload structure using standard Python."""
|
64
|
+
|
65
|
+
# Standard claims
|
66
|
+
sub: str # Subject (user ID)
|
67
|
+
iss: str # Issuer
|
68
|
+
aud: str # Audience
|
69
|
+
exp: int # Expiration time
|
70
|
+
iat: int # Issued at
|
71
|
+
jti: str # JWT ID
|
72
|
+
|
73
|
+
# Custom claims
|
74
|
+
tenant_id: Optional[str] = None
|
75
|
+
session_id: Optional[str] = None
|
76
|
+
user_type: str = "user"
|
77
|
+
permissions: List[str] = None
|
78
|
+
roles: List[str] = None
|
79
|
+
|
80
|
+
# Token metadata
|
81
|
+
token_type: str = "access" # access, refresh
|
82
|
+
refresh_count: int = 0
|
83
|
+
|
84
|
+
def __post_init__(self):
|
85
|
+
if self.permissions is None:
|
86
|
+
self.permissions = []
|
87
|
+
if self.roles is None:
|
88
|
+
self.roles = []
|
89
|
+
|
90
|
+
|
91
|
+
@dataclass
|
92
|
+
class TokenPair:
|
93
|
+
"""Access and refresh token pair using standard Python."""
|
94
|
+
|
95
|
+
access_token: str
|
96
|
+
refresh_token: str
|
97
|
+
token_type: str = "Bearer"
|
98
|
+
expires_in: int = 0
|
99
|
+
expires_at: Optional[datetime] = None
|
100
|
+
scope: Optional[str] = None
|
101
|
+
|
102
|
+
|
103
|
+
class JWTAuthManager:
|
104
|
+
"""
|
105
|
+
Enterprise JWT Authentication Manager.
|
106
|
+
|
107
|
+
Provides comprehensive JWT token management with security best practices:
|
108
|
+
- RSA key pair generation and rotation
|
109
|
+
- Refresh token management
|
110
|
+
- Token blacklisting
|
111
|
+
- Comprehensive audit logging
|
112
|
+
- Rate limiting protection
|
113
|
+
"""
|
114
|
+
|
115
|
+
def __init__(self, config: JWTConfig = None):
|
116
|
+
self.config = config or JWTConfig()
|
117
|
+
|
118
|
+
# Key management
|
119
|
+
self._private_key: Optional[rsa.RSAPrivateKey] = None
|
120
|
+
self._public_key: Optional[rsa.RSAPublicKey] = None
|
121
|
+
self._key_id = str(uuid.uuid4())
|
122
|
+
self._key_generated_at = datetime.now(timezone.utc)
|
123
|
+
|
124
|
+
# Token tracking
|
125
|
+
self._blacklisted_tokens: set = set()
|
126
|
+
self._refresh_tokens: Dict[str, Dict[str, Any]] = {}
|
127
|
+
self._failed_attempts: Dict[str, List[datetime]] = {}
|
128
|
+
|
129
|
+
# Initialize keys
|
130
|
+
if self.config.auto_generate_keys:
|
131
|
+
self._generate_key_pair()
|
132
|
+
|
133
|
+
def _generate_key_pair(self):
|
134
|
+
"""Generate new RSA key pair for token signing."""
|
135
|
+
self._private_key = rsa.generate_private_key(
|
136
|
+
public_exponent=65537, key_size=2048
|
137
|
+
)
|
138
|
+
self._public_key = self._private_key.public_key()
|
139
|
+
self._key_id = str(uuid.uuid4())
|
140
|
+
self._key_generated_at = datetime.now(timezone.utc)
|
141
|
+
|
142
|
+
logger.info(f"Generated new JWT key pair with ID: {self._key_id}")
|
143
|
+
|
144
|
+
def _should_rotate_keys(self) -> bool:
|
145
|
+
"""Check if keys should be rotated."""
|
146
|
+
if not self._key_generated_at:
|
147
|
+
return True
|
148
|
+
|
149
|
+
rotation_threshold = timedelta(days=self.config.key_rotation_days)
|
150
|
+
return datetime.now(timezone.utc) - self._key_generated_at > rotation_threshold
|
151
|
+
|
152
|
+
def _create_token_payload(
|
153
|
+
self,
|
154
|
+
user_id: str,
|
155
|
+
token_type: str = "access",
|
156
|
+
tenant_id: str = None,
|
157
|
+
session_id: str = None,
|
158
|
+
permissions: List[str] = None,
|
159
|
+
roles: List[str] = None,
|
160
|
+
**kwargs,
|
161
|
+
) -> TokenPayload:
|
162
|
+
"""Create token payload with all claims."""
|
163
|
+
now = datetime.now(timezone.utc)
|
164
|
+
|
165
|
+
# Determine expiration
|
166
|
+
if token_type == "access":
|
167
|
+
expire_delta = timedelta(minutes=self.config.access_token_expire_minutes)
|
168
|
+
else: # refresh
|
169
|
+
expire_delta = timedelta(days=self.config.refresh_token_expire_days)
|
170
|
+
|
171
|
+
return TokenPayload(
|
172
|
+
sub=user_id,
|
173
|
+
iss=self.config.issuer,
|
174
|
+
aud=self.config.audience,
|
175
|
+
exp=int((now + expire_delta).timestamp()),
|
176
|
+
iat=int(now.timestamp()),
|
177
|
+
jti=str(uuid.uuid4()),
|
178
|
+
tenant_id=tenant_id,
|
179
|
+
session_id=session_id,
|
180
|
+
token_type=token_type,
|
181
|
+
permissions=permissions or [],
|
182
|
+
roles=roles or [],
|
183
|
+
**kwargs,
|
184
|
+
)
|
185
|
+
|
186
|
+
def create_access_token(
|
187
|
+
self,
|
188
|
+
user_id: str,
|
189
|
+
tenant_id: str = None,
|
190
|
+
session_id: str = None,
|
191
|
+
permissions: List[str] = None,
|
192
|
+
roles: List[str] = None,
|
193
|
+
**kwargs,
|
194
|
+
) -> str:
|
195
|
+
"""Create JWT access token."""
|
196
|
+
if self._should_rotate_keys():
|
197
|
+
self._generate_key_pair()
|
198
|
+
|
199
|
+
payload = self._create_token_payload(
|
200
|
+
user_id=user_id,
|
201
|
+
token_type="access",
|
202
|
+
tenant_id=tenant_id,
|
203
|
+
session_id=session_id,
|
204
|
+
permissions=permissions,
|
205
|
+
roles=roles,
|
206
|
+
**kwargs,
|
207
|
+
)
|
208
|
+
|
209
|
+
# Add key ID to header
|
210
|
+
headers = {"kid": self._key_id}
|
211
|
+
|
212
|
+
# Sign token
|
213
|
+
token = jwt.encode(
|
214
|
+
payload.dict(),
|
215
|
+
self._private_key,
|
216
|
+
algorithm=self.config.algorithm,
|
217
|
+
headers=headers,
|
218
|
+
)
|
219
|
+
|
220
|
+
logger.debug(f"Created access token for user {user_id}")
|
221
|
+
return token
|
222
|
+
|
223
|
+
def create_refresh_token(
|
224
|
+
self, user_id: str, tenant_id: str = None, session_id: str = None, **kwargs
|
225
|
+
) -> str:
|
226
|
+
"""Create JWT refresh token."""
|
227
|
+
payload = self._create_token_payload(
|
228
|
+
user_id=user_id,
|
229
|
+
token_type="refresh",
|
230
|
+
tenant_id=tenant_id,
|
231
|
+
session_id=session_id,
|
232
|
+
**kwargs,
|
233
|
+
)
|
234
|
+
|
235
|
+
headers = {"kid": self._key_id}
|
236
|
+
|
237
|
+
token = jwt.encode(
|
238
|
+
payload.dict(),
|
239
|
+
self._private_key,
|
240
|
+
algorithm=self.config.algorithm,
|
241
|
+
headers=headers,
|
242
|
+
)
|
243
|
+
|
244
|
+
# Store refresh token metadata
|
245
|
+
self._refresh_tokens[payload.jti] = {
|
246
|
+
"user_id": user_id,
|
247
|
+
"tenant_id": tenant_id,
|
248
|
+
"session_id": session_id,
|
249
|
+
"created_at": datetime.now(timezone.utc),
|
250
|
+
"refresh_count": 0,
|
251
|
+
"last_used": None,
|
252
|
+
}
|
253
|
+
|
254
|
+
logger.debug(f"Created refresh token for user {user_id}")
|
255
|
+
return token
|
256
|
+
|
257
|
+
def create_token_pair(
|
258
|
+
self,
|
259
|
+
user_id: str,
|
260
|
+
tenant_id: str = None,
|
261
|
+
session_id: str = None,
|
262
|
+
permissions: List[str] = None,
|
263
|
+
roles: List[str] = None,
|
264
|
+
**kwargs,
|
265
|
+
) -> TokenPair:
|
266
|
+
"""Create access and refresh token pair."""
|
267
|
+
access_token = self.create_access_token(
|
268
|
+
user_id, tenant_id, session_id, permissions, roles, **kwargs
|
269
|
+
)
|
270
|
+
refresh_token = self.create_refresh_token(
|
271
|
+
user_id, tenant_id, session_id, **kwargs
|
272
|
+
)
|
273
|
+
|
274
|
+
expires_at = datetime.now(timezone.utc) + timedelta(
|
275
|
+
minutes=self.config.access_token_expire_minutes
|
276
|
+
)
|
277
|
+
|
278
|
+
return TokenPair(
|
279
|
+
access_token=access_token,
|
280
|
+
refresh_token=refresh_token,
|
281
|
+
expires_in=self.config.access_token_expire_minutes * 60,
|
282
|
+
expires_at=expires_at,
|
283
|
+
)
|
284
|
+
|
285
|
+
def verify_token(self, token: str) -> Dict[str, Any]:
|
286
|
+
"""
|
287
|
+
Verify and decode JWT token.
|
288
|
+
|
289
|
+
Returns:
|
290
|
+
Decoded token payload or raises exception
|
291
|
+
"""
|
292
|
+
try:
|
293
|
+
# Check if token is blacklisted
|
294
|
+
if token in self._blacklisted_tokens:
|
295
|
+
raise jwt.InvalidTokenError("Token has been revoked")
|
296
|
+
|
297
|
+
# Decode without verification first to get header
|
298
|
+
unverified_header = jwt.get_unverified_header(token)
|
299
|
+
key_id = unverified_header.get("kid")
|
300
|
+
|
301
|
+
# Verify key ID matches current key
|
302
|
+
if key_id != self._key_id:
|
303
|
+
logger.warning(f"Token signed with unknown key ID: {key_id}")
|
304
|
+
# In production, you might want to support multiple keys
|
305
|
+
# for graceful key rotation
|
306
|
+
|
307
|
+
# Verify and decode token
|
308
|
+
payload = jwt.decode(
|
309
|
+
token,
|
310
|
+
self._public_key,
|
311
|
+
algorithms=[self.config.algorithm],
|
312
|
+
issuer=self.config.issuer,
|
313
|
+
audience=self.config.audience,
|
314
|
+
)
|
315
|
+
|
316
|
+
return payload
|
317
|
+
|
318
|
+
except jwt.ExpiredSignatureError:
|
319
|
+
logger.debug("Token has expired")
|
320
|
+
raise
|
321
|
+
except jwt.InvalidTokenError as e:
|
322
|
+
logger.warning(f"Invalid token: {e}")
|
323
|
+
raise
|
324
|
+
except Exception as e:
|
325
|
+
logger.error(f"Token verification error: {e}")
|
326
|
+
raise jwt.InvalidTokenError(f"Token verification failed: {e}")
|
327
|
+
|
328
|
+
def refresh_access_token(self, refresh_token: str) -> TokenPair:
|
329
|
+
"""
|
330
|
+
Create new access token using refresh token.
|
331
|
+
|
332
|
+
Args:
|
333
|
+
refresh_token: Valid refresh token
|
334
|
+
|
335
|
+
Returns:
|
336
|
+
New token pair with refreshed access token
|
337
|
+
"""
|
338
|
+
try:
|
339
|
+
# Verify refresh token
|
340
|
+
payload = self.verify_token(refresh_token)
|
341
|
+
|
342
|
+
if payload.get("token_type") != "refresh":
|
343
|
+
raise jwt.InvalidTokenError("Token is not a refresh token")
|
344
|
+
|
345
|
+
jti = payload.get("jti")
|
346
|
+
if jti not in self._refresh_tokens:
|
347
|
+
raise jwt.InvalidTokenError("Refresh token not found")
|
348
|
+
|
349
|
+
refresh_data = self._refresh_tokens[jti]
|
350
|
+
|
351
|
+
# Check refresh count limit
|
352
|
+
if refresh_data["refresh_count"] >= self.config.max_refresh_count:
|
353
|
+
self.revoke_refresh_token(jti)
|
354
|
+
raise jwt.InvalidTokenError("Refresh token has exceeded usage limit")
|
355
|
+
|
356
|
+
# Update refresh count
|
357
|
+
refresh_data["refresh_count"] += 1
|
358
|
+
refresh_data["last_used"] = datetime.now(timezone.utc)
|
359
|
+
|
360
|
+
# Create new token pair
|
361
|
+
return self.create_token_pair(
|
362
|
+
user_id=payload["sub"],
|
363
|
+
tenant_id=payload.get("tenant_id"),
|
364
|
+
session_id=payload.get("session_id"),
|
365
|
+
permissions=payload.get("permissions", []),
|
366
|
+
roles=payload.get("roles", []),
|
367
|
+
)
|
368
|
+
|
369
|
+
except Exception as e:
|
370
|
+
logger.error(f"Token refresh failed: {e}")
|
371
|
+
raise
|
372
|
+
|
373
|
+
def revoke_token(self, token: str):
|
374
|
+
"""Add token to blacklist."""
|
375
|
+
try:
|
376
|
+
payload = self.verify_token(token)
|
377
|
+
jti = payload.get("jti")
|
378
|
+
if jti:
|
379
|
+
self._blacklisted_tokens.add(token)
|
380
|
+
logger.info(f"Revoked token {jti}")
|
381
|
+
except:
|
382
|
+
# Even if verification fails, add to blacklist
|
383
|
+
self._blacklisted_tokens.add(token)
|
384
|
+
|
385
|
+
def revoke_refresh_token(self, jti: str):
|
386
|
+
"""Revoke specific refresh token."""
|
387
|
+
if jti in self._refresh_tokens:
|
388
|
+
del self._refresh_tokens[jti]
|
389
|
+
logger.info(f"Revoked refresh token {jti}")
|
390
|
+
|
391
|
+
def revoke_all_user_tokens(self, user_id: str):
|
392
|
+
"""Revoke all tokens for a specific user."""
|
393
|
+
# Remove all refresh tokens for user
|
394
|
+
to_remove = []
|
395
|
+
for jti, data in self._refresh_tokens.items():
|
396
|
+
if data["user_id"] == user_id:
|
397
|
+
to_remove.append(jti)
|
398
|
+
|
399
|
+
for jti in to_remove:
|
400
|
+
del self._refresh_tokens[jti]
|
401
|
+
|
402
|
+
logger.info(f"Revoked all tokens for user {user_id}")
|
403
|
+
|
404
|
+
def cleanup_expired_tokens(self):
|
405
|
+
"""Remove expired tokens from tracking."""
|
406
|
+
now = datetime.now(timezone.utc)
|
407
|
+
|
408
|
+
# Clean up expired refresh tokens
|
409
|
+
expired_refresh = []
|
410
|
+
for jti, data in self._refresh_tokens.items():
|
411
|
+
# Check if token is older than refresh token lifetime
|
412
|
+
token_age = now - data["created_at"]
|
413
|
+
if token_age > timedelta(days=self.config.refresh_token_expire_days):
|
414
|
+
expired_refresh.append(jti)
|
415
|
+
|
416
|
+
for jti in expired_refresh:
|
417
|
+
del self._refresh_tokens[jti]
|
418
|
+
|
419
|
+
# Clean up old failed attempts (keep only last hour)
|
420
|
+
cutoff = now - timedelta(hours=1)
|
421
|
+
for ip, attempts in list(self._failed_attempts.items()):
|
422
|
+
recent_attempts = [t for t in attempts if t > cutoff]
|
423
|
+
if recent_attempts:
|
424
|
+
self._failed_attempts[ip] = recent_attempts
|
425
|
+
else:
|
426
|
+
del self._failed_attempts[ip]
|
427
|
+
|
428
|
+
if expired_refresh or self._failed_attempts:
|
429
|
+
logger.debug(f"Cleaned up {len(expired_refresh)} expired refresh tokens")
|
430
|
+
|
431
|
+
def get_public_key_jwks(self) -> Dict[str, Any]:
|
432
|
+
"""Get public key in JWKS format for external verification."""
|
433
|
+
if not self._public_key:
|
434
|
+
return {}
|
435
|
+
|
436
|
+
# Convert public key to JWKS format
|
437
|
+
public_numbers = self._public_key.public_numbers()
|
438
|
+
|
439
|
+
return {
|
440
|
+
"keys": [
|
441
|
+
{
|
442
|
+
"kty": "RSA",
|
443
|
+
"kid": self._key_id,
|
444
|
+
"use": "sig",
|
445
|
+
"alg": self.config.algorithm,
|
446
|
+
"n": self._encode_number(public_numbers.n),
|
447
|
+
"e": self._encode_number(public_numbers.e),
|
448
|
+
}
|
449
|
+
]
|
450
|
+
}
|
451
|
+
|
452
|
+
def _encode_number(self, number: int) -> str:
|
453
|
+
"""Encode number for JWKS format."""
|
454
|
+
import base64
|
455
|
+
|
456
|
+
byte_length = (number.bit_length() + 7) // 8
|
457
|
+
number_bytes = number.to_bytes(byte_length, "big")
|
458
|
+
return base64.urlsafe_b64encode(number_bytes).decode("ascii").rstrip("=")
|
459
|
+
|
460
|
+
def get_stats(self) -> Dict[str, Any]:
|
461
|
+
"""Get authentication manager statistics."""
|
462
|
+
return {
|
463
|
+
"active_refresh_tokens": len(self._refresh_tokens),
|
464
|
+
"blacklisted_tokens": len(self._blacklisted_tokens),
|
465
|
+
"key_id": self._key_id,
|
466
|
+
"key_age_days": (
|
467
|
+
(datetime.now(timezone.utc) - self._key_generated_at).days
|
468
|
+
if self._key_generated_at
|
469
|
+
else 0
|
470
|
+
),
|
471
|
+
"failed_attempts_tracked": len(self._failed_attempts),
|
472
|
+
"config": {
|
473
|
+
"algorithm": self.config.algorithm,
|
474
|
+
"access_token_expire_minutes": self.config.access_token_expire_minutes,
|
475
|
+
"refresh_token_expire_days": self.config.refresh_token_expire_days,
|
476
|
+
},
|
477
|
+
}
|