Skip to content
Merged
Prev Previous commit
Next Next commit
review feedback removed try-catch-blocks
  • Loading branch information
marceljk committed Aug 1, 2025
commit bbb974f911ea562a6a263e66d7bf5335a2f2820f
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cloud.stackit.sdk.core;

import cloud.stackit.sdk.core.config.CoreConfiguration;
import cloud.stackit.sdk.core.exception.ApiException;
import cloud.stackit.sdk.core.model.ServiceAccountKey;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
Expand Down Expand Up @@ -63,10 +64,11 @@ public String getAccessToken() {
* Creates the initial service account and refreshes expired access token.
* @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
* @throws InvalidKeySpecException Throws, when the private key in the service account can not be parsed
* @throws IOException Throws, when on unexpected responses from the key flow
* @throws InvalidKeySpecException thrown when the private key in the service account can not be parsed
* @throws IOException thrown on unexpected responses from the key flow
* @throws ApiException thrown on unexpected responses from the key flow
*/
public KeyFlowAuthenticator(CoreConfiguration cfg, ServiceAccountKey saKey) throws InvalidKeySpecException, IOException {
public KeyFlowAuthenticator(CoreConfiguration cfg, ServiceAccountKey saKey) throws InvalidKeySpecException, IOException, ApiException {
this.saKey = saKey;
this.gson = new Gson();
this.httpClient = new OkHttpClient.Builder()
Expand All @@ -86,50 +88,50 @@ public KeyFlowAuthenticator(CoreConfiguration cfg, ServiceAccountKey saKey) thro
createAccessToken();
}

public synchronized String getAccessToken() throws IOException {

/**
* Returns access token. If the token is expired it creates a new token.
* @throws IOException request for new access token failed
* @throws ApiException response for new access token with bad status code
*/
public synchronized String getAccessToken() throws IOException, ApiException {
if (token == null || token.isExpired()) {
createAccessTokenWithRefreshToken();
}
return token.getAccessToken();
}

/**
* Creates the inital accessToken and stores it in `this.token`
* Creates the initial accessToken and stores it in `this.token`
* @throws InvalidKeySpecException can not parse private key
* @throws IOException request for access token failed
* @throws ApiException response for new access token with bad status code
* @throws JsonSyntaxException parsing of the created access token failed
*/
private void createAccessToken() throws InvalidKeySpecException, IOException, JsonSyntaxException {
private void createAccessToken() throws InvalidKeySpecException, IOException, JsonSyntaxException, ApiException {
String grant = "urn:ietf:params:oauth:grant-type:jwt-bearer";
String assertion;
try {
assertion = generateSelfSignedJWT();
} catch (NoSuchAlgorithmException e) {
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);
}
try(Response response = requestToken(grant, assertion).execute()) {
parseTokenResponse(response);
} catch (IOException | ApiException e) {
throw new IOException("request for access token failed", e);
} catch (JsonSyntaxException e) {
throw new JsonSyntaxException("parsing access token failed", e);
Response response = requestToken(grant, assertion).execute();
parseTokenResponse(response);
response.close();
}
}

/**
* Creates a new access token with the existing refresh token
* @throws IOException request for new access token failed
* @throws ApiException response for new access token with bad status code
* @throws JsonSyntaxException can not parse new access token
*/
private synchronized void createAccessTokenWithRefreshToken() throws IOException, JsonSyntaxException {
private synchronized void createAccessTokenWithRefreshToken() throws IOException, JsonSyntaxException, ApiException {
String refreshToken = token.refreshToken;
try (Response response = requestToken(REFRESH_TOKEN, refreshToken).execute()) {
parseTokenResponse(response);
} catch (IOException | ApiException e) {
throw new IOException("request for new access token failed", e);
} catch (JsonSyntaxException e) {
throw new JsonSyntaxException("parsing refreshed access token failed", e);
}
Response response = requestToken(REFRESH_TOKEN, refreshToken).execute();
parseTokenResponse(response);
response.close();
}

private synchronized void parseTokenResponse(Response response) throws ApiException, JsonSyntaxException {
Expand All @@ -145,13 +147,9 @@ private synchronized void parseTokenResponse(Response response) throws ApiExcept
throw new JsonSyntaxException("body from token creation is null");
}

try {
token = gson.fromJson(new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8), KeyFlowTokenResponse.class);
token.expiresIn = JWT.decode(token.accessToken).getExpiresAt().toInstant().minusSeconds(tokenLeewayInSeconds).getEpochSecond();
response.body().close();
} catch (JsonSyntaxException e) {
throw new JsonSyntaxException("could not parse response of created token", e);
}
token = gson.fromJson(new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8), KeyFlowTokenResponse.class);
token.expiresIn = JWT.decode(token.accessToken).getExpiresAt().toInstant().minusSeconds(tokenLeewayInSeconds).getEpochSecond();
response.body().close();
}

private Call requestToken(String grant, String assertionValue) throws IOException {
Expand All @@ -171,11 +169,8 @@ private Call requestToken(String grant, String assertionValue) throws IOExceptio

private String generateSelfSignedJWT() throws InvalidKeySpecException, NoSuchAlgorithmException {
RSAPrivateKey prvKey;
try {
prvKey = saKey.getCredentials().getPrivateKeyParsed();
} catch (InvalidKeySpecException e) {
throw new InvalidKeySpecException("could not parse private key", e);
}

prvKey = saKey.getCredentials().getPrivateKeyParsed();
Algorithm algorithm = Algorithm.RSA512(prvKey);

Map<String, Object> jwtHeader = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package cloud.stackit.sdk.core;

import cloud.stackit.sdk.core.exception.ApiException;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
Expand All @@ -18,7 +19,14 @@ public KeyFlowInterceptor(KeyFlowAuthenticator authenticator) {
@Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
String accessToken = authenticator.getAccessToken();
String accessToken;
try {
accessToken = authenticator.getAccessToken();
} catch (ApiException e) {
// try-catch required, because ApiException can not be thrown in the implementation
// of Interceptor.intercept(Chain chain)
throw new RuntimeException(e);
}

Request authenticatedRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer " + accessToken)
Expand Down
35 changes: 20 additions & 15 deletions core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package cloud.stackit.sdk.core.auth;

import cloud.stackit.sdk.core.exception.ApiException;
import cloud.stackit.sdk.core.KeyFlowAuthenticator;
import cloud.stackit.sdk.core.config.CoreConfiguration;
import cloud.stackit.sdk.core.config.EnvironmentVariables;
import cloud.stackit.sdk.core.KeyFlowInterceptor;
import cloud.stackit.sdk.core.exception.CredentialsInFileNotFoundException;
import cloud.stackit.sdk.core.exception.PrivateKeyNotFoundException;
import cloud.stackit.sdk.core.model.ServiceAccountKey;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
Expand Down Expand Up @@ -32,22 +35,24 @@ public class SetupAuth {
/**
* Set up the KeyFlow Authentication and can be integrated in an OkHttp client, by adding `SetupAuth().getAuthHandler()` as interceptor.
* This relies on the configuration methods via ENVs or the credentials file in `$HOME/.stackit/credentials.json`
* @throws IOException when no file can be found
* @throws IOException when a file can be found
* @throws CredentialNotFoundException when no configuration is set or can be found
* @throws InvalidKeySpecException when the private key can not be parsed
* @throws ApiException when access token creation failed
*/
public SetupAuth() throws IOException, InvalidKeySpecException, CredentialNotFoundException {
public SetupAuth() throws IOException, InvalidKeySpecException, CredentialNotFoundException, ApiException {
this(new CoreConfiguration.Builder().build());
}

/**
* Set up the KeyFlow Authentication and can be integrated in an OkHttp client, by adding `SetupAuth().getAuthHandler()` as interceptor.
* @param cfg Configuration which describes, which service account and token endpoint should be used
* @throws IOException when no file can be found
* @throws CredentialNotFoundException when no configuration is set or can be found
* @throws IOException when a file can be found
* @throws CredentialsInFileNotFoundException when no credentials are set or can be found
* @throws InvalidKeySpecException when the private key can not be parsed
* @throws ApiException when access token creation failed
*/
public SetupAuth(CoreConfiguration cfg) throws IOException, CredentialNotFoundException, InvalidKeySpecException {
public SetupAuth(CoreConfiguration cfg) throws IOException, CredentialsInFileNotFoundException, InvalidKeySpecException, ApiException {
if (cfg == null) {
cfg = new CoreConfiguration.Builder().build();
}
Expand Down Expand Up @@ -90,10 +95,10 @@ public Interceptor getAuthHandler() {
* </ol>
* @param cfg
* @return ServiceAccountKey
* @throws CredentialNotFoundException throws error when no service account key or private key can be found
* @throws IOException throws an error if a file can not be found
* @throws CredentialsInFileNotFoundException thrown when no service account key or private key can be found
* @throws IOException thrown when a file can not be found
*/
private ServiceAccountKey setupKeyFlow(CoreConfiguration cfg) throws CredentialNotFoundException, IOException {
private ServiceAccountKey setupKeyFlow(CoreConfiguration cfg) throws CredentialsInFileNotFoundException, IOException {
// Explicit config in code
if (cfg.getServiceAccountKey() != null && !cfg.getServiceAccountKey().trim().isEmpty()) {
ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(cfg.getServiceAccountKey());
Expand Down Expand Up @@ -135,13 +140,13 @@ private ServiceAccountKey setupKeyFlow(CoreConfiguration cfg) throws CredentialN
}
}

private void loadPrivateKey(CoreConfiguration cfg, ServiceAccountKey saKey) throws CredentialNotFoundException {
private void loadPrivateKey(CoreConfiguration cfg, ServiceAccountKey saKey) throws PrivateKeyNotFoundException {
if (!saKey.getCredentials().isPrivateKeySet()) {
try {
String privateKey = getPrivateKey(cfg);
saKey.getCredentials().setPrivateKey(privateKey);
} catch (Exception e) {
throw new CredentialNotFoundException("could not find private key\n" + e.getMessage());
throw new PrivateKeyNotFoundException("could not find private key", e);
}
}
}
Expand Down Expand Up @@ -175,10 +180,10 @@ private void loadPrivateKey(CoreConfiguration cfg, ServiceAccountKey saKey) thro
* </ol>
* @param cfg
* @return found private key
* @throws CredentialNotFoundException throws if no private key could be found
* @throws CredentialsInFileNotFoundException throws if no private key could be found
* @throws IOException throws if the provided path can not be found or the file within the pathKey can not be found
*/
private String getPrivateKey(CoreConfiguration cfg) throws CredentialNotFoundException, IOException {
private String getPrivateKey(CoreConfiguration cfg) throws CredentialsInFileNotFoundException, IOException {
// Explicit code config
// Set private key
if (cfg.getPrivateKey() != null && !cfg.getPrivateKey().trim().isEmpty()) {
Expand Down Expand Up @@ -215,10 +220,10 @@ private String getPrivateKey(CoreConfiguration cfg) throws CredentialNotFoundExc
* @param valueKey key which contains the secret as value
* @param pathKey key which contains a path to a file
* @return Either the value of `valueKey` or the content of the file in `pathKey`
* @throws CredentialNotFoundException throws if no value was found in the credentials file
* @throws CredentialsInFileNotFoundException throws if no value was found in the credentials file
* @throws IOException throws if the provided path can not be found or the file within the pathKey can not be found
*/
private String readValueFromCredentialsFile(String path, String valueKey, String pathKey) throws IOException, CredentialNotFoundException {
private String readValueFromCredentialsFile(String path, String valueKey, String pathKey) throws IOException, CredentialsInFileNotFoundException {
// Read credentials file
String fileContent = new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8);
Type credentialsFileType = new TypeToken<Map<String, Object>>(){}.getType();
Expand All @@ -241,6 +246,6 @@ private String readValueFromCredentialsFile(String path, String valueKey, String
if (keyPath != null && !keyPath.trim().isEmpty()) {
return new String(Files.readAllBytes(Paths.get(keyPath)));
}
throw new CredentialNotFoundException("could not find " + valueKey + " or " + pathKey + " in " + path);
throw new CredentialsInFileNotFoundException("could not find " + valueKey + " or " + pathKey + " in " + path);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package cloud.stackit.sdk.core;
package cloud.stackit.sdk.core.exception;


import java.util.List;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package cloud.stackit.sdk.core.exception;

public class CredentialsInFileNotFoundException extends RuntimeException {

public CredentialsInFileNotFoundException(String msg) {
super(msg);
}

public CredentialsInFileNotFoundException(String msg, Throwable cause) {
super(msg, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package cloud.stackit.sdk.core.exception;

public class PrivateKeyNotFoundException extends RuntimeException {

public PrivateKeyNotFoundException(String msg) {
super(msg);
}

public PrivateKeyNotFoundException(String msg, Throwable cause) {
super(msg, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,10 @@ public RSAPublicKey getPublicKeyParsed() throws NoSuchAlgorithmException, Invali
byte[] publicBytes = Base64.getDecoder().decode(trimmedKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPublicKey pubKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);
return pubKey;
return (RSAPublicKey) keyFactory.generatePublic(keySpec);
}

public static ServiceAccountKey loadFromJson(String json) throws com.google.gson.JsonSyntaxException {
public static ServiceAccountKey loadFromJson(String json) throws JsonSyntaxException {
ServiceAccountKey saKey = new Gson().fromJson(json, ServiceAccountKey.class);
if (!saKey.isCredentialsSet()) {
throw new JsonSyntaxException("required field `credentials` in service account key is missing.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public ApiClient(OkHttpClient client) {
authentications = Collections.unmodifiableMap(authentications);
}

public ApiClient(CoreConfiguration config) throws IOException, InvalidKeySpecException, CredentialNotFoundException {
public ApiClient(CoreConfiguration config) throws IOException, InvalidKeySpecException, cloud.stackit.sdk.core.exception.ApiException {
init();

if (config.getCustomEndpoint() != null && !config.getCustomEndpoint().trim().isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ public DefaultApi(ApiClient apiClient) {
this.localVarApiClient = apiClient;
}

public DefaultApi(CoreConfiguration config) throws IOException, InvalidKeySpecException, CredentialNotFoundException {
// TODO: remove in follow up story the service specific ApiException and use instead the ApiException of core
public DefaultApi(CoreConfiguration config) throws IOException, InvalidKeySpecException, cloud.stackit.sdk.core.exception.ApiException {
if (config.getCustomEndpoint() != null && !config.getCustomEndpoint().trim().isEmpty()) {
localCustomBaseUrl = config.getCustomEndpoint();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import java.security.spec.InvalidKeySpecException;

public class main {
public static void main(String[] args) {
public static void main(String[] args) throws IOException, InvalidKeySpecException, ApiException, cloud.stackit.sdk.core.exception.ApiException {
String SERVICE_ACCOUNT_KEY_PATH = "/path/to/your/sa/key.json";
String SERIVCE_ACCOUNT_MAIL = "name-1234@sa.stackit.cloud";

Expand Down