11package cloud .stackit .sdk .core ;
22
3+ import cloud .stackit .sdk .core .config .Configuration ;
34import cloud .stackit .sdk .core .model .ServiceAccountKey ;
45import com .auth0 .jwt .JWT ;
56import com .auth0 .jwt .algorithms .Algorithm ;
67import com .google .gson .Gson ;
8+ import com .google .gson .JsonSyntaxException ;
79import com .google .gson .annotations .SerializedName ;
810import okhttp3 .*;
911
1012import java .io .IOException ;
1113import java .io .InputStreamReader ;
1214import java .net .HttpURLConnection ;
1315import java .nio .charset .StandardCharsets ;
16+ import java .security .NoSuchAlgorithmException ;
1417import java .security .interfaces .RSAPrivateKey ;
18+ import java .security .spec .InvalidKeySpecException ;
1519import java .util .Date ;
1620import java .util .HashMap ;
1721import java .util .Map ;
1822import java .util .UUID ;
1923import java .util .concurrent .TimeUnit ;
2024
25+ /**
26+ * KeyFlowAuthenticator handles the Key Flow Authentication based on the Service Account Key.
27+ */
2128public 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