miso-client 0.1.0__py3-none-any.whl → 3.7.2__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 (69) hide show
  1. miso_client/__init__.py +523 -130
  2. miso_client/api/__init__.py +35 -0
  3. miso_client/api/auth_api.py +367 -0
  4. miso_client/api/logs_api.py +91 -0
  5. miso_client/api/permissions_api.py +88 -0
  6. miso_client/api/roles_api.py +88 -0
  7. miso_client/api/types/__init__.py +75 -0
  8. miso_client/api/types/auth_types.py +183 -0
  9. miso_client/api/types/logs_types.py +71 -0
  10. miso_client/api/types/permissions_types.py +31 -0
  11. miso_client/api/types/roles_types.py +31 -0
  12. miso_client/errors.py +30 -4
  13. miso_client/models/__init__.py +4 -0
  14. miso_client/models/config.py +275 -72
  15. miso_client/models/error_response.py +39 -0
  16. miso_client/models/filter.py +255 -0
  17. miso_client/models/pagination.py +44 -0
  18. miso_client/models/sort.py +25 -0
  19. miso_client/services/__init__.py +6 -5
  20. miso_client/services/auth.py +496 -87
  21. miso_client/services/cache.py +42 -41
  22. miso_client/services/encryption.py +18 -17
  23. miso_client/services/logger.py +467 -328
  24. miso_client/services/logger_chain.py +288 -0
  25. miso_client/services/permission.py +130 -67
  26. miso_client/services/redis.py +28 -23
  27. miso_client/services/role.py +145 -62
  28. miso_client/utils/__init__.py +3 -3
  29. miso_client/utils/audit_log_queue.py +222 -0
  30. miso_client/utils/auth_strategy.py +88 -0
  31. miso_client/utils/auth_utils.py +65 -0
  32. miso_client/utils/circuit_breaker.py +125 -0
  33. miso_client/utils/client_token_manager.py +244 -0
  34. miso_client/utils/config_loader.py +88 -17
  35. miso_client/utils/controller_url_resolver.py +80 -0
  36. miso_client/utils/data_masker.py +104 -33
  37. miso_client/utils/environment_token.py +126 -0
  38. miso_client/utils/error_utils.py +216 -0
  39. miso_client/utils/fastapi_endpoints.py +166 -0
  40. miso_client/utils/filter.py +364 -0
  41. miso_client/utils/filter_applier.py +143 -0
  42. miso_client/utils/filter_parser.py +110 -0
  43. miso_client/utils/flask_endpoints.py +169 -0
  44. miso_client/utils/http_client.py +494 -262
  45. miso_client/utils/http_client_logging.py +352 -0
  46. miso_client/utils/http_client_logging_helpers.py +197 -0
  47. miso_client/utils/http_client_query_helpers.py +138 -0
  48. miso_client/utils/http_error_handler.py +92 -0
  49. miso_client/utils/http_log_formatter.py +115 -0
  50. miso_client/utils/http_log_masker.py +203 -0
  51. miso_client/utils/internal_http_client.py +435 -0
  52. miso_client/utils/jwt_tools.py +125 -16
  53. miso_client/utils/logger_helpers.py +206 -0
  54. miso_client/utils/logging_helpers.py +70 -0
  55. miso_client/utils/origin_validator.py +128 -0
  56. miso_client/utils/pagination.py +275 -0
  57. miso_client/utils/request_context.py +285 -0
  58. miso_client/utils/sensitive_fields_loader.py +116 -0
  59. miso_client/utils/sort.py +116 -0
  60. miso_client/utils/token_utils.py +114 -0
  61. miso_client/utils/url_validator.py +66 -0
  62. miso_client/utils/user_token_refresh.py +245 -0
  63. miso_client-3.7.2.dist-info/METADATA +1021 -0
  64. miso_client-3.7.2.dist-info/RECORD +68 -0
  65. miso_client-0.1.0.dist-info/METADATA +0 -551
  66. miso_client-0.1.0.dist-info/RECORD +0 -23
  67. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/WHEEL +0 -0
  68. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/licenses/LICENSE +0 -0
  69. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,435 @@
