diff --git a/CHANGELOG.md b/CHANGELOG.md index 532935d..589984c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,30 @@ ## Release (2025-MM-DD) -- `loadbalancer`: [v0.1.0](services/loadbalancer/CHANGELOG.md#v010) - - Initial onboarding of STACKIT Java SDK for Load balancer service -- `alb`: [v0.1.0](services/alb/CHANGELOG.md#v010) - - Initial onboarding of STACKIT Java SDK for Application load balancer service -- `objectstorage`: [v0.1.0](services/objectstorage/CHANGELOG.md#v010) - - Initial onboarding of STACKIT Java SDK for Object storage service -- `serverupdate`: [v0.1.0](services/serverupdate/CHANGELOG.md#v010) - - Initial onboarding of STACKIT Java SDK for Server Update service +- `core`: [v0.4.1](core/CHANGELOG.md/#v041) + - **Bugfix:** Add check in `KeyFlowAuthenticator` to prevent endless loops +- `iaas`: [v0.3.1](services/iaas/CHANGELOG.md#v031) + - Bump dependency `cloud.stackit.sdk.core` to v0.4.1 +- `resourcemanager`: [v0.4.1](services/resourcemanager/CHANGELOG.md#v041) + - Bump dependency `cloud.stackit.sdk.core` to v0.4.1 +- `loadbalancer`: + - [v0.1.1](services/loadbalancer/CHANGELOG.md#v011) + - Bump dependency `cloud.stackit.sdk.core` to v0.4.1 + - [v0.1.0](services/loadbalancer/CHANGELOG.md#v010) + - Initial onboarding of STACKIT Java SDK for Load balancer service +- `alb`: + - [v0.1.1](services/alb/CHANGELOG.md#v011) + - Bump dependency `cloud.stackit.sdk.core` to v0.4.1 + - [v0.1.0](services/alb/CHANGELOG.md#v010) + - Initial onboarding of STACKIT Java SDK for Application load balancer service +- `objectstorage`: + - [v0.1.1](services/objectstorage/CHANGELOG.md#v011) + - Bump dependency `cloud.stackit.sdk.core` to v0.4.1 + - [v0.1.0](services/objectstorage/CHANGELOG.md#v010) + - Initial onboarding of STACKIT Java SDK for Object storage service +- `serverupdate`: + - [v0.1.1](services/serverupdate/CHANGELOG.md#v011) + - Bump dependency `cloud.stackit.sdk.core` to v0.4.1 + - [v0.1.0](services/serverupdate/CHANGELOG.md#v010) + - Initial onboarding of STACKIT Java SDK for Server Update service ## Release (2025-10-29) - `core`: diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index ee493e4..63f379e 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -1,3 +1,6 @@ +## v0.4.1 +- **Bugfix:** Add check in `KeyFlowAuthenticator` to prevent endless loops + ## v0.4.0 - **Feature:** Added core wait handler structure which can be used by every service waiter implementation. diff --git a/core/VERSION b/core/VERSION index 1d0ba9e..267577d 100644 --- a/core/VERSION +++ b/core/VERSION @@ -1 +1 @@ -0.4.0 +0.4.1 diff --git a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java index 2cb552b..9d7bb64 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java +++ b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java @@ -52,6 +52,12 @@ public class KeyFlowAuthenticator implements Authenticator { /** * Creates the initial service account and refreshes expired access token. * + *
NOTE: It's normal that 2 requests are sent, it's regular OkHttp Authenticator behavior. + * The first request is always attempted without the authenticator and in case the response is + * Unauthorized(=401), OkHttp reattempt the request with the authenticator. See OkHttp + * Docs + * * @deprecated use constructor with OkHttpClient instead to prevent resource leaks. Will be * removed in April 2026. * @param cfg Configuration to set a custom token endpoint and the token expiration leeway. @@ -65,6 +71,12 @@ public KeyFlowAuthenticator(CoreConfiguration cfg, ServiceAccountKey saKey) { /** * Creates the initial service account and refreshes expired access token. * + *
NOTE: It's normal that 2 requests are sent, it's regular OkHttp Authenticator behavior. + * The first request is always attempted without the authenticator and in case the response is + * Unauthorized(=401), OkHttp reattempt the request with the authenticator. See OkHttp + * Docs + * * @deprecated use constructor with OkHttpClient instead to prevent resource leaks. Will be * removed in April 2026. * @param cfg Configuration to set a custom token endpoint and the token expiration leeway. @@ -81,6 +93,12 @@ public KeyFlowAuthenticator( /** * Creates the initial service account and refreshes expired access token. * + *
NOTE: It's normal that 2 requests are sent, it's regular OkHttp Authenticator behavior. + * The first request is always attempted without the authenticator and in case the response is + * Unauthorized(=401), OkHttp reattempt the request with the authenticator. See OkHttp + * Docs + * * @param httpClient OkHttpClient object * @param cfg Configuration to set a custom token endpoint and the token expiration leeway. */ @@ -91,6 +109,12 @@ public KeyFlowAuthenticator(OkHttpClient httpClient, CoreConfiguration cfg) thro /** * Creates the initial service account and refreshes expired access token. * + *
NOTE: It's normal that 2 requests are sent, it's regular OkHttp Authenticator behavior.
+ * The first request is always attempted without the authenticator and in case the response is
+ * Unauthorized(=401), OkHttp reattempt the request with the authenticator. See OkHttp
+ * Docs
+ *
* @param httpClient OkHttpClient object
* @param cfg Configuration to set a custom token endpoint and the token expiration leeway.
* @param saKey Service Account Key, which should be used for the authentication
@@ -129,6 +153,9 @@ protected KeyFlowAuthenticator(
@Override
public Request authenticate(Route route, @NotNull Response response) throws IOException {
+ if (response.request().header("Authorization") != null) {
+ return null; // Give up, we've already attempted to authenticate.
+ }
String accessToken;
try {
accessToken = getAccessToken();
diff --git a/core/src/test/java/cloud/stackit/sdk/core/KeyFlowAuthenticatorTest.java b/core/src/test/java/cloud/stackit/sdk/core/KeyFlowAuthenticatorTest.java
index 7f763ce..1e8fc6b 100644
--- a/core/src/test/java/cloud/stackit/sdk/core/KeyFlowAuthenticatorTest.java
+++ b/core/src/test/java/cloud/stackit/sdk/core/KeyFlowAuthenticatorTest.java
@@ -16,8 +16,7 @@
import java.security.spec.InvalidKeySpecException;
import java.time.temporal.ChronoUnit;
import java.util.Date;
-import okhttp3.HttpUrl;
-import okhttp3.OkHttpClient;
+import okhttp3.*;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.AfterEach;
@@ -62,6 +61,9 @@ class KeyFlowAuthenticatorTest {
+ "h/9afEtu5aUE/m+1vGBoH8z1\n"
+ "-----END PRIVATE KEY-----\n";
+ private static final Request mockRequest =
+ new Request.Builder().url("https://stackit.com").get().build();
+
private ServiceAccountKey createDummyServiceAccount() {
ServiceAccountCredentials credentials =
new ServiceAccountCredentials("aud", "iss", "kid", PRIVATE_KEY, "sub");
@@ -270,4 +272,92 @@ void createAccessTokenWithRefreshTokenResponse200WithEmptyBodyThrowsException()
assertThrows(
JsonSyntaxException.class, keyFlowAuthenticator::createAccessTokenWithRefreshToken);
}
+
+ @Test
+ @DisplayName("authenticator sets Authorization header")
+ void authenticatorSetsAuthorizationHeaderIfNotAuthenticated()
+ throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
+ // Setup mockServer
+ final String authorizationHeader = "Authorization";
+ KeyFlowAuthenticator.KeyFlowTokenResponse mockResponse = mockResponseBody(false);
+ // mock response for KeyFlow authentication with mocked access token
+ MockResponse mockedResponse =
+ new MockResponse()
+ .setResponseCode(200)
+ .setBody(new Gson().toJson(mockResponse))
+ .addHeader("Content-type", "application/json");
+ mockWebServer.enqueue(mockedResponse);
+ HttpUrl url = mockWebServer.url(MOCK_WEBSERVER_PATH);
+
+ // Set unauthorized request
+ Response unauthorizedRequest =
+ new Response.Builder()
+ .request(mockRequest)
+ .code(401)
+ .message("Unauthorized")
+ .protocol(Protocol.HTTP_2)
+ .build();
+
+ // Config
+ CoreConfiguration cfg =
+ new CoreConfiguration().tokenCustomUrl(url.toString()); // Use mockWebServer
+
+ // Check if "Authorization" header is unset
+ assertNull(unauthorizedRequest.request().header(authorizationHeader));
+
+ // Prepare keyFlowAuthenticator
+ KeyFlowAuthenticator keyFlowAuthenticator =
+ new KeyFlowAuthenticator(httpClient, cfg, createDummyServiceAccount());
+ // authenticator creates new access token and sets it the Authorization header
+ Request newRequest = keyFlowAuthenticator.authenticate(null, unauthorizedRequest);
+
+ // Check if new request is not null
+ assertNotNull(newRequest);
+ // Check if the "Authorization" Header is set
+ assertNotNull(newRequest.header(authorizationHeader));
+ }
+
+ @Test
+ @DisplayName("Authenticator returns null when already authenticated")
+ void authenticatorReturnsNullWhenAlreadyAuthenticated()
+ throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
+ // Setup mockServer
+ final String authorizationHeader = "Authorization";
+ KeyFlowAuthenticator.KeyFlowTokenResponse mockResponse = mockResponseBody(false);
+ // mock response for KeyFlow authentication with mocked access token
+ MockResponse mockedResponse =
+ new MockResponse()
+ .setResponseCode(200)
+ .setBody(new Gson().toJson(mockResponse))
+ .addHeader("Content-type", "application/json");
+ mockWebServer.enqueue(mockedResponse);
+ HttpUrl url = mockWebServer.url(MOCK_WEBSERVER_PATH);
+
+ // Set unauthorized request
+ Response unauthorizedRequest =
+ new Response.Builder()
+ .request(
+ mockRequest
+ .newBuilder()
+ .addHeader(authorizationHeader, "