Skip to content

Commit e857948

Browse files
committed
add unit tests for authentication
1 parent 6a60e50 commit e857948

File tree

12 files changed

+1035
-121
lines changed

12 files changed

+1035
-121
lines changed

core/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,8 @@ dependencies {
33
implementation 'com.auth0:java-jwt:4.5.0'
44
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
55
implementation 'com.google.code.gson:gson:2.9.1'
6+
7+
testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0'
8+
testImplementation 'org.mockito:mockito-core:5.18.0'
9+
testImplementation 'org.mockito:mockito-junit-jupiter:5.18.0'
610
}

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

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package cloud.stackit.sdk.core;
22

33
import cloud.stackit.sdk.core.config.CoreConfiguration;
4+
import cloud.stackit.sdk.core.config.EnvironmentVariables;
45
import cloud.stackit.sdk.core.exception.ApiException;
56
import cloud.stackit.sdk.core.model.ServiceAccountKey;
7+
import cloud.stackit.sdk.core.utils.Utils;
68
import com.auth0.jwt.JWT;
79
import com.auth0.jwt.algorithms.Algorithm;
810
import com.google.gson.Gson;
@@ -36,7 +38,7 @@ public class KeyFlowAuthenticator {
3638
private final String tokenUrl;
3739
private long tokenLeewayInSeconds = DEFAULT_TOKEN_LEEWAY;
3840

39-
private static class KeyFlowTokenResponse {
41+
protected static class KeyFlowTokenResponse {
4042
@SerializedName("access_token")
4143
private String accessToken;
4244

@@ -52,27 +54,42 @@ private static class KeyFlowTokenResponse {
5254
@SerializedName("token_type")
5355
private String tokenType;
5456

55-
public boolean isExpired() {
57+
public KeyFlowTokenResponse(
58+
String accessToken,
59+
String refreshToken,
60+
long expiresIn,
61+
String scope,
62+
String tokenType) {
63+
this.accessToken = accessToken;
64+
this.refreshToken = refreshToken;
65+
this.expiresIn = expiresIn;
66+
this.scope = scope;
67+
this.tokenType = tokenType;
68+
}
69+
70+
protected boolean isExpired() {
5671
return expiresIn < new Date().toInstant().getEpochSecond();
5772
}
5873

59-
public String getAccessToken() {
74+
protected String getAccessToken() {
6075
return accessToken;
6176
}
6277
}
6378

79+
public KeyFlowAuthenticator(CoreConfiguration cfg, ServiceAccountKey saKey) {
80+
this(cfg, saKey, null);
81+
}
82+
6483
/**
6584
* Creates the initial service account and refreshes expired access token.
6685
*
6786
* @param cfg Configuration to set a custom token endpoint and the token expiration leeway.
6887
* @param saKey Service Account Key, which should be used for the authentication
69-
* @throws InvalidKeySpecException thrown when the private key in the service account can not be
70-
* parsed
71-
* @throws IOException thrown on unexpected responses from the key flow
72-
* @throws ApiException thrown on unexpected responses from the key flow
7388
*/
74-
public KeyFlowAuthenticator(CoreConfiguration cfg, ServiceAccountKey saKey)
75-
throws InvalidKeySpecException, IOException, ApiException {
89+
public KeyFlowAuthenticator(
90+
CoreConfiguration cfg,
91+
ServiceAccountKey saKey,
92+
EnvironmentVariables environmentVariables) {
7693
this.saKey = saKey;
7794
this.gson = new Gson();
7895
this.httpClient =
@@ -81,26 +98,36 @@ public KeyFlowAuthenticator(CoreConfiguration cfg, ServiceAccountKey saKey)
8198
.writeTimeout(10, TimeUnit.SECONDS)
8299
.readTimeout(30, TimeUnit.SECONDS)
83100
.build();
84-
if (cfg.getTokenCustomUrl() != null && !cfg.getTokenCustomUrl().trim().isEmpty()) {
101+
102+
if (environmentVariables == null) {
103+
environmentVariables = new EnvironmentVariables();
104+
}
105+
106+
if (Utils.isStringSet(cfg.getTokenCustomUrl())) {
85107
this.tokenUrl = cfg.getTokenCustomUrl();
108+
} else if (Utils.isStringSet(environmentVariables.getStackitTokenBaseurl())) {
109+
this.tokenUrl = environmentVariables.getStackitTokenBaseurl();
86110
} else {
87111
this.tokenUrl = DEFAULT_TOKEN_ENDPOINT;
88112
}
89113
if (cfg.getTokenExpirationLeeway() != null && cfg.getTokenExpirationLeeway() > 0) {
90114
this.tokenLeewayInSeconds = cfg.getTokenExpirationLeeway();
91115
}
92-
93-
createAccessToken();
94116
}
95117

96118
/**
97119
* Returns access token. If the token is expired it creates a new token.
98120
*
121+
* @throws InvalidKeySpecException thrown when the private key in the service account can not be
122+
* parsed
99123
* @throws IOException request for new access token failed
100124
* @throws ApiException response for new access token with bad status code
101125
*/
102-
public synchronized String getAccessToken() throws IOException, ApiException {
103-
if (token == null || token.isExpired()) {
126+
public synchronized String getAccessToken()
127+
throws IOException, ApiException, InvalidKeySpecException {
128+
if (token == null) {
129+
createAccessToken();
130+
} else if (token.isExpired()) {
104131
createAccessTokenWithRefreshToken();
105132
}
106133
return token.getAccessToken();
@@ -114,7 +141,7 @@ public synchronized String getAccessToken() throws IOException, ApiException {
114141
* @throws ApiException response for new access token with bad status code
115142
* @throws JsonSyntaxException parsing of the created access token failed
116143
*/
117-
private void createAccessToken()
144+
protected void createAccessToken()
118145
throws InvalidKeySpecException, IOException, JsonSyntaxException, ApiException {
119146
String grant = "urn:ietf:params:oauth:grant-type:jwt-bearer";
120147
String assertion;
@@ -137,7 +164,7 @@ private void createAccessToken()
137164
* @throws ApiException response for new access token with bad status code
138165
* @throws JsonSyntaxException can not parse new access token
139166
*/
140-
private synchronized void createAccessTokenWithRefreshToken()
167+
protected synchronized void createAccessTokenWithRefreshToken()
141168
throws IOException, JsonSyntaxException, ApiException {
142169
String refreshToken = token.refreshToken;
143170
Response response = requestToken(REFRESH_TOKEN, refreshToken).execute();
@@ -146,7 +173,7 @@ private synchronized void createAccessTokenWithRefreshToken()
146173
}
147174

148175
private synchronized void parseTokenResponse(Response response)
149-
throws ApiException, JsonSyntaxException {
176+
throws ApiException, JsonSyntaxException, IOException {
150177
if (response.code() != HttpURLConnection.HTTP_OK) {
151178
String body = null;
152179
if (response.body() != null) {
@@ -156,20 +183,15 @@ private synchronized void parseTokenResponse(Response response)
156183
throw new ApiException(
157184
response.message(), response.code(), response.headers().toMultimap(), body);
158185
}
159-
if (response.body() == null) {
186+
if (response.body() == null || response.body().contentLength() == 0) {
160187
throw new JsonSyntaxException("body from token creation is null");
161188
}
162189

163-
token =
190+
KeyFlowTokenResponse keyFlowTokenResponse =
164191
gson.fromJson(
165192
new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8),
166193
KeyFlowTokenResponse.class);
167-
token.expiresIn =
168-
JWT.decode(token.accessToken)
169-
.getExpiresAt()
170-
.toInstant()
171-
.minusSeconds(tokenLeewayInSeconds)
172-
.getEpochSecond();
194+
setToken(keyFlowTokenResponse);
173195
response.body().close();
174196
}
175197

@@ -189,6 +211,16 @@ private Call requestToken(String grant, String assertionValue) throws IOExceptio
189211
return httpClient.newCall(request);
190212
}
191213

214+
protected void setToken(KeyFlowTokenResponse response) {
215+
token = response;
216+
token.expiresIn =
217+
JWT.decode(response.accessToken)
218+
.getExpiresAt()
219+
.toInstant()
220+
.minusSeconds(tokenLeewayInSeconds)
221+
.getEpochSecond();
222+
}
223+
192224
private String generateSelfSignedJWT()
193225
throws InvalidKeySpecException, NoSuchAlgorithmException {
194226
RSAPrivateKey prvKey;

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import cloud.stackit.sdk.core.exception.ApiException;
44
import java.io.IOException;
5+
import java.security.spec.InvalidKeySpecException;
56
import okhttp3.Interceptor;
67
import okhttp3.Request;
78
import okhttp3.Response;
@@ -16,11 +17,12 @@ public KeyFlowInterceptor(KeyFlowAuthenticator authenticator) {
1617

1718
@NotNull @Override
1819
public Response intercept(Chain chain) throws IOException {
20+
1921
Request originalRequest = chain.request();
2022
String accessToken;
2123
try {
2224
accessToken = authenticator.getAccessToken();
23-
} catch (ApiException e) {
25+
} catch (InvalidKeySpecException | ApiException e) {
2426
// try-catch required, because ApiException can not be thrown in the implementation
2527
// of Interceptor.intercept(Chain chain)
2628
throw new RuntimeException(e);

0 commit comments

Comments
 (0)