Skip to content

Commit bf7b5bd

Browse files
author
Vadim Torchin
committed
fix: bound token refresh retries and raise on final failure
1 parent 423e2da commit bf7b5bd

File tree

2 files changed

+51
-12
lines changed

2 files changed

+51
-12
lines changed

core/src/stackit/core/auth_methods/key_auth.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class KeyAuth(AuthBase):
4242
DEFAULT_TOKEN_ENDPOINT = "https://service-account.api.stackit.cloud/token" # noqa S105 false positive
4343
TOKEN_EXPIRY_CHECK_INTERVAL = timedelta(seconds=60)
4444
EXPIRATION_LEEWAY = timedelta(minutes=5)
45+
MAX_REFRESH_RETRIES = 3
4546

4647
timeout: Optional[int] = 30
4748
initial_token: Optional[str]
@@ -73,6 +74,8 @@ def __init__(
7374

7475
def __call__(self, r: Request) -> Request:
7576
with self.lock:
77+
if self.refresh_future is not None and self.refresh_future.done():
78+
self.refresh_future.result()
7679
if self.__is_token_expired(self.access_token):
7780
if self.refresh_future is None or self.refresh_future.done():
7881
self.refresh_future = self.executor.submit(self.__refresh_token)
@@ -108,13 +111,15 @@ def __fetch_token_from_endpoint(self) -> None:
108111
self.access_token = response_json["access_token"]
109112
self.refresh_token = response_json["refresh_token"]
110113
except requests.RequestException as e:
111-
print(f"Initial token fetch failed: {e}")
114+
raise requests.RequestException("Initial token fetch failed") from e
112115

113116
def __start_token_refresh_task(self):
114117
def token_refresh_task():
115118
while True:
116119
time.sleep(self.TOKEN_EXPIRY_CHECK_INTERVAL.total_seconds())
117120
with self.lock:
121+
if self.refresh_future is not None and self.refresh_future.done():
122+
self.refresh_future.result()
118123
if self.__is_token_expired(self.access_token) and (
119124
self.refresh_future is None or self.refresh_future.done()
120125
):
@@ -135,16 +140,19 @@ def __refresh_token(self):
135140
"refresh_token": self.refresh_token,
136141
}
137142

138-
try:
139-
response = requests.post(self.token_endpoint, data=body, timeout=self.timeout)
140-
response.raise_for_status()
141-
response_data = response.json()
142-
new_token = response_data.get("access_token")
143-
# with self.lock:
144-
self.access_token = new_token
145-
print("Token successfully refreshed!")
146-
except requests.RequestException as e:
147-
print(f"Token refresh failed: {e}")
143+
last_exception = None
144+
for _ in range(self.MAX_REFRESH_RETRIES):
145+
try:
146+
response = requests.post(self.token_endpoint, data=body, timeout=self.timeout)
147+
response.raise_for_status()
148+
response_data = response.json()
149+
new_token = response_data.get("access_token")
150+
self.access_token = new_token
151+
return
152+
except requests.RequestException as e:
153+
last_exception = e
154+
155+
raise requests.RequestException("Token refresh failed after retries") from last_exception
148156

149157
def __is_token_expired(self, token: str) -> bool:
150158
try:

core/tests/core/test_auth.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
import pytest
44
import json
5+
import jwt
6+
import requests
57
from unittest.mock import patch, mock_open, Mock
68

79
from requests.auth import HTTPBasicAuth
810

9-
from stackit.core.auth_methods.key_auth import KeyAuth
11+
from stackit.core.auth_methods.key_auth import KeyAuth, ServiceAccountKey
1012
from stackit.core.auth_methods.token_auth import TokenAuth
1113
from stackit.core.authorization import Authorization
1214
from stackit.core.configuration import Configuration
@@ -262,3 +264,32 @@ def test_service_account_keyfile_not_found_raises_exception(self):
262264
config = Configuration(service_account_key_path="/non/existent/path/to/file")
263265
with pytest.raises(FileNotFoundError):
264266
Authorization(config)
267+
268+
def test_token_refresh_fails_after_retries(self, service_account_key_file_json):
269+
service_account_key = ServiceAccountKey.model_validate_json(service_account_key_file_json)
270+
service_account_key.credentials.private_key = "test-private-key"
271+
272+
def set_initial_token(auth):
273+
auth.initial_token = "test-initial-token"
274+
275+
with patch.object(KeyAuth, "_KeyAuth__create_initial_token", new=set_initial_token), patch.object(
276+
KeyAuth, "_KeyAuth__start_token_refresh_task", return_value=None
277+
), patch("requests.post") as mock_post:
278+
init_response = Mock()
279+
init_response.raise_for_status.return_value = None
280+
init_response.json.return_value = {
281+
"access_token": jwt.encode({"exp": 4102444800}, "secret", algorithm="HS256"),
282+
"refresh_token": jwt.encode({"exp": 4102444800}, "secret", algorithm="HS256"),
283+
}
284+
mock_post.return_value = init_response
285+
286+
auth = KeyAuth(service_account_key)
287+
auth.refresh_token = jwt.encode({"exp": 4102444800}, "secret", algorithm="HS256")
288+
289+
mock_post.reset_mock()
290+
mock_post.side_effect = requests.RequestException("refresh failed")
291+
292+
with pytest.raises(requests.RequestException):
293+
auth._KeyAuth__refresh_token()
294+
295+
assert mock_post.call_count == KeyAuth.MAX_REFRESH_RETRIES

0 commit comments

Comments
 (0)