1
+ """
2
+ Internal HTTP client utility for controller communication.
3
+
4
+ This module provides the internal HTTP client implementation with automatic client
5
+ token management. This class is not meant to be used directly - use the public
6
+ HttpClient class instead which adds ISO 27001 compliant audit and debug logging.
7
+ """
8
+
9
+ from typing import Any, Dict, Literal, Optional
10
+
11
+ import httpx
12
+
13
+ from ..errors import AuthenticationError, ConnectionError, MisoClientError
14
+ from ..models.config import AuthStrategy, MisoClientConfig
15
+ from .auth_strategy import AuthStrategyHandler
16
+ from .client_token_manager import ClientTokenManager
17
+ from .controller_url_resolver import resolve_controller_url
18
+ from .http_error_handler import parse_error_response
19
+
20
+
21
+ class InternalHttpClient:
22
+ """
23
+ Internal HTTP client for Miso Controller communication with automatic client token management.
24
+
25
+ This class contains the core HTTP functionality without logging.
26
+ It is wrapped by the public HttpClient class which adds audit and debug logging.
27
+ """
28
+
29
+ def __init__(self, config: MisoClientConfig):
30
+ """
31
+ Initialize internal HTTP client with configuration.
32
+
33
+ Args:
34
+ config: MisoClient configuration
35
+ """
36
+ self.config = config
37
+ self.client: Optional[httpx.AsyncClient] = None
38
+ self.token_manager = ClientTokenManager(config)
39
+
40
+ async def _initialize_client(self):
41
+ """Initialize HTTP client if not already initialized."""
42
+ if self.client is None:
43
+ # Use resolved URL (controllerPrivateUrl or controller_url)
44
+ resolved_url = resolve_controller_url(self.config)
45
+ self.client = httpx.AsyncClient(
46
+ base_url=resolved_url,
47
+ timeout=30.0,
48
+ headers={
49
+ "Content-Type": "application/json",
50
+ },
51
+ )
52
+
53
+ async def _ensure_client_token(self):
54
+ """Ensure client token is set in headers."""
55
+ await self._initialize_client()
56
+ token = await self.token_manager.get_client_token()
57
+ if self.client:
58
+ self.client.headers["x-client-token"] = token
59
+
60
+ async def close(self):
61
+ """Close the HTTP client."""
62
+ if self.client:
63
+ await self.client.aclose()
64
+ self.client = None
65
+
66
+ async def __aenter__(self):
67
+ """Async context manager entry."""
68
+ return self
69
+
70
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
71
+ """Async context manager exit."""
72
+ await self.close()
73
+
74
+ async def get(self, url: str, **kwargs) -> Any:
75
+ """
76
+ Make GET request.
77
+
78
+ Args:
79
+ url: Request URL
80
+ **kwargs: Additional httpx request parameters
81
+
82
+ Returns:
83
+ Response data (JSON parsed)
84
+
85
+ Raises:
86
+ MisoClientError: If request fails
87
+ """
88
+ await self._initialize_client()
89
+ await self._ensure_client_token()
90
+ try:
91
+ assert self.client is not None
92
+ response = await self.client.get(url, **kwargs)
93
+
94
+ # Handle 401 - clear token to force refresh
95
+ if response.status_code == 401:
96
+ self.token_manager.clear_token()
97
+
98
+ response.raise_for_status()
99
+ return response.json()
100
+ except httpx.HTTPStatusError as e:
101
+ # Try to parse structured error response
102
+ error_response = parse_error_response(e.response, url)
103
+ error_body = {}
104
+ if (
105
+ e.response.headers.get("content-type", "").startswith("application/json")
106
+ and not error_response
107
+ ):
108
+ try:
109
+ error_body = e.response.json()
110
+ except (ValueError, TypeError):
111
+ pass
112
+
113
+ raise MisoClientError(
114
+ f"HTTP {e.response.status_code}: {e.response.text}",
115
+ status_code=e.response.status_code,
116
+ error_body=error_body,
117
+ error_response=error_response,
118
+ )
119
+ except httpx.RequestError as e:
120
+ raise ConnectionError(f"Request failed: {str(e)}")
121
+
122
+ async def post(self, url: str, data: Optional[Dict[str, Any]] = None, **kwargs) -> Any:
123
+ """
124
+ Make POST request.
125
+
126
+ Args:
127
+ url: Request URL
128
+ data: Request data (will be JSON encoded)
129
+ **kwargs: Additional httpx request parameters
130
+
131
+ Returns:
132
+ Response data (JSON parsed)
133
+
134
+ Raises:
135
+ MisoClientError: If request fails
136
+ """
137
+ await self._initialize_client()
138
+ await self._ensure_client_token()
139
+ try:
140
+ assert self.client is not None
141
+ response = await self.client.post(url, json=data, **kwargs)
142
+
143
+ if response.status_code == 401:
144
+ self.token_manager.clear_token()
145
+
146
+ response.raise_for_status()
147
+ return response.json()
148
+ except httpx.HTTPStatusError as e:
149
+ # Try to parse structured error response
150
+ error_response = parse_error_response(e.response, url)
151
+ error_body = {}
152
+ if (
153
+ e.response.headers.get("content-type", "").startswith("application/json")
154
+ and not error_response
155
+ ):
156
+ try:
157
+ error_body = e.response.json()
158
+ except (ValueError, TypeError):
159
+ pass
160
+
161
+ raise MisoClientError(
162
+ f"HTTP {e.response.status_code}: {e.response.text}",
163
+ status_code=e.response.status_code,
164
+ error_body=error_body,
165
+ error_response=error_response,
166
+ )
167
+ except httpx.RequestError as e:
168
+ raise ConnectionError(f"Request failed: {str(e)}")
169
+
170
+ async def put(self, url: str, data: Optional[Dict[str, Any]] = None, **kwargs) -> Any:
171
+ """
172
+ Make PUT request.
173
+
174
+ Args:
175
+ url: Request URL
176
+ data: Request data (will be JSON encoded)
177
+ **kwargs: Additional httpx request parameters
178
+
179
+ Returns:
180
+ Response data (JSON parsed)
181
+
182
+ Raises:
183
+ MisoClientError: If request fails
184
+ """
185
+ await self._initialize_client()
186
+ await self._ensure_client_token()
187
+ try:
188
+ assert self.client is not None
189
+ response = await self.client.put(url, json=data, **kwargs)
190
+
191
+ if response.status_code == 401:
192
+ self.token_manager.clear_token()
193
+
194
+ response.raise_for_status()
195
+ return response.json()
196
+ except httpx.HTTPStatusError as e:
197
+ # Try to parse structured error response
198
+ error_response = parse_error_response(e.response, url)
199
+ error_body = {}
200
+ if (
201
+ e.response.headers.get("content-type", "").startswith("application/json")
202
+ and not error_response
203
+ ):
204
+ try:
205
+ error_body = e.response.json()
206
+ except (ValueError, TypeError):
207
+ pass
208
+
209
+ raise MisoClientError(
210
+ f"HTTP {e.response.status_code}: {e.response.text}",
211
+ status_code=e.response.status_code,
212
+ error_body=error_body,
213
+ error_response=error_response,
214
+ )
215
+ except httpx.RequestError as e:
216
+ raise ConnectionError(f"Request failed: {str(e)}")
217
+
218
+ async def delete(self, url: str, **kwargs) -> Any:
219
+ """
220
+ Make DELETE request.
221
+
222
+ Args:
223
+ url: Request URL
224
+ **kwargs: Additional httpx request parameters
225
+
226
+ Returns:
227
+ Response data (JSON parsed)
228
+
229
+ Raises:
230
+ MisoClientError: If request fails
231
+ """
232
+ await self._initialize_client()
233
+ await self._ensure_client_token()
234
+ try:
235
+ assert self.client is not None
236
+ response = await self.client.delete(url, **kwargs)
237
+
238
+ if response.status_code == 401:
239
+ self.token_manager.clear_token()
240
+
241
+ response.raise_for_status()
242
+ return response.json()
243
+ except httpx.HTTPStatusError as e:
244
+ # Try to parse structured error response
245
+ error_response = parse_error_response(e.response, url)
246
+ error_body = {}
247
+ if (
248
+ e.response.headers.get("content-type", "").startswith("application/json")
249
+ and not error_response
250
+ ):
251
+ try:
252
+ error_body = e.response.json()
253
+ except (ValueError, TypeError):
254
+ pass
255
+
256
+ raise MisoClientError(
257
+ f"HTTP {e.response.status_code}: {e.response.text}",
258
+ status_code=e.response.status_code,
259
+ error_body=error_body,
260
+ error_response=error_response,
261
+ )
262
+ except httpx.RequestError as e:
263
+ raise ConnectionError(f"Request failed: {str(e)}")
264
+
265
+ async def request(
266
+ self,
267
+ method: Literal["GET", "POST", "PUT", "DELETE"],
268
+ url: str,
269
+ data: Optional[Dict[str, Any]] = None,
270
+ **kwargs,
271
+ ) -> Any:
272
+ """
273
+ Generic request method.
274
+
275
+ Args:
276
+ method: HTTP method
277
+ url: Request URL
278
+ data: Request data (for POST/PUT)
279
+ **kwargs: Additional httpx request parameters
280
+
281
+ Returns:
282
+ Response data (JSON parsed)
283
+
284
+ Raises:
285
+ MisoClientError: If request fails
286
+ """
287
+ method_upper = method.upper()
288
+ if method_upper == "GET":
289
+ return await self.get(url, **kwargs)
290
+ elif method_upper == "POST":
291
+ return await self.post(url, data, **kwargs)
292
+ elif method_upper == "PUT":
293
+ return await self.put(url, data, **kwargs)
294
+ elif method_upper == "DELETE":
295
+ return await self.delete(url, **kwargs)
296
+ else:
297
+ raise ValueError(f"Unsupported HTTP method: {method}")
298
+
299
+ async def authenticated_request(
300
+ self,
301
+ method: Literal["GET", "POST", "PUT", "DELETE"],
302
+ url: str,
303
+ token: str,
304
+ data: Optional[Dict[str, Any]] = None,
305
+ auth_strategy: Optional[AuthStrategy] = None,
306
+ **kwargs,
307
+ ) -> Any:
308
+ """
309
+ Make authenticated request with Bearer token.
310
+
311
+ IMPORTANT: Client token is sent as x-client-token header (via _ensure_client_token)
312
+ User token is sent as Authorization: Bearer header (this method parameter)
313
+ These are two separate tokens for different purposes.
314
+
315
+ Args:
316
+ method: HTTP method
317
+ url: Request URL
318
+ token: User authentication token (sent as Bearer token)
319
+ data: Request data (for POST/PUT)
320
+ auth_strategy: Optional authentication strategy (defaults to bearer + client-token)
321
+ **kwargs: Additional httpx request parameters
322
+
323
+ Returns:
324
+ Response data (JSON parsed)
325
+
326
+ Raises:
327
+ MisoClientError: If request fails
328
+ """
329
+ # If no strategy provided, use default (backward compatibility)
330
+ if auth_strategy is None:
331
+ auth_strategy = AuthStrategyHandler.get_default_strategy()
332
+ # Set bearer token from parameter
333
+ auth_strategy.bearerToken = token
334
+
335
+ # Use request_with_auth_strategy for consistency
336
+ return await self.request_with_auth_strategy(method, url, auth_strategy, data, **kwargs)
337
+
338
+ async def request_with_auth_strategy(
339
+ self,
340
+ method: Literal["GET", "POST", "PUT", "DELETE"],
341
+ url: str,
342
+ auth_strategy: AuthStrategy,
343
+ data: Optional[Dict[str, Any]] = None,
344
+ **kwargs,
345
+ ) -> Any:
346
+ """
347
+ Make request with authentication strategy (priority-based fallback).
348
+
349
+ Tries authentication methods in priority order until one succeeds.
350
+ If a method returns 401, automatically tries the next method in the strategy.
351
+
352
+ Args:
353
+ method: HTTP method
354
+ url: Request URL
355
+ auth_strategy: Authentication strategy configuration
356
+ data: Request data (for POST/PUT)
357
+ **kwargs: Additional httpx request parameters
358
+
359
+ Returns:
360
+ Response data (JSON parsed)
361
+
362
+ Raises:
363
+ MisoClientError: If all authentication methods fail
364
+ """
365
+ await self._initialize_client()
366
+
367
+ # Get client token once (used by client-token and client-credentials methods)
368
+ # Client token is always sent (identifies the application)
369
+ client_token: Optional[str] = None
370
+ if "client-token" in auth_strategy.methods or "client-credentials" in auth_strategy.methods:
371
+ client_token = await self.token_manager.get_client_token()
372
+
373
+ # Try each method in priority order
374
+ last_error: Optional[Exception] = None
375
+ for auth_method in auth_strategy.methods:
376
+ try:
377
+ # Build headers for this auth method
378
+ auth_headers = AuthStrategyHandler.build_auth_headers(
379
+ auth_method, auth_strategy, client_token
380
+ )
381
+
382
+ # Merge with existing headers
383
+ request_headers = kwargs.get("headers", {}).copy()
384
+ request_headers.update(auth_headers)
385
+ request_kwargs = {**kwargs, "headers": request_headers}
386
+
387
+ # Make the request using existing request method
388
+ # Note: request() will call _ensure_client_token() which always sends client token
389
+ try:
390
+ return await self.request(method, url, data, **request_kwargs)
391
+ except httpx.HTTPStatusError as e:
392
+ # If 401, try next method
393
+ if e.response.status_code == 401:
394
+ # Clear client token to force refresh on next attempt
395
+ if auth_method in ["client-token", "client-credentials"]:
396
+ self.token_manager.clear_token()
397
+ last_error = e
398
+ continue
399
+ # For other HTTP errors, re-raise (don't try next method)
400
+ raise
401
+ except httpx.RequestError as e:
402
+ # Connection errors - don't retry with different auth
403
+ raise ConnectionError(f"Request failed: {str(e)}")
404
+
405
+ except ValueError as e:
406
+ # Missing credentials for this method - try next
407
+ last_error = e
408
+ continue
409
+ except (ConnectionError, MisoClientError):
410
+ # Don't retry connection errors or non-401 client errors
411
+ raise
412
+
413
+ # All methods failed
414
+ if last_error:
415
+ status_code = getattr(last_error, "status_code", 401)
416
+ error_response = None
417
+ if hasattr(last_error, "error_response"):
418
+ error_response = last_error.error_response
419
+ raise MisoClientError(
420
+ f"All authentication methods failed. Last error: {str(last_error)}",
421
+ status_code=status_code,
422
+ error_response=error_response,
423
+ )
424
+ raise AuthenticationError("No authentication methods available")
425
+
426
+ async def get_environment_token(self) -> str:
427
+ """
428
+ Get environment token using client credentials.
429
+
430
+ This is called automatically by HttpClient but can be called manually.
431
+
432
+ Returns:
433
+ Client token string
434
+ """
435
+ return await self.token_manager.get_client_token()
@@ -3,23 +3,26 @@ JWT token utilities for safe decoding without verification.
3
3
 
