Skip to content

Commit ffae878

Browse files
committed
improve documentation and error handling of KeyFlow and Configuration
1 parent 3b2eb24 commit ffae878

File tree

5 files changed

+244
-109
lines changed

5 files changed

+244
-109
lines changed

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

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,42 @@
11
package cloud.stackit.sdk.core;
22

3+
import cloud.stackit.sdk.core.config.Configuration;
34
import cloud.stackit.sdk.core.model.ServiceAccountKey;
45
import com.auth0.jwt.JWT;
56
import com.auth0.jwt.algorithms.Algorithm;
67
import com.google.gson.Gson;
8+
import com.google.gson.JsonSyntaxException;
79
import com.google.gson.annotations.SerializedName;
810
import okhttp3.*;
911

1012
import java.io.IOException;
1113
import java.io.InputStreamReader;
1214
import java.net.HttpURLConnection;
1315
import java.nio.charset.StandardCharsets;
16+
import java.security.NoSuchAlgorithmException;
1417
import java.security.interfaces.RSAPrivateKey;
18+
import java.security.spec.InvalidKeySpecException;
1519
import java.util.Date;
1620
import java.util.HashMap;
1721
import java.util.Map;
1822
import java.util.UUID;
1923
import java.util.concurrent.TimeUnit;
2024

