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.
- miso_client/__init__.py +523 -130
- miso_client/api/__init__.py +35 -0
- miso_client/api/auth_api.py +367 -0
- miso_client/api/logs_api.py +91 -0
- miso_client/api/permissions_api.py +88 -0
- miso_client/api/roles_api.py +88 -0
- miso_client/api/types/__init__.py +75 -0
- miso_client/api/types/auth_types.py +183 -0
- miso_client/api/types/logs_types.py +71 -0
- miso_client/api/types/permissions_types.py +31 -0
- miso_client/api/types/roles_types.py +31 -0
- miso_client/errors.py +30 -4
- miso_client/models/__init__.py +4 -0
- miso_client/models/config.py +275 -72
- miso_client/models/error_response.py +39 -0
- miso_client/models/filter.py +255 -0
- miso_client/models/pagination.py +44 -0
- miso_client/models/sort.py +25 -0
- miso_client/services/__init__.py +6 -5
- miso_client/services/auth.py +496 -87
- miso_client/services/cache.py +42 -41
- miso_client/services/encryption.py +18 -17
- miso_client/services/logger.py +467 -328
- miso_client/services/logger_chain.py +288 -0
- miso_client/services/permission.py +130 -67
- miso_client/services/redis.py +28 -23
- miso_client/services/role.py +145 -62
- miso_client/utils/__init__.py +3 -3
- miso_client/utils/audit_log_queue.py +222 -0
- miso_client/utils/auth_strategy.py +88 -0
- miso_client/utils/auth_utils.py +65 -0
- miso_client/utils/circuit_breaker.py +125 -0
- miso_client/utils/client_token_manager.py +244 -0
- miso_client/utils/config_loader.py +88 -17
- miso_client/utils/controller_url_resolver.py +80 -0
- miso_client/utils/data_masker.py +104 -33
- miso_client/utils/environment_token.py +126 -0
- miso_client/utils/error_utils.py +216 -0
- miso_client/utils/fastapi_endpoints.py +166 -0
- miso_client/utils/filter.py +364 -0
- miso_client/utils/filter_applier.py +143 -0
- miso_client/utils/filter_parser.py +110 -0
- miso_client/utils/flask_endpoints.py +169 -0
- miso_client/utils/http_client.py +494 -262
- miso_client/utils/http_client_logging.py +352 -0
- miso_client/utils/http_client_logging_helpers.py +197 -0
- miso_client/utils/http_client_query_helpers.py +138 -0
- miso_client/utils/http_error_handler.py +92 -0
- miso_client/utils/http_log_formatter.py +115 -0
- miso_client/utils/http_log_masker.py +203 -0
- miso_client/utils/internal_http_client.py +435 -0
- miso_client/utils/jwt_tools.py +125 -16
- miso_client/utils/logger_helpers.py +206 -0
- miso_client/utils/logging_helpers.py +70 -0
- miso_client/utils/origin_validator.py +128 -0
- miso_client/utils/pagination.py +275 -0
- miso_client/utils/request_context.py +285 -0
- miso_client/utils/sensitive_fields_loader.py +116 -0
- miso_client/utils/sort.py +116 -0
- miso_client/utils/token_utils.py +114 -0
- miso_client/utils/url_validator.py +66 -0
- miso_client/utils/user_token_refresh.py +245 -0
- miso_client-3.7.2.dist-info/METADATA +1021 -0
- miso_client-3.7.2.dist-info/RECORD +68 -0
- miso_client-0.1.0.dist-info/METADATA +0 -551
- miso_client-0.1.0.dist-info/RECORD +0 -23
- {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/WHEEL +0 -0
- {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/licenses/LICENSE +0 -0
- {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/top_level.txt +0 -0
miso_client/services/auth.py
CHANGED
|
@@ -5,156 +5,565 @@ This module handles authentication operations including client token management,
|
|
|
5
5
|
token validation, user information retrieval, and logout functionality.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import hashlib
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, cast
|
|
12
|
+
|
|
13
|
+
from ..models.config import AuthResult, AuthStrategy, UserInfo
|
|
14
|
+
from ..services.cache import CacheService
|
|
10
15
|
from ..services.redis import RedisService
|
|
16
|
+
from ..utils.error_utils import extract_correlation_id_from_error
|
|
11
17
|
from ..utils.http_client import HttpClient
|
|
18
|
+
from ..utils.jwt_tools import decode_token
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from ..api import ApiClient
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
12
24
|
|
|
13
25
|
|
|
14
26
|
class AuthService:
|
|
15
27
|
"""Authentication service for token validation and user management."""
|
|
16
|
-
|
|
17
|
-
def __init__(
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
http_client: HttpClient,
|
|
32
|
+
redis: RedisService,
|
|
33
|
+
cache: Optional[CacheService] = None,
|
|
34
|
+
api_client: Optional["ApiClient"] = None,
|
|
35
|
+
):
|
|
18
36
|
"""
|
|
19
37
|
Initialize authentication service.
|
|
20
|
-
|
|
38
|
+
|
|
21
39
|
Args:
|
|
22
|
-
http_client: HTTP client instance
|
|
40
|
+
http_client: HTTP client instance (for backward compatibility)
|
|
23
41
|
redis: Redis service instance
|
|
42
|
+
cache: Optional cache service instance (for token validation caching)
|
|
43
|
+
api_client: Optional API client instance (for typed API calls)
|
|
24
44
|
"""
|
|
25
45
|
self.config = http_client.config
|
|
26
46
|
self.http_client = http_client
|
|
27
47
|
self.redis = redis
|
|
28
|
-
|
|
48
|
+
self.cache = cache
|
|
49
|
+
self.api_client = api_client
|
|
50
|
+
self.validation_ttl = self.config.validation_ttl
|
|
51
|
+
|
|
52
|
+
def _get_token_cache_key(self, token: str) -> str:
|
|
53
|
+
"""
|
|
54
|
+
Generate cache key for token validation using SHA-256 hash.
|
|
55
|
+
|
|
56
|
+
Uses token hash instead of full token for security.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
token: JWT token string
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Cache key string in format: token_validation:{sha256_hash}
|
|
63
|
+
"""
|
|
64
|
+
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
|
65
|
+
return f"token_validation:{token_hash}"
|
|
66
|
+
|
|
67
|
+
def _get_cache_ttl_from_token(self, token: str) -> int:
|
|
68
|
+
"""
|
|
69
|
+
Calculate smart TTL based on token expiration.
|
|
70
|
+
|
|
71
|
+
If token has expiration claim, cache until token_exp - 30s buffer.
|
|
72
|
+
Minimum: 60 seconds, Maximum: validation_ttl.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
token: JWT token string
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
TTL in seconds
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
decoded = decode_token(token)
|
|
82
|
+
if decoded and "exp" in decoded:
|
|
83
|
+
token_exp = decoded["exp"]
|
|
84
|
+
if isinstance(token_exp, (int, float)):
|
|
85
|
+
now = time.time()
|
|
86
|
+
# Calculate TTL as token_exp - now - 30s buffer
|
|
87
|
+
ttl = int(token_exp - now - 30)
|
|
88
|
+
# Clamp between min (60s) and max (validation_ttl)
|
|
89
|
+
return max(60, min(ttl, self.validation_ttl))
|
|
90
|
+
except Exception:
|
|
91
|
+
# If token expiration cannot be determined, use default TTL
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
return self.validation_ttl
|
|
95
|
+
|
|
29
96
|
async def get_environment_token(self) -> str:
|
|
30
97
|
"""
|
|
31
98
|
Get environment token using client credentials.
|
|
32
|
-
|
|
99
|
+
|
|
33
100
|
This is called automatically by HttpClient, but can be called manually if needed.
|
|
34
|
-
|
|
101
|
+
|
|
35
102
|
Returns:
|
|
36
103
|
Client token string
|
|
37
|
-
|
|
104
|
+
|
|
38
105
|
Raises:
|
|
39
106
|
AuthenticationError: If token fetch fails
|
|
40
107
|
"""
|
|
41
108
|
return await self.http_client.get_environment_token()
|
|
42
|
-
|
|
43
|
-
def
|
|
109
|
+
|
|
110
|
+
async def _check_cache_for_token(self, token: str) -> Optional[Dict[str, Any]]:
|
|
44
111
|
"""
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
Returns the login URL for browser redirect or manual navigation.
|
|
48
|
-
Backend will extract environment and application from client token.
|
|
49
|
-
|
|
112
|
+
Check cache for token validation result.
|
|
113
|
+
|
|
50
114
|
Args:
|
|
51
|
-
|
|
52
|
-
|
|
115
|
+
token: JWT token to check
|
|
116
|
+
|
|
53
117
|
Returns:
|
|
54
|
-
|
|
118
|
+
Cached validation result if found, None otherwise
|
|
55
119
|
"""
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
120
|
+
if not self.cache:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
cache_key = self._get_token_cache_key(token)
|
|
124
|
+
cached_result = await self.cache.get(cache_key)
|
|
125
|
+
if cached_result and isinstance(cached_result, dict):
|
|
126
|
+
logger.debug("Token validation cache hit")
|
|
127
|
+
return cast(Dict[str, Any], cached_result)
|
|
128
|
+
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
async def _fetch_validation_from_api_client(
|
|
132
|
+
self, token: str, auth_strategy: Optional[AuthStrategy] = None
|
|
133
|
+
) -> Dict[str, Any]:
|
|
59
134
|
"""
|
|
60
|
-
|
|
61
|
-
|
|
135
|
+
Fetch token validation using ApiClient.
|
|
136
|
+
|
|
62
137
|
Args:
|
|
63
138
|
token: JWT token to validate
|
|
64
|
-
|
|
139
|
+
auth_strategy: Optional authentication strategy
|
|
140
|
+
|
|
65
141
|
Returns:
|
|
66
|
-
|
|
142
|
+
Validation result dictionary
|
|
67
143
|
"""
|
|
68
|
-
|
|
144
|
+
if not self.api_client:
|
|
145
|
+
raise ValueError("ApiClient is required for this method")
|
|
146
|
+
response = await self.api_client.auth.validate_token(token, auth_strategy=auth_strategy)
|
|
147
|
+
# Extract data from typed response
|
|
148
|
+
return {
|
|
149
|
+
"success": response.success,
|
|
150
|
+
"data": {
|
|
151
|
+
"authenticated": response.data.authenticated,
|
|
152
|
+
"user": response.data.user.model_dump() if response.data.user else None,
|
|
153
|
+
"expiresAt": response.data.expiresAt,
|
|
154
|
+
},
|
|
155
|
+
"timestamp": response.timestamp,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async def _fetch_validation_from_http_client(
|
|
159
|
+
self, token: str, auth_strategy: Optional[AuthStrategy] = None
|
|
160
|
+
) -> Dict[str, Any]:
|
|
161
|
+
"""
|
|
162
|
+
Fetch token validation using HttpClient (backward compatibility).
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
token: JWT token to validate
|
|
166
|
+
auth_strategy: Optional authentication strategy
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Validation result dictionary
|
|
170
|
+
"""
|
|
171
|
+
if auth_strategy is not None:
|
|
69
172
|
result = await self.http_client.authenticated_request(
|
|
70
173
|
"POST",
|
|
71
|
-
"/api/auth/validate",
|
|
72
|
-
token
|
|
174
|
+
"/api/v1/auth/validate",
|
|
175
|
+
token,
|
|
176
|
+
{"token": token},
|
|
177
|
+
auth_strategy=auth_strategy,
|
|
73
178
|
)
|
|
74
|
-
|
|
179
|
+
return result # type: ignore[no-any-return]
|
|
180
|
+
|
|
181
|
+
result = await self.http_client.authenticated_request(
|
|
182
|
+
"POST", "/api/v1/auth/validate", token, {"token": token}
|
|
183
|
+
)
|
|
184
|
+
return result # type: ignore[no-any-return]
|
|
185
|
+
|
|
186
|
+
async def _fetch_validation_from_api(
|
|
187
|
+
self, token: str, auth_strategy: Optional[AuthStrategy] = None
|
|
188
|
+
) -> Dict[str, Any]:
|
|
189
|
+
"""
|
|
190
|
+
Fetch token validation from API (ApiClient or HttpClient).
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
token: JWT token to validate
|
|
194
|
+
auth_strategy: Optional authentication strategy
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Validation result dictionary
|
|
198
|
+
"""
|
|
199
|
+
if self.api_client:
|
|
200
|
+
return await self._fetch_validation_from_api_client(token, auth_strategy)
|
|
201
|
+
else:
|
|
202
|
+
return await self._fetch_validation_from_http_client(token, auth_strategy)
|
|
203
|
+
|
|
204
|
+
async def _cache_validation_result(self, token: str, result: Dict[str, Any]) -> None:
|
|
205
|
+
"""
|
|
206
|
+
Cache successful validation results.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
token: JWT token that was validated
|
|
210
|
+
result: Validation result dictionary
|
|
211
|
+
"""
|
|
212
|
+
if not self.cache:
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
result_dict: Dict[str, Any] = result
|
|
216
|
+
if result_dict.get("data", {}).get("authenticated") is not True:
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
cache_key = self._get_token_cache_key(token)
|
|
220
|
+
ttl = self._get_cache_ttl_from_token(token)
|
|
221
|
+
try:
|
|
222
|
+
await self.cache.set(cache_key, result_dict, ttl)
|
|
223
|
+
logger.debug(f"Token validation cached with TTL: {ttl}s")
|
|
224
|
+
except Exception as error:
|
|
225
|
+
logger.warning("Failed to cache validation result", exc_info=error)
|
|
226
|
+
|
|
227
|
+
async def _validate_token_request(
|
|
228
|
+
self, token: str, auth_strategy: Optional[AuthStrategy] = None
|
|
229
|
+
) -> Dict[str, Any]:
|
|
230
|
+
"""
|
|
231
|
+
Helper method to call /api/v1/auth/validate endpoint with proper request body.
|
|
232
|
+
|
|
233
|
+
Checks cache before making HTTP request and caches successful validation results.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
token: JWT token to validate
|
|
237
|
+
auth_strategy: Optional authentication strategy
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Validation result dictionary
|
|
241
|
+
"""
|
|
242
|
+
# Check cache first
|
|
243
|
+
cached_result = await self._check_cache_for_token(token)
|
|
244
|
+
if cached_result:
|
|
245
|
+
return cached_result
|
|
246
|
+
|
|
247
|
+
# Cache miss - fetch from API
|
|
248
|
+
result = await self._fetch_validation_from_api(token, auth_strategy)
|
|
249
|
+
|
|
250
|
+
# Cache successful validation results
|
|
251
|
+
await self._cache_validation_result(token, result)
|
|
252
|
+
|
|
253
|
+
return result
|
|
254
|
+
|
|
255
|
+
async def login(self, redirect: str, state: Optional[str] = None) -> Dict[str, Any]:
|
|
256
|
+
"""
|
|
257
|
+
Initiate login flow by calling the controller login endpoint.
|
|
258
|
+
|
|
259
|
+
This method calls GET /api/v1/auth/login with redirect and optional state parameters.
|
|
260
|
+
The controller returns a login URL that should be used to redirect the user to Keycloak.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
redirect: Callback URL where Keycloak redirects after authentication (required)
|
|
264
|
+
state: Optional CSRF protection token (auto-generated by backend if omitted)
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Dictionary containing:
|
|
268
|
+
- success: True if successful
|
|
269
|
+
- data: Dictionary with loginUrl and state
|
|
270
|
+
- timestamp: Response timestamp
|
|
271
|
+
|
|
272
|
+
Example:
|
|
273
|
+
>>> response = await auth_service.login(
|
|
274
|
+
... redirect="http://localhost:3000/auth/callback",
|
|
275
|
+
... state="abc123"
|
|
276
|
+
... )
|
|
277
|
+
>>> login_url = response["data"]["loginUrl"]
|
|
278
|
+
>>> state = response["data"]["state"]
|
|
279
|
+
"""
|
|
280
|
+
try:
|
|
281
|
+
if self.api_client:
|
|
282
|
+
# Use ApiClient for typed API calls
|
|
283
|
+
response = await self.api_client.auth.login(redirect, state)
|
|
284
|
+
# Extract data from typed response
|
|
285
|
+
return {
|
|
286
|
+
"success": response.success,
|
|
287
|
+
"data": {
|
|
288
|
+
"loginUrl": response.data.loginUrl,
|
|
289
|
+
"state": state, # State is returned in response if provided
|
|
290
|
+
},
|
|
291
|
+
"timestamp": response.timestamp,
|
|
292
|
+
}
|
|
293
|
+
else:
|
|
294
|
+
# Fallback to HttpClient for backward compatibility
|
|
295
|
+
params = {"redirect": redirect}
|
|
296
|
+
if state:
|
|
297
|
+
params["state"] = state
|
|
298
|
+
|
|
299
|
+
response = await self.http_client.get("/api/v1/auth/login", params=params)
|
|
300
|
+
return response # type: ignore[no-any-return]
|
|
301
|
+
except Exception as error:
|
|
302
|
+
correlation_id = extract_correlation_id_from_error(error)
|
|
303
|
+
logger.error(
|
|
304
|
+
"Login failed",
|
|
305
|
+
exc_info=error,
|
|
306
|
+
extra={"correlationId": correlation_id} if correlation_id else None,
|
|
307
|
+
)
|
|
308
|
+
# Return empty dict on error per service method pattern
|
|
309
|
+
return {}
|
|
310
|
+
|
|
311
|
+
async def validate_token(
|
|
312
|
+
self, token: str, auth_strategy: Optional[AuthStrategy] = None
|
|
313
|
+
) -> bool:
|
|
314
|
+
"""
|
|
315
|
+
Validate token with controller.
|
|
316
|
+
|
|
317
|
+
If API_KEY is configured and token matches it, bypasses OAuth2 validation.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
token: JWT token to validate (or API_KEY for testing)
|
|
321
|
+
auth_strategy: Optional authentication strategy
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
True if token is valid, False otherwise
|
|
325
|
+
"""
|
|
326
|
+
# Check API_KEY first (for testing)
|
|
327
|
+
if self.config.api_key and token == self.config.api_key:
|
|
328
|
+
return True
|
|
329
|
+
|
|
330
|
+
# Fall back to OAuth2 validation
|
|
331
|
+
try:
|
|
332
|
+
result = await self._validate_token_request(token, auth_strategy)
|
|
75
333
|
auth_result = AuthResult(**result)
|
|
76
334
|
return auth_result.authenticated
|
|
77
|
-
|
|
78
|
-
except Exception:
|
|
79
|
-
|
|
335
|
+
|
|
336
|
+
except Exception as error:
|
|
337
|
+
correlation_id = extract_correlation_id_from_error(error)
|
|
338
|
+
logger.error(
|
|
339
|
+
"Token validation failed",
|
|
340
|
+
exc_info=error,
|
|
341
|
+
extra={"correlationId": correlation_id} if correlation_id else None,
|
|
342
|
+
)
|
|
80
343
|
return False
|
|
81
|
-
|
|
82
|
-
async def get_user(
|
|
344
|
+
|
|
345
|
+
async def get_user(
|
|
346
|
+
self, token: str, auth_strategy: Optional[AuthStrategy] = None
|
|
347
|
+
) -> Optional[UserInfo]:
|
|
83
348
|
"""
|
|
84
349
|
Get user information from token.
|
|
85
|
-
|
|
350
|
+
|
|
351
|
+
If API_KEY is configured and token matches it, returns None (no user info for API key auth).
|
|
352
|
+
|
|
86
353
|
Args:
|
|
87
|
-
token: JWT token
|
|
88
|
-
|
|
354
|
+
token: JWT token (or API_KEY for testing)
|
|
355
|
+
auth_strategy: Optional authentication strategy
|
|
356
|
+
|
|
89
357
|
Returns:
|
|
90
358
|
UserInfo if token is valid, None otherwise
|
|
91
359
|
"""
|
|
360
|
+
# Check API_KEY first (for testing)
|
|
361
|
+
if self.config.api_key and token == self.config.api_key:
|
|
362
|
+
# API key authentication doesn't provide user info
|
|
363
|
+
return None
|
|
364
|
+
|
|
365
|
+
# Fall back to OAuth2 validation
|
|
92
366
|
try:
|
|
93
|
-
result = await self.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if auth_result.authenticated and auth_result.user:
|
|
102
|
-
return auth_result.user
|
|
103
|
-
|
|
367
|
+
result = await self._validate_token_request(token, auth_strategy)
|
|
368
|
+
# _validate_token_request returns dict with "data" key
|
|
369
|
+
authenticated = result.get("data", {}).get("authenticated", False)
|
|
370
|
+
user_data = result.get("data", {}).get("user")
|
|
371
|
+
|
|
372
|
+
if authenticated and user_data:
|
|
373
|
+
return UserInfo(**user_data)
|
|
374
|
+
|
|
104
375
|
return None
|
|
105
|
-
|
|
106
|
-
except Exception:
|
|
107
|
-
|
|
376
|
+
|
|
377
|
+
except Exception as error:
|
|
378
|
+
correlation_id = extract_correlation_id_from_error(error)
|
|
379
|
+
logger.error(
|
|
380
|
+
"Failed to get user info",
|
|
381
|
+
exc_info=error,
|
|
382
|
+
extra={"correlationId": correlation_id} if correlation_id else None,
|
|
383
|
+
)
|
|
108
384
|
return None
|
|
109
|
-
|
|
110
|
-
async def get_user_info(
|
|
385
|
+
|
|
386
|
+
async def get_user_info(
|
|
387
|
+
self, token: str, auth_strategy: Optional[AuthStrategy] = None
|
|
388
|
+
) -> Optional[UserInfo]:
|
|
111
389
|
"""
|
|
112
|
-
Get user information from GET /api/auth/user endpoint.
|
|
113
|
-
|
|
390
|
+
Get user information from GET /api/v1/auth/user endpoint.
|
|
391
|
+
|
|
392
|
+
If API_KEY is configured and token matches it, returns None (no user info for API key auth).
|
|
393
|
+
|
|
114
394
|
Args:
|
|
115
|
-
token: JWT token
|
|
116
|
-
|
|
395
|
+
token: JWT token (or API_KEY for testing)
|
|
396
|
+
auth_strategy: Optional authentication strategy
|
|
397
|
+
|
|
117
398
|
Returns:
|
|
118
399
|
UserInfo if token is valid, None otherwise
|
|
119
400
|
"""
|
|
401
|
+
# Check API_KEY first (for testing)
|
|
402
|
+
if self.config.api_key and token == self.config.api_key:
|
|
403
|
+
# API key authentication doesn't provide user info
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
# Fall back to OAuth2 validation
|
|
120
407
|
try:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
408
|
+
if self.api_client:
|
|
409
|
+
# Use ApiClient for typed API calls
|
|
410
|
+
response = await self.api_client.auth.get_user(token, auth_strategy=auth_strategy)
|
|
411
|
+
# Extract user from typed response
|
|
412
|
+
return response.data.user
|
|
413
|
+
else:
|
|
414
|
+
# Fallback to HttpClient for backward compatibility
|
|
415
|
+
if auth_strategy is not None:
|
|
416
|
+
user_data = await self.http_client.authenticated_request(
|
|
417
|
+
"GET", "/api/v1/auth/user", token, auth_strategy=auth_strategy
|
|
418
|
+
)
|
|
419
|
+
else:
|
|
420
|
+
user_data = await self.http_client.authenticated_request(
|
|
421
|
+
"GET", "/api/v1/auth/user", token
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
return UserInfo(**user_data)
|
|
425
|
+
|
|
426
|
+
except Exception as error:
|
|
427
|
+
correlation_id = extract_correlation_id_from_error(error)
|
|
428
|
+
logger.error(
|
|
429
|
+
"Failed to get user info",
|
|
430
|
+
exc_info=error,
|
|
431
|
+
extra={"correlationId": correlation_id} if correlation_id else None,
|
|
125
432
|
)
|
|
126
|
-
|
|
127
|
-
return UserInfo(**user_data)
|
|
128
|
-
|
|
129
|
-
except Exception:
|
|
130
|
-
# Failed to get user info, return None
|
|
131
433
|
return None
|
|
132
|
-
|
|
133
|
-
async def logout(self) ->
|
|
434
|
+
|
|
435
|
+
async def logout(self, token: str) -> Dict[str, Any]:
|
|
134
436
|
"""
|
|
135
|
-
Logout user.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
437
|
+
Logout user by invalidating the access token.
|
|
438
|
+
|
|
439
|
+
This method calls POST /api/v1/auth/logout with the user's access token in the request body.
|
|
440
|
+
The token will be invalidated on the server side, and JWT token cache will be cleared automatically.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
token: Access token to invalidate (required)
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
Dictionary containing:
|
|
447
|
+
- success: True if successful
|
|
448
|
+
- message: Success message
|
|
449
|
+
- timestamp: Response timestamp
|
|
450
|
+
|
|
451
|
+
Example:
|
|
452
|
+
>>> response = await auth_service.logout(token="jwt-token-here")
|
|
453
|
+
>>> if response.get("success"):
|
|
454
|
+
... print("Logout successful")
|
|
141
455
|
"""
|
|
142
456
|
try:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
457
|
+
if self.api_client:
|
|
458
|
+
# Use ApiClient for typed API calls
|
|
459
|
+
response = await self.api_client.auth.logout(token)
|
|
460
|
+
# Extract data from typed response
|
|
461
|
+
result = {
|
|
462
|
+
"success": response.success,
|
|
463
|
+
"message": response.message,
|
|
464
|
+
"timestamp": response.timestamp,
|
|
465
|
+
}
|
|
466
|
+
else:
|
|
467
|
+
# Fallback to HttpClient for backward compatibility
|
|
468
|
+
result = await self.http_client.authenticated_request(
|
|
469
|
+
"POST", "/api/v1/auth/logout", token, {"token": token}
|
|
470
|
+
)
|
|
471
|
+
result = result # type: ignore[assignment]
|
|
472
|
+
|
|
473
|
+
# Clear JWT token cache after successful logout
|
|
474
|
+
try:
|
|
475
|
+
self.http_client.clear_user_token(token)
|
|
476
|
+
except Exception:
|
|
477
|
+
# Silently continue if cache clearing fails
|
|
478
|
+
pass
|
|
479
|
+
|
|
480
|
+
# Clear validation cache entry after successful logout
|
|
481
|
+
if self.cache:
|
|
482
|
+
try:
|
|
483
|
+
cache_key = self._get_token_cache_key(token)
|
|
484
|
+
await self.cache.delete(cache_key)
|
|
485
|
+
logger.debug("Token validation cache cleared on logout")
|
|
486
|
+
except Exception as error:
|
|
487
|
+
logger.warning("Failed to clear validation cache on logout", exc_info=error)
|
|
488
|
+
|
|
489
|
+
return result # type: ignore[no-any-return]
|
|
490
|
+
except Exception as error:
|
|
491
|
+
correlation_id = extract_correlation_id_from_error(error)
|
|
492
|
+
logger.error(
|
|
493
|
+
"Logout failed",
|
|
494
|
+
exc_info=error,
|
|
495
|
+
extra={"correlationId": correlation_id} if correlation_id else None,
|
|
496
|
+
)
|
|
497
|
+
# Return empty dict on error per service method pattern
|
|
498
|
+
return {}
|
|
499
|
+
|
|
500
|
+
async def refresh_user_token(self, refresh_token: str) -> Optional[Dict[str, Any]]:
|
|
501
|
+
"""
|
|
502
|
+
Refresh user access token using refresh token.
|
|
503
|
+
|
|
504
|
+
The refresh endpoint uses the refresh token in the request body for authentication.
|
|
505
|
+
Client token is automatically sent via x-client-token header.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
refresh_token: Refresh token string
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
Dictionary containing:
|
|
512
|
+
- token: New access token
|
|
513
|
+
- refreshToken: New refresh token (if provided)
|
|
514
|
+
- expiresIn: Token expiration in seconds
|
|
515
|
+
None if refresh fails
|
|
516
|
+
"""
|
|
517
|
+
try:
|
|
518
|
+
if self.api_client:
|
|
519
|
+
# Use ApiClient for typed API calls
|
|
520
|
+
response = await self.api_client.auth.refresh_token(refresh_token)
|
|
521
|
+
# Extract data from typed response
|
|
522
|
+
# Map accessToken to token for backward compatibility
|
|
523
|
+
return {
|
|
524
|
+
"success": response.success,
|
|
525
|
+
"data": {
|
|
526
|
+
"token": response.data.accessToken, # Map accessToken to token
|
|
527
|
+
"accessToken": response.data.accessToken,
|
|
528
|
+
"refreshToken": response.data.refreshToken,
|
|
529
|
+
"expiresIn": response.data.expiresIn,
|
|
530
|
+
},
|
|
531
|
+
"message": response.message,
|
|
532
|
+
"timestamp": response.timestamp,
|
|
533
|
+
}
|
|
534
|
+
else:
|
|
535
|
+
# Fallback to HttpClient for backward compatibility
|
|
536
|
+
# Uses request() (not authenticated_request()) since refresh token is the auth
|
|
537
|
+
response = await self.http_client.request(
|
|
538
|
+
"POST",
|
|
539
|
+
"/api/v1/auth/refresh",
|
|
540
|
+
{"refreshToken": refresh_token},
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
return response # type: ignore[no-any-return]
|
|
544
|
+
except Exception as error:
|
|
545
|
+
correlation_id = extract_correlation_id_from_error(error)
|
|
546
|
+
logger.error(
|
|
547
|
+
"Failed to refresh user token",
|
|
548
|
+
exc_info=error,
|
|
549
|
+
extra={"correlationId": correlation_id} if correlation_id else None,
|
|
550
|
+
)
|
|
551
|
+
return None
|
|
552
|
+
|
|
553
|
+
async def is_authenticated(
|
|
554
|
+
self, token: str, auth_strategy: Optional[AuthStrategy] = None
|
|
555
|
+
) -> bool:
|
|
151
556
|
"""
|
|
152
557
|
Check if user is authenticated (has valid token).
|
|
153
|
-
|
|
558
|
+
|
|
154
559
|
Args:
|
|
155
560
|
token: JWT token
|
|
156
|
-
|
|
561
|
+
auth_strategy: Optional authentication strategy
|
|
562
|
+
|
|
157
563
|
Returns:
|
|
158
564
|
True if user is authenticated, False otherwise
|
|
159
565
|
"""
|
|
160
|
-
|
|
566
|
+
if auth_strategy is not None:
|
|
567
|
+
return await self.validate_token(token, auth_strategy=auth_strategy)
|
|
568
|
+
else:
|
|
569
|
+
return await self.validate_token(token)
|