Skip to content

Commit 7ba2e6e

Browse files
authored
fix(core): prevent endless authentication loops (#160)
* fix: prevent endless authentication loops * docs: document that OkHttp try requests always without Authenticator and when 401 returns, it retries the request with Authenticator
1 parent f2553be commit 7ba2e6e

File tree

17 files changed

+173
-17
lines changed

17 files changed

+173
-17
lines changed

CHANGELOG.md

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,30 @@
11
## Release (2025-MM-DD)
2-
- `loadbalancer`: [v0.1.0](services/loadbalancer/CHANGELOG.md#v010)
3-
- Initial onboarding of STACKIT Java SDK for Load balancer service
4-
- `alb`: [v0.1.0](services/alb/CHANGELOG.md#v010)
5-
- Initial onboarding of STACKIT Java SDK for Application load balancer service
6-
- `objectstorage`: [v0.1.0](services/objectstorage/CHANGELOG.md#v010)
7-
- Initial onboarding of STACKIT Java SDK for Object storage service
8-
- `serverupdate`: [v0.1.0](services/serverupdate/CHANGELOG.md#v010)
9-
- Initial onboarding of STACKIT Java SDK for Server Update service
2+
- `core`: [v0.4.1](core/CHANGELOG.md/#v041)
3+
- **Bugfix:** Add check in `KeyFlowAuthenticator` to prevent endless loops
4+
- `iaas`: [v0.3.1](services/iaas/CHANGELOG.md#v031)
5+
- Bump dependency `cloud.stackit.sdk.core` to v0.4.1
6+
- `resourcemanager`: [v0.4.1](services/resourcemanager/CHANGELOG.md#v041)
7+
- Bump dependency `cloud.stackit.sdk.core` to v0.4.1
8+
- `loadbalancer`:
9+
- [v0.1.1](services/loadbalancer/CHANGELOG.md#v011)
10+
- Bump dependency `cloud.stackit.sdk.core` to v0.4.1
11+
- [v0.1.0](services/loadbalancer/CHANGELOG.md#v010)
12+
- Initial onboarding of STACKIT Java SDK for Load balancer service
13+
- `alb`:
14+
- [v0.1.1](services/alb/CHANGELOG.md#v011)
15+
- Bump dependency `cloud.stackit.sdk.core` to v0.4.1
16+
- [v0.1.0](services/alb/CHANGELOG.md#v010)
17+
- Initial onboarding of STACKIT Java SDK for Application load balancer service
18+
- `objectstorage`:
19+
- [v0.1.1](services/objectstorage/CHANGELOG.md#v011)
20+
- Bump dependency `cloud.stackit.sdk.core` to v0.4.1
21+
- [v0.1.0](services/objectstorage/CHANGELOG.md#v010)
22+
- Initial onboarding of STACKIT Java SDK for Object storage service
23+
- `serverupdate`:
24+
- [v0.1.1](services/serverupdate/CHANGELOG.md#v011)
25+
- Bump dependency `cloud.stackit.sdk.core` to v0.4.1
26+
- [v0.1.0](services/serverupdate/CHANGELOG.md#v010)
27+
- Initial onboarding of STACKIT Java SDK for Server Update service
1028

1129
## Release (2025-10-29)
1230
- `core`:

core/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## v0.4.1
2+
- **Bugfix:** Add check in `KeyFlowAuthenticator` to prevent endless loops
3+
14
## v0.4.0
25
- **Feature:** Added core wait handler structure which can be used by every service waiter implementation.
36

core/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.4.0
1+
0.4.1

core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ public class KeyFlowAuthenticator implements Authenticator {
5252
/**
5353
* Creates the initial service account and refreshes expired access token.
5454
*
55+
* <p>NOTE: It's normal that 2 requests are sent, it's regular OkHttp Authenticator behavior.
56+
* The first request is always attempted without the authenticator and in case the response is
57+
* Unauthorized(=401), OkHttp reattempt the request with the authenticator. See <a
58+
* href="https://square.github.io/okhttp/recipes/#handling-authentication-kt-java">OkHttp
59+
* Docs</a>
60+
*
5561
* @deprecated use constructor with OkHttpClient instead to prevent resource leaks. Will be
5662
* removed in April 2026.
5763
* @param cfg Configuration to set a custom token endpoint and the token expiration leeway.
@@ -65,6 +71,12 @@ public KeyFlowAuthenticator(CoreConfiguration cfg, ServiceAccountKey saKey) {
6571
/**
6672
* Creates the initial service account and refreshes expired access token.
6773
*
74+
* <p>NOTE: It's normal that 2 requests are sent, it's regular OkHttp Authenticator behavior.
75+
* The first request is always attempted without the authenticator and in case the response is
76+
* Unauthorized(=401), OkHttp reattempt the request with the authenticator. See <a
77+
* href="https://square.github.io/okhttp/recipes/#handling-authentication-kt-java">OkHttp
78+
* Docs</a>
79+
*
6880
* @deprecated use constructor with OkHttpClient instead to prevent resource leaks. Will be
6981
* removed in April 2026.
7082
* @param cfg Configuration to set a custom token endpoint and the token expiration leeway.
@@ -81,6 +93,12 @@ public KeyFlowAuthenticator(
8193
/**
8294
* Creates the initial service account and refreshes expired access token.
8395
*
96+
* <p>NOTE: It's normal that 2 requests are sent, it's regular OkHttp Authenticator behavior.
97+
* The first request is always attempted without the authenticator and in case the response is
98+
* Unauthorized(=401), OkHttp reattempt the request with the authenticator. See <a
99+
* href="https://square.github.io/okhttp/recipes/#handling-authentication-kt-java">OkHttp
100+
* Docs</a>
101+
*
84102
* @param httpClient OkHttpClient object
85103
* @param cfg Configuration to set a custom token endpoint and the token expiration leeway.
86104
*/
@@ -91,6 +109,12 @@ public KeyFlowAuthenticator(OkHttpClient httpClient, CoreConfiguration cfg) thro
91109
/**
92110
* Creates the initial service account and refreshes expired access token.
93111
*
112+
* <p>NOTE: It's normal that 2 requests are sent, it's regular OkHttp Authenticator behavior.
113+
* The first request is always attempted without the authenticator and in case the response is
114+
* Unauthorized(=401), OkHttp reattempt the request with the authenticator. See <a
115+
* href="https://square.github.io/okhttp/recipes/#handling-authentication-kt-java">OkHttp
116+
* Docs</a>
117+
*
94118
* @param httpClient OkHttpClient object
95119
* @param cfg Configuration to set a custom token endpoint and the token expiration leeway.
96120
* @param saKey Service Account Key, which should be used for the authentication
@@ -129,6 +153,9 @@ protected KeyFlowAuthenticator(
129153

130154
@Override
131155
public Request authenticate(Route route, @NotNull Response response) throws IOException {
156+
if (response.request().header("Authorization") != null) {
157+
return null; // Give up, we've already attempted to authenticate.
158+
}
132159
String accessToken;
133160
try {
134161
accessToken = getAccessToken();

core/src/test/java/cloud/stackit/sdk/core/KeyFlowAuthenticatorTest.java

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
import java.security.spec.InvalidKeySpecException;
1717
import java.time.temporal.ChronoUnit;
1818
import java.util.Date;
19-
import okhttp3.HttpUrl;
20-
import okhttp3.OkHttpClient;
19+
import okhttp3.*;
2120
import okhttp3.mockwebserver.MockResponse;
2221
import okhttp3.mockwebserver.MockWebServer;
2322
import org.junit.jupiter.api.AfterEach;
@@ -62,6 +61,9 @@ class KeyFlowAuthenticatorTest {
6261
+ "h/9afEtu5aUE/m+1vGBoH8z1\n"
6362
+ "-----END PRIVATE KEY-----\n";
6463

64+
private static final Request mockRequest =
65+
new Request.Builder().url("https://stackit.com").get().build();
66+
6567
private ServiceAccountKey createDummyServiceAccount() {
6668
ServiceAccountCredentials credentials =
6769
new ServiceAccountCredentials("aud", "iss", "kid", PRIVATE_KEY, "sub");
@@ -270,4 +272,92 @@ void createAccessTokenWithRefreshTokenResponse200WithEmptyBodyThrowsException()
270272
assertThrows(
271273
JsonSyntaxException.class, keyFlowAuthenticator::createAccessTokenWithRefreshToken);
272274
}
275+
276+
@Test
277+
@DisplayName("authenticator sets Authorization header")
278+
void authenticatorSetsAuthorizationHeaderIfNotAuthenticated()
279+
throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
280+
// Setup mockServer
281+
final String authorizationHeader = "Authorization";
282+
KeyFlowAuthenticator.KeyFlowTokenResponse mockResponse = mockResponseBody(false);
283+
// mock response for KeyFlow authentication with mocked access token
284+
MockResponse mockedResponse =
285+
new MockResponse()
286+
.setResponseCode(200)
287+
.setBody(new Gson().toJson(mockResponse))
288+
.addHeader("Content-type", "application/json");
289+
mockWebServer.enqueue(mockedResponse);
290+
HttpUrl url = mockWebServer.url(MOCK_WEBSERVER_PATH);
291+
292+
// Set unauthorized request
293+
Response unauthorizedRequest =
294+
new Response.Builder()
295+
.request(mockRequest)
296+
.code(401)
297+
.message("Unauthorized")
298+
.protocol(Protocol.HTTP_2)
299+
.build();
300+
301+
// Config
302+
CoreConfiguration cfg =
303+
new CoreConfiguration().tokenCustomUrl(url.toString()); // Use mockWebServer
304+
305+
// Check if "Authorization" header is unset
306+
assertNull(unauthorizedRequest.request().header(authorizationHeader));
307+
308+
// Prepare keyFlowAuthenticator
309+
KeyFlowAuthenticator keyFlowAuthenticator =
310+
new KeyFlowAuthenticator(httpClient, cfg, createDummyServiceAccount());
311+
// authenticator creates new access token and sets it the Authorization header
312+
Request newRequest = keyFlowAuthenticator.authenticate(null, unauthorizedRequest);
313+
314+
// Check if new request is not null
315+
assertNotNull(newRequest);
316+
// Check if the "Authorization" Header is set
317+
assertNotNull(newRequest.header(authorizationHeader));
318+
}
319+
320+
@Test
321+
@DisplayName("Authenticator returns null when already authenticated")
322+
void authenticatorReturnsNullWhenAlreadyAuthenticated()
323+
throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
324+
// Setup mockServer
325+
final String authorizationHeader = "Authorization";
326+
KeyFlowAuthenticator.KeyFlowTokenResponse mockResponse = mockResponseBody(false);
327+
// mock response for KeyFlow authentication with mocked access token
328+
MockResponse mockedResponse =
329+
new MockResponse()
330+
.setResponseCode(200)
331+
.setBody(new Gson().toJson(mockResponse))
332+
.addHeader("Content-type", "application/json");
333+
mockWebServer.enqueue(mockedResponse);
334+
HttpUrl url = mockWebServer.url(MOCK_WEBSERVER_PATH);
335+
336+
// Set unauthorized request
337+
Response unauthorizedRequest =
338+
new Response.Builder()
339+
.request(
340+
mockRequest
341+
.newBuilder()
342+
.addHeader(authorizationHeader, "<my-access-token>")
343+
.build())
344+
.code(401)
345+
.message("Unauthorized")
346+
.protocol(Protocol.HTTP_2)
347+
.build(); // Unauthorized request
348+
349+
// Config
350+
CoreConfiguration cfg =
351+
new CoreConfiguration().tokenCustomUrl(url.toString()); // Use mockWebServer
352+
353+
// Check if "Authorization" header is set
354+
assertNotNull(unauthorizedRequest.request().header(authorizationHeader));
355+
356+
// Prepare keyFlowAuthenticator
357+
KeyFlowAuthenticator keyFlowAuthenticator =
358+
new KeyFlowAuthenticator(httpClient, cfg, createDummyServiceAccount());
359+
360+
// Authenticator returns no new request, because "Authorization" header was already set
361+
assertNull(keyFlowAuthenticator.authenticate(null, unauthorizedRequest));
362+
}
273363
}

services/alb/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1+
## v0.1.1
2+
- Bump dependency `cloud.stackit.sdk.core` to v0.4.1
3+
14
## v0.1.0
25
- Initial onboarding of STACKIT Java SDK for Application load balancer service

services/alb/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.0
1+
0.1.1

services/iaas/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## v0.3.1
2+
- Bump dependency `cloud.stackit.sdk.core` to v0.4.1
3+
14
## v0.3.0
25
- **Feature:** Add `createdAt` and `updatedAt` attributes to `SecurityGroupRule`, `BaseSecurityGroupRule`, `CreateSecurityGroupRulePayload` model classes
36
- **Feature:** Add `description` attribute to `CreateNicPayload`, `NIC`, `UpdateNicPayload` model classes

services/iaas/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.3.0
1+
0.3.1

services/loadbalancer/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1+
## v0.1.1
2+
- Bump dependency `cloud.stackit.sdk.core` to v0.4.1
3+
14
## v0.1.0
25
- Initial onboarding of STACKIT Java SDK for Load balancer service

0 commit comments

Comments
 (0)