Skip to content

Commit a4a2124

Browse files
Bug fix
1 parent 3127bc6 commit a4a2124

1 file changed

Lines changed: 52 additions & 11 deletions

File tree

src/lara_sdk/_client.py

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import hashlib
44
import hmac
55
import json
6+
import time
67
from typing import Dict, Optional, Union, List
78

89
import requests
@@ -121,13 +122,35 @@ def post_and_get_stream(self, path: str, body: Dict = None, files: Dict = None,
121122
"""
122123
return self._request_stream('POST', path, body, files, headers)
123124

125+
def _is_token_expired(self, buffer_seconds: int = 5) -> bool:
126+
"""Check if the current JWT token is expired or about to expire.
127+
128+
:param buffer_seconds: Number of seconds before actual expiry to consider the token expired.
129+
:return: True if the token is expired or will expire within the buffer period.
130+
"""
131+
if not self._token:
132+
return True
133+
try:
134+
parts = self._token.split('.')
135+
if len(parts) != 3:
136+
return True
137+
padded_encoded_payload = parts[1] + '=' * (-len(parts[1]) % 4) # Pad base64 string if necessary
138+
payload = json.loads(base64.urlsafe_b64decode(padded_encoded_payload))
139+
exp = payload.get('exp')
140+
if not isinstance(exp, (int, float)):
141+
return True
142+
return exp <= time.time() + buffer_seconds
143+
except Exception:
144+
return True
145+
124146
def _request(self, method: str, path: str, body: Dict = None, files: Dict = None, headers: Dict = None,
125147
retry_count: int = 0) -> Optional[Union[Dict, List, bytes]]:
126148
"""
127149
Execute an authenticated HTTP request with automatic token management.
128150
"""
129-
# Ensure we have a valid token
130-
if self._token is None:
151+
# Ensure we have a valid, non-expired token
152+
if self._token is None or self._is_token_expired():
153+
self._token = None
131154
self._authenticate()
132155

133156
if not path.startswith('/'):
@@ -174,14 +197,16 @@ def _request(self, method: str, path: str, body: Dict = None, files: Dict = None
174197
# Handle 401 - token expired, refresh and retry once
175198
if response.status_code == 401 and retry_count < 1:
176199
self._token = None
200+
self._refresh_or_reauthenticate()
177201
return self._request(method, path, body, files, headers, retry_count=retry_count + 1)
178202

179203
raise LaraApiError.from_response(response)
180204

181205
def _request_stream(self, method: str, path: str, body: Dict = None, files: Dict = None, headers: Dict = None,
182206
retry_count: int = 0):
183-
# Ensure we have a valid token
184-
if self._token is None:
207+
# Ensure we have a valid, non-expired token
208+
if self._token is None or self._is_token_expired():
209+
self._token = None
185210
self._authenticate()
186211

187212
if not path.startswith('/'):
@@ -209,6 +234,7 @@ def _request_stream(self, method: str, path: str, body: Dict = None, files: Dict
209234
# Handle 401 - token expired, refresh and retry once
210235
if response.status_code == 401 and retry_count < 1:
211236
self._token = None
237+
self._refresh_or_reauthenticate()
212238
yield from self._request_stream(method, path, body, files, headers, retry_count=retry_count + 1)
213239
return
214240

@@ -239,19 +265,34 @@ def _authenticate(self) -> str:
239265
if self._token is not None:
240266
return self._token
241267

242-
if self._refresh_token is not None:
243-
# Try to refresh first
244-
self._refresh()
245-
elif isinstance(self._auth, AuthToken):
268+
if isinstance(self._auth, AuthToken) and self._refresh_token is None:
246269
self._token = self._auth.token
247270
self._refresh_token = self._auth.refresh_token
248-
elif isinstance(self._auth, AccessKey):
249-
self._authenticate_with_access_key()
250271
else:
251-
raise ValueError(f'Invalid authentication type: {type(self._auth).__name__}')
272+
self._refresh_or_reauthenticate()
252273

253274
return self._token
254275

276+
def _refresh_or_reauthenticate(self) -> None:
277+
"""
278+
Try to refresh the token. If refresh fails, fall back to AccessKey re-authentication.
279+
"""
280+
if self._refresh_token is not None:
281+
# Try to refresh first
282+
try:
283+
self._refresh()
284+
return
285+
except LaraApiError as e:
286+
self._refresh_token = None
287+
if not isinstance(self._auth, AccessKey):
288+
raise
289+
290+
if isinstance(self._auth, AccessKey):
291+
self._authenticate_with_access_key()
292+
return
293+
294+
raise ValueError(f'No authentication method available for token renewal')
295+
255296
def _authenticate_with_access_key(self) -> None:
256297
"""Authenticate using AccessKey with challenge-response."""
257298
path = '/v2/auth'

0 commit comments

Comments
 (0)