4
4
  This module provides utilities for extracting information from JWT tokens
5
5
  without verification, used for cache optimization and context extraction.
6
+ Includes JWT token caching for performance optimization.
6
7
  """
7
8
 
9
+ from datetime import datetime, timedelta
10
+ from typing import Any, Dict, Optional, Tuple, cast
11
+
8
12
  import jwt
9
- from typing import Optional, Dict, Any, cast
10
13
 
11
14
 
12
15
  def decode_token(token: str) -> Optional[Dict[str, Any]]:
13
16
  """
14
17
  Safely decode JWT token without verification.
15
-
18
+
16
19
  This is used for extracting user information (like userId) from tokens
17
20
  for cache optimization. The token is NOT verified - it should only be
18
21
  used for cache key generation, not for authentication decisions.
19
-
22
+
20
23
  Args:
21
24
  token: JWT token string
22
-
25
+
23
26
  Returns:
24
27
  Decoded token payload as dictionary, or None if decoding fails
25
28
  """
@@ -35,44 +38,150 @@ def decode_token(token: str) -> Optional[Dict[str, Any]]:
35
38
  def extract_user_id(token: str) -> Optional[str]:
36
39
  """
37
40
  Extract user ID from JWT token.
38
-
41
+
39
42
  Tries common JWT claim fields: sub, userId, user_id, id
