Skip to content

Commit b368002

Browse files
committed
moved authentication example from resourcemanager to example module
- run formatter - configured build.gradle for core module
1 parent 9517d9f commit b368002

File tree

21 files changed

+1327
-1186
lines changed

21 files changed

+1327
-1186
lines changed

core/build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11

2+
dependencies {
3+
implementation 'com.auth0:java-jwt:4.5.0'
4+
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
5+
implementation 'com.google.code.gson:gson:2.9.1'
6+
}

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

Lines changed: 0 additions & 3 deletions
This file was deleted.

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

Lines changed: 187 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
import com.google.gson.Gson;
99
import com.google.gson.JsonSyntaxException;
1010
import com.google.gson.annotations.SerializedName;
11-
import okhttp3.*;
12-
1311
import java.io.IOException;
1412
import java.io.InputStreamReader;
1513
import java.net.HttpURLConnection;
@@ -22,168 +20,193 @@
2220
import java.util.Map;
2321
import java.util.UUID;
2422
import java.util.concurrent.TimeUnit;
23+
import okhttp3.*;
2524

26-
/**
27-
* KeyFlowAuthenticator handles the Key Flow Authentication based on the Service Account Key.
28-
*/
25+
/** KeyFlowAuthenticator handles the Key Flow Authentication based on the Service Account Key. */
2926
public class KeyFlowAuthenticator {
30-
private final String REFRESH_TOKEN = "refresh_token";
31-
private final String ASSERTION = "assertion";
32-
private final String DEFAULT_TOKEN_ENDPOINT = "https://service-account.api.stackit.cloud/token";
33-
private final long DEFAULT_TOKEN_LEEWAY = 60;
34-
35-
private final OkHttpClient httpClient;
36-
private final ServiceAccountKey saKey;
37-
private KeyFlowTokenResponse token;
38-
private final Gson gson;
39-
private final String tokenUrl;
40-
private long tokenLeewayInSeconds = DEFAULT_TOKEN_LEEWAY;
41-
42-
private static class KeyFlowTokenResponse {
43-
@SerializedName("access_token")
44-
private String accessToken;
45-
@SerializedName("refresh_token")
46-
private String refreshToken;
47-
@SerializedName("expires_in")
48-
private long expiresIn;
49-
@SerializedName("scope")
50-
private String scope;
51-
@SerializedName("token_type")
52-
private String tokenType;
53-
54-
public boolean isExpired() {
55-
return expiresIn < new Date().toInstant().getEpochSecond();
56-
}
57-
58-
public String getAccessToken() {
59-
return accessToken;
60-
}
61-
}
62-
63-
/**
64-
* Creates the initial service account and refreshes expired access token.
65-
* @param cfg Configuration to set a custom token endpoint and the token expiration leeway.
66-
* @param saKey Service Account Key, which should be used for the authentication
67-
* @throws InvalidKeySpecException thrown when the private key in the service account can not be parsed
68-
* @throws IOException thrown on unexpected responses from the key flow
69-
* @throws ApiException thrown on unexpected responses from the key flow
70-
*/
71-
public KeyFlowAuthenticator(CoreConfiguration cfg, ServiceAccountKey saKey) throws InvalidKeySpecException, IOException, ApiException {
72-
this.saKey = saKey;
73-
this.gson = new Gson();
74-
this.httpClient = new OkHttpClient.Builder()
75-
.connectTimeout(10, TimeUnit.SECONDS)
76-
.writeTimeout(10, TimeUnit.SECONDS)
77-
.readTimeout(30, TimeUnit.SECONDS)
78-
.build();
79-
if (cfg.getTokenCustomUrl() != null && !cfg.getTokenCustomUrl().trim().isEmpty()) {
80-
this.tokenUrl = cfg.getTokenCustomUrl();
81-
} else {
82-
this.tokenUrl = DEFAULT_TOKEN_ENDPOINT;
83-
}
84-
if (cfg.getTokenExpirationLeeway() != null && cfg.getTokenExpirationLeeway() > 0) {
85-
this.tokenLeewayInSeconds = cfg.getTokenExpirationLeeway();
86-
}
87-
88-
createAccessToken();
89-
}
90-
91-
92-
/**
93-
* Returns access token. If the token is expired it creates a new token.
94-
* @throws IOException request for new access token failed
95-
* @throws ApiException response for new access token with bad status code
96-
*/
97-
public synchronized String getAccessToken() throws IOException, ApiException {
98-
if (token == null || token.isExpired()) {
99-
createAccessTokenWithRefreshToken();
100-
}
101-
return token.getAccessToken();
102-
}
103-
104-
/**
105-
* Creates the initial accessToken and stores it in `this.token`
106-
* @throws InvalidKeySpecException can not parse private key
107-
* @throws IOException request for access token failed
108-
* @throws ApiException response for new access token with bad status code
109-
* @throws JsonSyntaxException parsing of the created access token failed
110-
*/
111-
private void createAccessToken() throws InvalidKeySpecException, IOException, JsonSyntaxException, ApiException {
112-
String grant = "urn:ietf:params:oauth:grant-type:jwt-bearer";
113-
String assertion;
114-
try {
115-
assertion = generateSelfSignedJWT();
116-
} catch (NoSuchAlgorithmException e) {
117-
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);
118-
}
119-
Response response = requestToken(grant, assertion).execute();
120-
parseTokenResponse(response);
121-
response.close();
122-
}
123-
124-
/**
125-
* Creates a new access token with the existing refresh token
126-
* @throws IOException request for new access token failed
127-
* @throws ApiException response for new access token with bad status code
128-
* @throws JsonSyntaxException can not parse new access token
129-
*/
130-
private synchronized void createAccessTokenWithRefreshToken() throws IOException, JsonSyntaxException, ApiException {
131-
String refreshToken = token.refreshToken;
132-
Response response = requestToken(REFRESH_TOKEN, refreshToken).execute();
133-
parseTokenResponse(response);
134-
response.close();
135-
}
136-
137-
private synchronized void parseTokenResponse(Response response) throws ApiException, JsonSyntaxException {
138-
if (response.code() != HttpURLConnection.HTTP_OK) {
139-
String body = null;
140-
if (response.body() != null) {
141-
body = response.body().toString();
142-
response.body().close();
143-
}
144-
throw new ApiException(response.message(), response.code(), response.headers().toMultimap(), body);
145-
}
146-
if (response.body() == null) {
147-
throw new JsonSyntaxException("body from token creation is null");
148-
}
149-
150-
token = gson.fromJson(new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8), KeyFlowTokenResponse.class);
151-
token.expiresIn = JWT.decode(token.accessToken).getExpiresAt().toInstant().minusSeconds(tokenLeewayInSeconds).getEpochSecond();
152-
response.body().close();
153-
}
154-
155-
private Call requestToken(String grant, String assertionValue) throws IOException {
156-
FormBody.Builder bodyBuilder = new FormBody.Builder();
157-
bodyBuilder.addEncoded("grant_type", grant);
158-
String assertionKey = grant.equals(REFRESH_TOKEN) ? REFRESH_TOKEN : ASSERTION;
159-
bodyBuilder.addEncoded(assertionKey, assertionValue);
160-
FormBody body = bodyBuilder.build();
161-
162-
Request request = new Request.Builder()
163-
.url(tokenUrl)
164-
.post(body)
165-
.addHeader("Content-Type", "application/x-www-form-urlencoded")
166-
.build();
167-
return httpClient.newCall(request);
168-
}
169-
170-
private String generateSelfSignedJWT() throws InvalidKeySpecException, NoSuchAlgorithmException {
171-
RSAPrivateKey prvKey;
172-
173-
prvKey = saKey.getCredentials().getPrivateKeyParsed();
174-
Algorithm algorithm = Algorithm.RSA512(prvKey);
175-
176-
Map<String, Object> jwtHeader = new HashMap<>();
177-
jwtHeader.put("kid", saKey.getCredentials().getKid());
178-
179-
return JWT.create()
180-
.withIssuer(saKey.getCredentials().getIss())
181-
.withSubject(saKey.getCredentials().getSub())
182-
.withJWTId(UUID.randomUUID().toString())
183-
.withAudience(saKey.getCredentials().getAud())
184-
.withIssuedAt(new Date())
185-
.withExpiresAt(new Date().toInstant().plusSeconds(10 * 60))
186-
.withHeader(jwtHeader)
187-
.sign(algorithm);
188-
}
27+
private final String REFRESH_TOKEN = "refresh_token";
28+
private final String ASSERTION = "assertion";
29+
private final String DEFAULT_TOKEN_ENDPOINT = "https://service-account.api.stackit.cloud/token";
30+
private final long DEFAULT_TOKEN_LEEWAY = 60;
31+
32+
private final OkHttpClient httpClient;
33+
private final ServiceAccountKey saKey;
34+
private KeyFlowTokenResponse token;
35+
private final Gson gson;
36+
private final String tokenUrl;
37+
private long tokenLeewayInSeconds = DEFAULT_TOKEN_LEEWAY;
38+
39+
private static class KeyFlowTokenResponse {
40+
@SerializedName("access_token")
41+
private String accessToken;
42+
43+
@SerializedName("refresh_token")
44+
private String refreshToken;
45+
46+
@SerializedName("expires_in")
47+
private long expiresIn;
48+
49+
@SerializedName("scope")
50+
private String scope;
51+
52+
@SerializedName("token_type")
53+
private String tokenType;
54+
55+
public boolean isExpired() {
56+
return expiresIn < new Date().toInstant().getEpochSecond();
57+
}
58+
59+
public String getAccessToken() {
60+
return accessToken;
61+
}
62+
}
63+
64+
/**
65+
* Creates the initial service account and refreshes expired access token.
66+
*
67+
* @param cfg Configuration to set a custom token endpoint and the token expiration leeway.
68+
* @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
73+
*/
74+
public KeyFlowAuthenticator(CoreConfiguration cfg, ServiceAccountKey saKey)
75+
throws InvalidKeySpecException, IOException, ApiException {
76+
this.saKey = saKey;
77+
this.gson = new Gson();
78+
this.httpClient =
79+
new OkHttpClient.Builder()
80+
.connectTimeout(10, TimeUnit.SECONDS)
81+
.writeTimeout(10, TimeUnit.SECONDS)
82+
.readTimeout(30, TimeUnit.SECONDS)
83+
.build();
84+
if (cfg.getTokenCustomUrl() != null && !cfg.getTokenCustomUrl().trim().isEmpty()) {
85+
this.tokenUrl = cfg.getTokenCustomUrl();
86+
} else {
87+
this.tokenUrl = DEFAULT_TOKEN_ENDPOINT;
88+
}
89+
if (cfg.getTokenExpirationLeeway() != null && cfg.getTokenExpirationLeeway() > 0) {
90+
this.tokenLeewayInSeconds = cfg.getTokenExpirationLeeway();
91+
}
92+
93+
createAccessToken();
94+
}
95+
96+
/**
97+
* Returns access token. If the token is expired it creates a new token.
98+
*
99+
* @throws IOException request for new access token failed
100+
* @throws ApiException response for new access token with bad status code
101+
*/
102+
public synchronized String getAccessToken() throws IOException, ApiException {
103+
if (token == null || token.isExpired()) {
104+
createAccessTokenWithRefreshToken();
105+
}
106+
return token.getAccessToken();
107+
}
108+
109+
/**
110+
* Creates the initial accessToken and stores it in `this.token`
111+
*
112+
* @throws InvalidKeySpecException can not parse private key
113+
* @throws IOException request for access token failed
114+
* @throws ApiException response for new access token with bad status code
115+
* @throws JsonSyntaxException parsing of the created access token failed
116+
*/
117+
private void createAccessToken()
118+
throws InvalidKeySpecException, IOException, JsonSyntaxException, ApiException {
119+
String grant = "urn:ietf:params:oauth:grant-type:jwt-bearer";
120+
String assertion;
121+
try {
122+
assertion = generateSelfSignedJWT();
123+
} catch (NoSuchAlgorithmException e) {
124+
throw new RuntimeException(
125+
"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",
126+
e);
127+
}
128+
Response response = requestToken(grant, assertion).execute();
129+
parseTokenResponse(response);
130+
response.close();
131+
}
132+
133+
/**
134+
* Creates a new access token with the existing refresh token
135+
*
136+
* @throws IOException request for new access token failed
137+
* @throws ApiException response for new access token with bad status code
138+
* @throws JsonSyntaxException can not parse new access token
139+
*/
140+
private synchronized void createAccessTokenWithRefreshToken()
141+
throws IOException, JsonSyntaxException, ApiException {
142+
String refreshToken = token.refreshToken;
143+
Response response = requestToken(REFRESH_TOKEN, refreshToken).execute();
144+
parseTokenResponse(response);
145+
response.close();
146+
}
147+
148+
private synchronized void parseTokenResponse(Response response)
149+
throws ApiException, JsonSyntaxException {
150+
if (response.code() != HttpURLConnection.HTTP_OK) {
151+
String body = null;
152+
if (response.body() != null) {
153+
body = response.body().toString();
154+
response.body().close();
155+
}
156+
throw new ApiException(
157+
response.message(), response.code(), response.headers().toMultimap(), body);
158+
}
159+
if (response.body() == null) {
160+
throw new JsonSyntaxException("body from token creation is null");
161+
}
162+
163+
token =
164+
gson.fromJson(
165+
new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8),
166+
KeyFlowTokenResponse.class);
167+
token.expiresIn =
168+
JWT.decode(token.accessToken)
169+
.getExpiresAt()
170+
.toInstant()
171+
.minusSeconds(tokenLeewayInSeconds)
172+
.getEpochSecond();
173+
response.body().close();
174+
}
175+
176+
private Call requestToken(String grant, String assertionValue) throws IOException {
177+
FormBody.Builder bodyBuilder = new FormBody.Builder();
178+
bodyBuilder.addEncoded("grant_type", grant);
179+
String assertionKey = grant.equals(REFRESH_TOKEN) ? REFRESH_TOKEN : ASSERTION;
180+
bodyBuilder.addEncoded(assertionKey, assertionValue);
181+
FormBody body = bodyBuilder.build();
182+
183+
Request request =
184+
new Request.Builder()
185+
.url(tokenUrl)
186+
.post(body)
187+
.addHeader("Content-Type", "application/x-www-form-urlencoded")
188+
.build();
189+
return httpClient.newCall(request);
190+
}
191+
192+
private String generateSelfSignedJWT()
193+
throws InvalidKeySpecException, NoSuchAlgorithmException {
194+
RSAPrivateKey prvKey;
195+
196+
prvKey = saKey.getCredentials().getPrivateKeyParsed();
197+
Algorithm algorithm = Algorithm.RSA512(prvKey);
198+
199+
Map<String, Object> jwtHeader = new HashMap<>();
200+
jwtHeader.put("kid", saKey.getCredentials().getKid());
201+
202+
return JWT.create()
203+
.withIssuer(saKey.getCredentials().getIss())
204+
.withSubject(saKey.getCredentials().getSub())
205+
.withJWTId(UUID.randomUUID().toString())
206+
.withAudience(saKey.getCredentials().getAud())
207+
.withIssuedAt(new Date())
208+
.withExpiresAt(new Date().toInstant().plusSeconds(10 * 60))
209+
.withHeader(jwtHeader)
210+
.sign(algorithm);
211+
}
189212
}

0 commit comments

Comments
 (0)