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.
Files changed (146) hide show
  1. kailash/__init__.py +33 -1
  2. kailash/access_control/__init__.py +129 -0
  3. kailash/access_control/managers.py +461 -0
  4. kailash/access_control/rule_evaluators.py +467 -0
  5. kailash/access_control_abac.py +825 -0
  6. kailash/config/__init__.py +27 -0
  7. kailash/config/database_config.py +359 -0
  8. kailash/database/__init__.py +28 -0
  9. kailash/database/execution_pipeline.py +499 -0
  10. kailash/middleware/__init__.py +306 -0
  11. kailash/middleware/auth/__init__.py +33 -0
  12. kailash/middleware/auth/access_control.py +436 -0
  13. kailash/middleware/auth/auth_manager.py +422 -0
  14. kailash/middleware/auth/jwt_auth.py +477 -0
  15. kailash/middleware/auth/kailash_jwt_auth.py +616 -0
  16. kailash/middleware/communication/__init__.py +37 -0
  17. kailash/middleware/communication/ai_chat.py +989 -0
  18. kailash/middleware/communication/api_gateway.py +802 -0
  19. kailash/middleware/communication/events.py +470 -0
  20. kailash/middleware/communication/realtime.py +710 -0
  21. kailash/middleware/core/__init__.py +21 -0
  22. kailash/middleware/core/agent_ui.py +890 -0
  23. kailash/middleware/core/schema.py +643 -0
  24. kailash/middleware/core/workflows.py +396 -0
  25. kailash/middleware/database/__init__.py +63 -0
  26. kailash/middleware/database/base.py +113 -0
  27. kailash/middleware/database/base_models.py +525 -0
  28. kailash/middleware/database/enums.py +106 -0
  29. kailash/middleware/database/migrations.py +12 -0
  30. kailash/{api/database.py → middleware/database/models.py} +183 -291
  31. kailash/middleware/database/repositories.py +685 -0
  32. kailash/middleware/database/session_manager.py +19 -0
  33. kailash/middleware/mcp/__init__.py +38 -0
  34. kailash/middleware/mcp/client_integration.py +585 -0
  35. kailash/middleware/mcp/enhanced_server.py +576 -0
  36. kailash/nodes/__init__.py +25 -3
  37. kailash/nodes/admin/__init__.py +35 -0
  38. kailash/nodes/admin/audit_log.py +794 -0
  39. kailash/nodes/admin/permission_check.py +864 -0
  40. kailash/nodes/admin/role_management.py +823 -0
  41. kailash/nodes/admin/security_event.py +1519 -0
  42. kailash/nodes/admin/user_management.py +944 -0
  43. kailash/nodes/ai/a2a.py +24 -7
  44. kailash/nodes/ai/ai_providers.py +1 -0
  45. kailash/nodes/ai/embedding_generator.py +11 -11
  46. kailash/nodes/ai/intelligent_agent_orchestrator.py +99 -11
  47. kailash/nodes/ai/llm_agent.py +407 -2
  48. kailash/nodes/ai/self_organizing.py +85 -10
  49. kailash/nodes/api/auth.py +287 -6
  50. kailash/nodes/api/rest.py +151 -0
  51. kailash/nodes/auth/__init__.py +17 -0
  52. kailash/nodes/auth/directory_integration.py +1228 -0
  53. kailash/nodes/auth/enterprise_auth_provider.py +1328 -0
  54. kailash/nodes/auth/mfa.py +2338 -0
  55. kailash/nodes/auth/risk_assessment.py +872 -0
  56. kailash/nodes/auth/session_management.py +1093 -0
  57. kailash/nodes/auth/sso.py +1040 -0
  58. kailash/nodes/base.py +344 -13
  59. kailash/nodes/base_cycle_aware.py +4 -2
  60. kailash/nodes/base_with_acl.py +1 -1
  61. kailash/nodes/code/python.py +293 -12
  62. kailash/nodes/compliance/__init__.py +9 -0
  63. kailash/nodes/compliance/data_retention.py +1888 -0
  64. kailash/nodes/compliance/gdpr.py +2004 -0
  65. kailash/nodes/data/__init__.py +22 -2
  66. kailash/nodes/data/async_connection.py +469 -0
  67. kailash/nodes/data/async_sql.py +757 -0
  68. kailash/nodes/data/async_vector.py +598 -0
  69. kailash/nodes/data/readers.py +767 -0
  70. kailash/nodes/data/retrieval.py +360 -1
  71. kailash/nodes/data/sharepoint_graph.py +397 -21
  72. kailash/nodes/data/sql.py +94 -5
  73. kailash/nodes/data/streaming.py +68 -8
  74. kailash/nodes/data/vector_db.py +54 -4
  75. kailash/nodes/enterprise/__init__.py +13 -0
  76. kailash/nodes/enterprise/batch_processor.py +741 -0
  77. kailash/nodes/enterprise/data_lineage.py +497 -0
  78. kailash/nodes/logic/convergence.py +31 -9
  79. kailash/nodes/logic/operations.py +14 -3
  80. kailash/nodes/mixins/__init__.py +8 -0
  81. kailash/nodes/mixins/event_emitter.py +201 -0
  82. kailash/nodes/mixins/mcp.py +9 -4
  83. kailash/nodes/mixins/security.py +165 -0
  84. kailash/nodes/monitoring/__init__.py +7 -0
  85. kailash/nodes/monitoring/performance_benchmark.py +2497 -0
  86. kailash/nodes/rag/__init__.py +284 -0
  87. kailash/nodes/rag/advanced.py +1615 -0
  88. kailash/nodes/rag/agentic.py +773 -0
  89. kailash/nodes/rag/conversational.py +999 -0
  90. kailash/nodes/rag/evaluation.py +875 -0
  91. kailash/nodes/rag/federated.py +1188 -0
  92. kailash/nodes/rag/graph.py +721 -0
  93. kailash/nodes/rag/multimodal.py +671 -0
  94. kailash/nodes/rag/optimized.py +933 -0
  95. kailash/nodes/rag/privacy.py +1059 -0
  96. kailash/nodes/rag/query_processing.py +1335 -0
  97. kailash/nodes/rag/realtime.py +764 -0
  98. kailash/nodes/rag/registry.py +547 -0
  99. kailash/nodes/rag/router.py +837 -0
  100. kailash/nodes/rag/similarity.py +1854 -0
  101. kailash/nodes/rag/strategies.py +566 -0
  102. kailash/nodes/rag/workflows.py +575 -0
  103. kailash/nodes/security/__init__.py +19 -0
  104. kailash/nodes/security/abac_evaluator.py +1411 -0
  105. kailash/nodes/security/audit_log.py +91 -0
  106. kailash/nodes/security/behavior_analysis.py +1893 -0
  107. kailash/nodes/security/credential_manager.py +401 -0
  108. kailash/nodes/security/rotating_credentials.py +760 -0
  109. kailash/nodes/security/security_event.py +132 -0
  110. kailash/nodes/security/threat_detection.py +1103 -0
  111. kailash/nodes/testing/__init__.py +9 -0
  112. kailash/nodes/testing/credential_testing.py +499 -0
  113. kailash/nodes/transform/__init__.py +10 -2
  114. kailash/nodes/transform/chunkers.py +592 -1
  115. kailash/nodes/transform/processors.py +484 -14
  116. kailash/nodes/validation.py +321 -0
  117. kailash/runtime/access_controlled.py +1 -1
  118. kailash/runtime/async_local.py +41 -7
  119. kailash/runtime/docker.py +1 -1
  120. kailash/runtime/local.py +474 -55
  121. kailash/runtime/parallel.py +1 -1
  122. kailash/runtime/parallel_cyclic.py +1 -1
  123. kailash/runtime/testing.py +210 -2
  124. kailash/utils/migrations/__init__.py +25 -0
  125. kailash/utils/migrations/generator.py +433 -0
  126. kailash/utils/migrations/models.py +231 -0
  127. kailash/utils/migrations/runner.py +489 -0
  128. kailash/utils/secure_logging.py +342 -0
  129. kailash/workflow/__init__.py +16 -0
  130. kailash/workflow/cyclic_runner.py +3 -4
  131. kailash/workflow/graph.py +70 -2
  132. kailash/workflow/resilience.py +249 -0
  133. kailash/workflow/templates.py +726 -0
  134. {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/METADATA +253 -20
  135. kailash-0.4.0.dist-info/RECORD +223 -0
  136. kailash/api/__init__.py +0 -17
  137. kailash/api/__main__.py +0 -6
  138. kailash/api/studio_secure.py +0 -893
  139. kailash/mcp/__main__.py +0 -13
  140. kailash/mcp/server_new.py +0 -336
  141. kailash/mcp/servers/__init__.py +0 -12
  142. kailash-0.3.1.dist-info/RECORD +0 -136
  143. {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/WHEEL +0 -0
  144. {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/entry_points.txt +0 -0
  145. {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/licenses/LICENSE +0 -0
  146. {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
+ }