25+
/**
26+
* KeyFlowAuthenticator handles the Key Flow Authentication based on the Service Account Key.
27+
*/
2128
public class KeyFlowAuthenticator {
2229
private final String REFRESH_TOKEN = "refresh_token";
2330
private final String ASSERTION = "assertion";
31+
private final String DEFAULT_TOKEN_ENDPOINT = "https://service-account.api.stackit.cloud/token";
32+
private final long DEFAULT_TOKEN_LEEWAY = 60;
2433

2534
private final OkHttpClient httpClient;
2635
private final ServiceAccountKey saKey;
2736
private KeyFlowTokenResponse token;
2837
private final Gson gson;
2938
private final String tokenUrl;
39+
private long tokenLeewayInSeconds = DEFAULT_TOKEN_LEEWAY;
3040

3141
private static class KeyFlowTokenResponse {
3242
@SerializedName("access_token")
@@ -41,23 +51,38 @@ private static class KeyFlowTokenResponse {
4151
private String tokenType;
4252

4353
public boolean isExpired() {
44-
return expiresIn < new Date().toInstant().minusSeconds(60).getEpochSecond();
54+
return expiresIn < new Date().toInstant().getEpochSecond();
4555
}
4656

4757
public String getAccessToken() {
4858
return accessToken;
4959
}
5060
}
5161

52-
public KeyFlowAuthenticator(ServiceAccountKey saKey) {
62+
/**
63+
* Creates the initial service account and refreshes expired access token.
64+
* @param cfg Configuration to set a custom token endpoint and the token expiration leeway.
65+
* @param saKey Service Account Key, which should be used for the authentication
66+
* @throws InvalidKeySpecException Throws, when the private key in the service account can not be parsed
67+
* @throws IOException Throws, when on unexpected responses from the key flow
68+
*/
69+
public KeyFlowAuthenticator(Configuration cfg, ServiceAccountKey saKey) throws InvalidKeySpecException, IOException {
5370
this.saKey = saKey;
5471
this.gson = new Gson();
5572
this.httpClient = new OkHttpClient.Builder()
5673
.connectTimeout(10, TimeUnit.SECONDS)
5774
.writeTimeout(10, TimeUnit.SECONDS)
5875
.readTimeout(30, TimeUnit.SECONDS)
5976
.build();
60-
this.tokenUrl = "https://service-account.api.stackit.cloud/token";
77+
if (cfg.getTokenCustomUrl() != null && !cfg.getTokenCustomUrl().trim().isEmpty()) {
78+
this.tokenUrl = cfg.getTokenCustomUrl();
79+
} else {
80+
this.tokenUrl = DEFAULT_TOKEN_ENDPOINT;
81+
}
82+
if (cfg.getTokenExpirationLeeway() != null && cfg.getTokenExpirationLeeway() > 0) {
83+
this.tokenLeewayInSeconds = cfg.getTokenExpirationLeeway();
84+
}
85+
6186
createAccessToken();
6287
}
6388

@@ -68,26 +93,46 @@ public synchronized String getAccessToken() throws IOException {
6893
return token.getAccessToken();
6994
}
7095

71-
private void createAccessToken() {
96+
/**
97+
* Creates the inital accessToken and stores it in `this.token`
98+
* @throws InvalidKeySpecException can not parse private key
99+
* @throws IOException request for access token failed
100+
* @throws JsonSyntaxException parsing of the created access token failed
101+
*/
102+
private void createAccessToken() throws InvalidKeySpecException, IOException, JsonSyntaxException {
72103
String grant = "urn:ietf:params:oauth:grant-type:jwt-bearer";
73-
String assertion = generateSelfSignedJWT();
104+
String assertion;
105+
try {
106+
assertion = generateSelfSignedJWT();
107+
} catch (NoSuchAlgorithmException e) {
108+
throw new RuntimeException("could not find required algorithm for jwt signing. This should not happen and should be reported on https://github.com/stackitcloud/stackit-sdk-java/issues", e);
109+
}
74110
try(Response response = requestToken(grant, assertion).execute()) {
75111
parseTokenResponse(response);
76112
} catch (IOException | ApiException e) {
77-
e.printStackTrace();
113+
throw new IOException("request for access token failed", e);
114+
} catch (JsonSyntaxException e) {
115+
throw new JsonSyntaxException("parsing access token failed", e);
78116
}
79117
}
80118

81-
private synchronized void createAccessTokenWithRefreshToken() throws IOException {
119+
/**
120+
* Creates a new access token with the existing refresh token
121+
* @throws IOException request for new access token failed
122+
* @throws JsonSyntaxException can not parse new access token
123+
*/
124+
private synchronized void createAccessTokenWithRefreshToken() throws IOException, JsonSyntaxException {
82125
String refreshToken = token.refreshToken;
83126
try (Response response = requestToken(REFRESH_TOKEN, refreshToken).execute()) {
84127
parseTokenResponse(response);
85-
} catch (ApiException e) {
86-
e.printStackTrace();
128+
} catch (IOException | ApiException e) {
129+
throw new IOException("request for new access token failed", e);
130+
} catch (JsonSyntaxException e) {
131+
throw new JsonSyntaxException("parsing refreshed access token failed", e);
87132
}
88133
}
89134

90-
private synchronized void parseTokenResponse(Response response) throws ApiException {
135+
private synchronized void parseTokenResponse(Response response) throws ApiException, JsonSyntaxException {
91136
if (response.code() != HttpURLConnection.HTTP_OK) {
92137
String body = null;
93138
if (response.body() != null) {
@@ -97,22 +142,23 @@ private synchronized void parseTokenResponse(Response response) throws ApiExcept
97142
throw new ApiException(response.message(), response.code(), response.headers().toMultimap(), body);
98143
}
99144
if (response.body() == null) {
100-
throw new ApiException("body from token creation is null");
145+
throw new JsonSyntaxException("body from token creation is null");
101146
}
102147

103-
token = gson.fromJson(new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8), KeyFlowTokenResponse.class);
104-
token.expiresIn = JWT.decode(token.accessToken).getExpiresAt().toInstant().getEpochSecond();
105-
response.body().close();
148+
try {
149+
token = gson.fromJson(new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8), KeyFlowTokenResponse.class);
150+
token.expiresIn = JWT.decode(token.accessToken).getExpiresAt().toInstant().minusSeconds(tokenLeewayInSeconds).getEpochSecond();
151+
response.body().close();
152+
} catch (JsonSyntaxException e) {
153+
throw new JsonSyntaxException("could not parse response of created token", e);
154+
}
106155
}
107156

108-
private Call requestToken(String grant, String assertion) throws IOException {
157+
private Call requestToken(String grant, String assertionValue) throws IOException {
109158
FormBody.Builder bodyBuilder = new FormBody.Builder();
110159
bodyBuilder.addEncoded("grant_type", grant);
111-
if (grant.equals(REFRESH_TOKEN)) {
112-
bodyBuilder.addEncoded(REFRESH_TOKEN, assertion);
113-
} else {
114-
bodyBuilder.addEncoded(ASSERTION, assertion);
115-
}
160+
String assertionKey = grant.equals(REFRESH_TOKEN) ? REFRESH_TOKEN : ASSERTION;
161+
bodyBuilder.addEncoded(assertionKey, assertionValue);
116162
FormBody body = bodyBuilder.build();
117163

118164
Request request = new Request.Builder()
@@ -123,14 +169,14 @@ private Call requestToken(String grant, String assertion) throws IOException {
123169
return httpClient.newCall(request);
124170
}
125171

126-
private String generateSelfSignedJWT() {
127-
RSAPrivateKey prvKey = saKey.getCredentials().getPrivateKeyParsed();
128-
Algorithm algorithm = null;
172+
private String generateSelfSignedJWT() throws InvalidKeySpecException, NoSuchAlgorithmException {
173+
RSAPrivateKey prvKey;
129174
try {
130-
algorithm = Algorithm.RSA512(prvKey);
131-
} catch (Exception e) {
132-
e.printStackTrace();
175+
prvKey = saKey.getCredentials().getPrivateKeyParsed();
176+
} catch (InvalidKeySpecException e) {
177+
throw new InvalidKeySpecException("could not parse private key", e);
133178
}
179+
Algorithm algorithm = Algorithm.RSA512(prvKey);
134180

135181
Map<String, Object> jwtHeader = new HashMap<>();
136182
jwtHeader.put("kid", saKey.getCredentials().getKid());

0 commit comments

Comments
 (0)