40
-
43
+
41
44
  Args:
42
45
  token: JWT token string
43
-
46
+
44
47
  Returns:
45
48
  User ID string if found, None otherwise
46
49
  """
47
50
  decoded = decode_token(token)
48
51
  if not decoded:
49
52
  return None
50
-
53
+
51
54
  # Try common JWT claim fields for user ID
52
55
  user_id = (
53
- decoded.get("sub") or
54
- decoded.get("userId") or
55
- decoded.get("user_id") or
56
- decoded.get("id")
56
+ decoded.get("sub") or decoded.get("userId") or decoded.get("user_id") or decoded.get("id")
57
57
  )
58
-
58
+
59
59
  return str(user_id) if user_id else None
60
60
 
61
61
 
62
62
  def extract_session_id(token: str) -> Optional[str]:
63
63
  """
64
64
  Extract session ID from JWT token.
65
-
65
+
66
66
  Args:
67
67
  token: JWT token string
68
-
68
+
69
69
  Returns:
70
70
  Session ID string if found, None otherwise
71
71
  """
72
72
  decoded = decode_token(token)
73
73
  if not decoded:
74
74
  return None
75
-
75
+
76
76
  value = decoded.get("sid") or decoded.get("sessionId")
77
77
  return value if isinstance(value, str) else None
78
78
 
79
+
80
+ class JwtTokenCache:
81
+ """
82
+ JWT token cache with expiration tracking.
83
+
84
+ Caches decoded JWT tokens to avoid repeated decoding operations.
85
+ """
86
+
87
+ def __init__(self, max_size: int = 1000):
88
+ """
89
+ Initialize JWT token cache.
90
+
91
+ Args:
92
+ max_size: Maximum cache size to prevent memory leaks
93
+ """
94
+ self._cache: Dict[str, Tuple[Dict[str, Any], datetime]] = {}
95
+ self._max_size = max_size
96
+
97
+ def get_decoded_token(self, token: str) -> Optional[Dict[str, Any]]:
98
+ """
99
+ Get decoded JWT token with caching for performance optimization.
100
+
101
+ Tokens are cached with expiration tracking to avoid repeated decoding.
102
+
103
+ Args:
104
+ token: JWT token string
105
+
106
+ Returns:
107
+ Decoded token payload as dictionary, or None if decoding fails
108
+ """
109
+ now = datetime.now()
110
+
111
+ # Check cache first
112
+ if token in self._cache:
113
+ cached_decoded, expires_at = self._cache[token]
114
+ # If not expired, return cached value
115
+ if expires_at > now:
116
+ return cached_decoded
117
+ # Expired, remove from cache
118
+ del self._cache[token]
119
+
120
+ # Decode token
121
+ try:
122
+ decoded = decode_token(token)
123
+ if not decoded:
124
+ return None
125
+
126
+ # Extract expiration from token (if available)
127
+ expires_at = now + timedelta(hours=1) # Default: 1 hour cache
128
+ if "exp" in decoded and isinstance(decoded["exp"], (int, float)):
129
+ # Use token expiration minus 5 minutes buffer
130
+ token_exp = datetime.fromtimestamp(decoded["exp"])
131
+ expires_at = min(token_exp - timedelta(minutes=5), now + timedelta(hours=1))
132
+ elif "iat" in decoded and "exp" not in decoded:
133
+ # Estimate expiration if only issued_at is present
134
+ expires_at = now + timedelta(hours=1)
135
+
136
+ # Cache the decoded token
137
+ # Limit cache size to prevent memory leaks
138
+ if len(self._cache) >= self._max_size:
139
+ # Remove oldest entries (simple FIFO - remove first 10%)
140
+ keys_to_remove = list(self._cache.keys())[: self._max_size // 10]
141
+ for key in keys_to_remove:
142
+ del self._cache[key]
143
+
144
+ self._cache[token] = (decoded, expires_at)
145
+ return decoded
146
+
147
+ except Exception:
148
+ return None
149
+
150
+ def extract_user_id_from_headers(self, headers: Dict[str, Any]) -> Optional[str]:
151
+ """
152
+ Extract user ID from JWT token in Authorization header with caching.
153
+
154
+ Args:
155
+ headers: Request headers dictionary
156
+
157
+ Returns:
158
+ User ID if found, None otherwise
159
+ """
160
+ auth_header = headers.get("authorization") or headers.get("Authorization")
161
+ if not auth_header or not isinstance(auth_header, str):
162
+ return None
163
+
164
+ # Extract token (Bearer <token> format)
165
+ if auth_header.startswith("Bearer "):
166
+ token = auth_header[7:]
167
+ else:
168
+ token = auth_header
169
+
170
+ try:
171
+ decoded = self.get_decoded_token(token)
172
+ if decoded:
173
+ return decoded.get("sub") or decoded.get("userId") or decoded.get("user_id")
174
+ except Exception:
175
+ pass
176
+
177
+ return None
178
+
179
+ def clear_token(self, token: str) -> None:
180
+ """
181
+ Clear a specific token from cache.
182
+
183
+ Args:
184
+ token: JWT token string to remove from cache
185
+ """
186
+ if token in self._cache:
187
+ del self._cache[token]