Skip to content

Commit 43247d8

Browse files
committed
add prototype keyflow implementation
1 parent 4d16f1c commit 43247d8

File tree

5 files changed

+498
-0
lines changed

5 files changed

+498
-0
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package cloud.stackit.sdk.core;
2+
3+
4+
import java.util.List;
5+
import java.util.Map;
6+
7+
/**
8+
* <p>ApiException class.</p>
9+
*/
10+
public class ApiException extends Exception {
11+
private static final long serialVersionUID = 1L;
12+
13+
private int code = 0;
14+
private Map<String, List<String>> responseHeaders = null;
15+
private String responseBody = null;
16+
17+
/**
18+
* <p>Constructor for ApiException.</p>
19+
*/
20+
public ApiException() {}
21+
22+
/**
23+
* <p>Constructor for ApiException.</p>
24+
*
25+
* @param throwable a {@link java.lang.Throwable} object
26+
*/
27+
public ApiException(Throwable throwable) {
28+
super(throwable);
29+
}
30+
31+
/**
32+
* <p>Constructor for ApiException.</p>
33+
*
34+
* @param message the error message
35+
*/
36+
public ApiException(String message) {
37+
super(message);
38+
}
39+
40+
/**
41+
* <p>Constructor for ApiException.</p>
42+
*
43+
* @param message the error message
44+
* @param throwable a {@link java.lang.Throwable} object
45+
* @param code HTTP status code
46+
* @param responseHeaders a {@link java.util.Map} of HTTP response headers
47+
* @param responseBody the response body
48+
*/
49+
public ApiException(String message, Throwable throwable, int code, Map<String, List<String>> responseHeaders, String responseBody) {
50+
super(message, throwable);
51+
this.code = code;
52+
this.responseHeaders = responseHeaders;
53+
this.responseBody = responseBody;
54+
}
55+
56+
/**
57+
* <p>Constructor for ApiException.</p>
58+
*
59+
* @param message the error message
60+
* @param code HTTP status code
61+
* @param responseHeaders a {@link java.util.Map} of HTTP response headers
62+
* @param responseBody the response body
63+
*/
64+
public ApiException(String message, int code, Map<String, List<String>> responseHeaders, String responseBody) {
65+
this(message, null, code, responseHeaders, responseBody);
66+
}
67+
68+
/**
69+
* <p>Constructor for ApiException.</p>
70+
*
71+
* @param message the error message
72+
* @param throwable a {@link java.lang.Throwable} object
73+
* @param code HTTP status code
74+
* @param responseHeaders a {@link java.util.Map} of HTTP response headers
75+
*/
76+
public ApiException(String message, Throwable throwable, int code, Map<String, List<String>> responseHeaders) {
77+
this(message, throwable, code, responseHeaders, null);
78+
}
79+
80+
/**
81+
* <p>Constructor for ApiException.</p>
82+
*
83+
* @param code HTTP status code
84+
* @param responseHeaders a {@link java.util.Map} of HTTP response headers
85+
* @param responseBody the response body
86+
*/
87+
public ApiException(int code, Map<String, List<String>> responseHeaders, String responseBody) {
88+
this("Response Code: " + code + " Response Body: " + responseBody, null, code, responseHeaders, responseBody);
89+
}
90+
91+
/**
92+
* <p>Constructor for ApiException.</p>
93+
*
94+
* @param code HTTP status code
95+
* @param message a {@link java.lang.String} object
96+
*/
97+
public ApiException(int code, String message) {
98+
super(message);
99+
this.code = code;
100+
}
101+
102+
/**
103+
* <p>Constructor for ApiException.</p>
104+
*
105+
* @param code HTTP status code
106+
* @param message the error message
107+
* @param responseHeaders a {@link java.util.Map} of HTTP response headers
108+
* @param responseBody the response body
109+
*/
110+
public ApiException(int code, String message, Map<String, List<String>> responseHeaders, String responseBody) {
111+
this(code, message);
112+
this.responseHeaders = responseHeaders;
113+
this.responseBody = responseBody;
114+
}
115+
116+
/**
117+
* Get the HTTP status code.
118+
*
119+
* @return HTTP status code
120+
*/
121+
public int getCode() {
122+
return code;
123+
}
124+
125+
/**
126+
* Get the HTTP response headers.
127+
*
128+
* @return A map of list of string
129+
*/
130+
public Map<String, List<String>> getResponseHeaders() {
131+
return responseHeaders;
132+
}
133+
134+
/**
135+
* Get the HTTP response body.
136+
*
137+
* @return Response body in the form of string
138+
*/
139+
public String getResponseBody() {
140+
return responseBody;
141+
}
142+
143+
/**
144+
* Get the exception message including HTTP response data.
145+
*
146+
* @return The exception message
147+
*/
148+
public String getMessage() {
149+
return String.format("Message: %s%nHTTP response code: %s%nHTTP response body: %s%nHTTP response headers: %s",
150+
super.getMessage(), this.getCode(), this.getResponseBody(), this.getResponseHeaders());
151+
}
152+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package cloud.stackit.sdk.core;
2+
3+
import cloud.stackit.sdk.core.model.ServiceAccountCredentials;
4+
import cloud.stackit.sdk.core.model.ServiceAccountKey;
5+
import com.auth0.jwt.JWT;
6+
import com.auth0.jwt.algorithms.Algorithm;
7+
import com.google.gson.Gson;
8+
import com.google.gson.annotations.SerializedName;
9+
import okhttp3.*;
10+
11+
import java.io.IOException;
12+
import java.io.InputStream;
13+
import java.io.InputStreamReader;
14+
import java.net.HttpURLConnection;
15+
import java.nio.charset.StandardCharsets;
16+
import java.security.interfaces.RSAPrivateKey;
17+
import java.security.interfaces.RSAPublicKey;
18+
import java.util.Date;
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
import java.util.UUID;
22+
import java.util.concurrent.TimeUnit;
23+
24+
public class KeyFlowAuthenticator {
25+
private final String REFRESH_TOKEN = "refresh_token";
26+
private final String ASSERTION = "assertion";
27+
28+
private final OkHttpClient httpClient;
29+
private final ServiceAccountKey saKey;
30+
private KeyFlowTokenResponse token;
31+
private final Gson gson;
32+
private final String tokenUrl;
33+
34+
private static class KeyFlowTokenResponse {
35+
@SerializedName("access_token")
36+
private String accessToken;
37+
@SerializedName("refresh_token")
38+
private String refreshToken;
39+
@SerializedName("expires_in")
40+
private long expiresIn;
41+
@SerializedName("scope")
42+
private String scope;
43+
@SerializedName("token_type")
44+
private String tokenType;
45+
46+
public boolean isExpired() {
47+
return expiresIn < new Date().toInstant().minusSeconds(60).getEpochSecond();
48+
}
49+
50+
public String getAccessToken() {
51+
return accessToken;
52+
}
53+
}
54+
55+
public KeyFlowAuthenticator(ServiceAccountKey saKey) {
56+
this.saKey = saKey;
57+
this.gson = new Gson();
58+
this.httpClient = new OkHttpClient.Builder()
59+
.connectTimeout(10, TimeUnit.SECONDS)
60+
.writeTimeout(10, TimeUnit.SECONDS)
61+
.readTimeout(30, TimeUnit.SECONDS)
62+
.build();
63+
this.tokenUrl = "https://service-account.api.stackit.cloud/token";
64+
createAccessToken();
65+
}
66+
67+
68+
public synchronized String getAccessToken() throws IOException {
69+
if (token == null || token.isExpired()) {
70+
createAccessTokenWithRefreshToken();
71+
}
72+
return token.getAccessToken();
73+
}
74+
75+
private void createAccessToken() {
76+
String grant = "urn:ietf:params:oauth:grant-type:jwt-bearer";
77+
String assertion = generateSelfSignedJWT();
78+
try(Response response = requestToken(grant, assertion).execute()) {
79+
parseTokenResponse(response);
80+
} catch (IOException | ApiException e) {
81+
e.printStackTrace();
82+
}
83+
}
84+
85+
private synchronized void createAccessTokenWithRefreshToken() throws IOException {
86+
String refreshToken = token.refreshToken;
87+
try (Response response = requestToken(REFRESH_TOKEN, refreshToken).execute()) {
88+
parseTokenResponse(response);
89+
} catch (ApiException e) {
90+
e.printStackTrace();
91+
}
92+
}
93+
94+
private synchronized void parseTokenResponse(Response response) throws ApiException {
95+
if (response.code() != HttpURLConnection.HTTP_OK) {
96+
String body = null;
97+
if (response.body() != null) {
98+
body = response.body().toString();
99+
response.body().close();
100+
}
101+
throw new ApiException(response.message(), response.code(), response.headers().toMultimap(), body);
102+
}
103+
if (response.body() == null) {
104+
throw new ApiException("body from token creation is null");
105+
}
106+
107+
token = gson.fromJson(new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8), KeyFlowTokenResponse.class);
108+
token.expiresIn = JWT.decode(token.accessToken).getExpiresAt().toInstant().getEpochSecond();
109+
response.body().close();
110+
}
111+
112+
private Call requestToken(String grant, String assertion) throws IOException {
113+
FormBody.Builder bodyBuilder = new FormBody.Builder();
114+
bodyBuilder.addEncoded("grant_type", grant);
115+
if (grant.equals(REFRESH_TOKEN)) {
116+
bodyBuilder.addEncoded(REFRESH_TOKEN, assertion);
117+
} else {
118+
bodyBuilder.addEncoded(ASSERTION, assertion);
119+
}
120+
FormBody body = bodyBuilder.build();
121+
122+
Request request = new Request.Builder()
123+
.url(tokenUrl)
124+
.post(body)
125+
.addHeader("Content-Type", "application/x-www-form-urlencoded")
126+
.build();
127+
return httpClient.newCall(request);
128+
}
129+
130+
private String generateSelfSignedJWT() {
131+
RSAPrivateKey prvKey = saKey.getCredentials().getPrivateKeyParsed();
132+
Algorithm algorithm = null;
133+
try {
134+
algorithm = Algorithm.RSA512(prvKey);
135+
} catch (Exception e) {
136+
e.printStackTrace();
137+
}
138+
139+
Map<String, Object> jwtHeader = new HashMap<>();
140+
jwtHeader.put("kid", saKey.getCredentials().getKid());
141+
142+
return JWT.create()
143+
.withIssuer(saKey.getCredentials().getIss())
144+
.withSubject(saKey.getCredentials().getSub().toString())
145+
.withJWTId(UUID.randomUUID().toString())
146+
.withAudience(saKey.getCredentials().getAud())
147+
.withIssuedAt(new Date())
148+
.withExpiresAt(new Date().toInstant().plusSeconds(10 * 60))
149+
.withHeader(jwtHeader)
150+
.sign(algorithm);
151+
}
152+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package cloud.stackit.sdk.core.keyflow;
2+
3+
import cloud.stackit.sdk.core.KeyFlowAuthenticator;
4+
import okhttp3.Interceptor;
5+
import okhttp3.Request;
6+
import okhttp3.Response;
7+
8+
import java.io.IOException;
9+
10+
public class KeyFlowInterceptor implements Interceptor {
11+
private final KeyFlowAuthenticator authenticator;
12+
13+
public KeyFlowInterceptor(KeyFlowAuthenticator authenticator) {
14+
this.authenticator = authenticator;
15+
}
16+
17+
@Override
18+
public Response intercept(Chain chain) throws IOException {
19+
Request originalRequest = chain.request();
20+
String accessToken = authenticator.getAccessToken();
21+
22+
Request authenticatedRequest = originalRequest.newBuilder()
23+
.header("Authorization", "Bearer " + accessToken)
24+
.build();
25+
return chain.proceed(authenticatedRequest);
26+
}
27+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package cloud.stackit.sdk.core.model;
2+
3+
import java.security.KeyFactory;
4+
import java.security.NoSuchAlgorithmException;
5+
import java.security.PrivateKey;
6+
import java.security.interfaces.RSAPrivateKey;
7+
import java.security.spec.InvalidKeySpecException;
8+
import java.security.spec.PKCS8EncodedKeySpec;
9+
import java.util.Base64;
10+
import java.util.UUID;
11+
12+
public class ServiceAccountCredentials {
13+
private final String aud;
14+
private final String iss;
15+
private final String kid;
16+
private final String privateKey;
17+
private final UUID sub;
18+
19+
public ServiceAccountCredentials(String aud, String iss, String kid, String privateKey, UUID sub) {
20+
this.aud = aud;
21+
this.iss = iss;
22+
this.kid = kid;
23+
this.privateKey = privateKey;
24+
this.sub = sub;
25+
}
26+
27+
public String getAud() {
28+
return aud;
29+
}
30+
31+
public String getIss() {
32+
return iss;
33+
}
34+
35+
public String getKid() {
36+
return kid;
37+
}
38+
39+
public String getPrivateKey() {
40+
return privateKey;
41+
}
42+
43+
public UUID getSub() {
44+
return sub;
45+
}
46+
47+
public RSAPrivateKey getPrivateKeyParsed() {
48+
RSAPrivateKey prvKey = null;
49+
try {
50+
String trimmedKey = privateKey.replaceFirst("-----BEGIN PRIVATE KEY-----", "");
51+
trimmedKey = trimmedKey.replaceFirst("-----END PRIVATE KEY-----", "");
52+
trimmedKey = trimmedKey.replaceAll("\n","");
53+
54+
byte[] privateBytes = Base64.getDecoder().decode(trimmedKey);
55+
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateBytes);
56+
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
57+
prvKey = (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
58+
} catch (InvalidKeySpecException e) {
59+
e.printStackTrace();
60+
} catch (NoSuchAlgorithmException e) {
61+
System.out.println(e);
62+
}
63+
return prvKey;
64+
}
65+
}

0 commit comments

Comments
 (0)