44import cloud .stackit .sdk .core .config .CoreConfiguration ;
55import cloud .stackit .sdk .core .config .EnvironmentVariables ;
66import cloud .stackit .sdk .core .exception .ApiException ;
7+ import cloud .stackit .sdk .core .exception .AuthenticationException ;
78import cloud .stackit .sdk .core .model .ServiceAccountKey ;
89import cloud .stackit .sdk .core .utils .Utils ;
910import com .auth0 .jwt .JWT ;
1920import java .security .interfaces .RSAPrivateKey ;
2021import java .security .spec .InvalidKeySpecException ;
2122import java .util .Date ;
22- import java .util .HashMap ;
2323import java .util .Map ;
2424import java .util .UUID ;
25+ import java .util .concurrent .ConcurrentHashMap ;
2526import java .util .concurrent .TimeUnit ;
2627import okhttp3 .*;
2728import org .jetbrains .annotations .NotNull ;
2829
29- /* KeyFlowAuthenticator handles the Key Flow Authentication based on the Service Account Key. */
30+ /*
31+ * KeyFlowAuthenticator handles the Key Flow Authentication based on the Service Account Key.
32+ */
3033public class KeyFlowAuthenticator implements Authenticator {
3134 private static final String REFRESH_TOKEN = "refresh_token" ;
3235 private static final String ASSERTION = "assertion" ;
@@ -44,6 +47,8 @@ public class KeyFlowAuthenticator implements Authenticator {
4447 private final String tokenUrl ;
4548 private long tokenLeewayInSeconds = DEFAULT_TOKEN_LEEWAY ;
4649
50+ private final Object tokenRefreshMonitor = new Object ();
51+
4752 /**
4853 * Creates the initial service account and refreshes expired access token.
4954 *
@@ -128,7 +133,7 @@ public Request authenticate(Route route, @NotNull Response response) throws IOEx
128133 try {
129134 accessToken = getAccessToken ();
130135 } catch (ApiException | InvalidKeySpecException e ) {
131- throw new RuntimeException ( e );
136+ throw new AuthenticationException ( "Failed to obtain access token" , e );
132137 }
133138
134139 // Return a new request with the refreshed token
@@ -140,19 +145,19 @@ public Request authenticate(Route route, @NotNull Response response) throws IOEx
140145
141146 protected static class KeyFlowTokenResponse {
142147 @ SerializedName ("access_token" )
143- private String accessToken ;
148+ private final String accessToken ;
144149
145150 @ SerializedName ("refresh_token" )
146- private String refreshToken ;
151+ private final String refreshToken ;
147152
148153 @ SerializedName ("expires_in" )
149154 private long expiresIn ;
150155
151156 @ SerializedName ("scope" )
152- private String scope ;
157+ private final String scope ;
153158
154159 @ SerializedName ("token_type" )
155- private String tokenType ;
160+ private final String tokenType ;
156161
157162 public KeyFlowTokenResponse (
158163 String accessToken ,
@@ -184,14 +189,16 @@ protected String getAccessToken() {
184189 * @throws IOException request for new access token failed
185190 * @throws ApiException response for new access token with bad status code
186191 */
187- public synchronized String getAccessToken ()
188- throws IOException , ApiException , InvalidKeySpecException {
189- if (token == null ) {
190- createAccessToken ();
191- } else if (token .isExpired ()) {
192- createAccessTokenWithRefreshToken ();
192+ @ SuppressWarnings ("PMD.AvoidSynchronizedStatement" )
193+ public String getAccessToken () throws IOException , ApiException , InvalidKeySpecException {
194+ synchronized (tokenRefreshMonitor ) {
195+ if (token == null ) {
196+ createAccessToken ();
197+ } else if (token .isExpired ()) {
198+ createAccessTokenWithRefreshToken ();
199+ }
200+ return token .getAccessToken ();
193201 }
194- return token .getAccessToken ();
195202 }
196203
197204 /**
@@ -202,20 +209,23 @@ public synchronized String getAccessToken()
202209 * @throws ApiException response for new access token with bad status code
203210 * @throws JsonSyntaxException parsing of the created access token failed
204211 */
205- protected void createAccessToken ()
206- throws InvalidKeySpecException , IOException , JsonSyntaxException , ApiException {
207- String grant = "urn:ietf:params:oauth:grant-type:jwt-bearer" ;
208- String assertion ;
209- try {
210- assertion = generateSelfSignedJWT ();
211- } catch (NoSuchAlgorithmException e ) {
212- throw new RuntimeException (
213- "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" ,
214- e );
212+ @ SuppressWarnings ("PMD.AvoidSynchronizedStatement" )
213+ protected void createAccessToken () throws InvalidKeySpecException , IOException , ApiException {
214+ synchronized (tokenRefreshMonitor ) {
215+ String assertion ;
216+ try {
217+ assertion = generateSelfSignedJWT ();
218+ } catch (NoSuchAlgorithmException e ) {
219+ throw new AuthenticationException (
220+ "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" ,
221+ e );
222+ }
223+
224+ String grant = "urn:ietf:params:oauth:grant-type:jwt-bearer" ;
225+ try (Response response = requestToken (grant , assertion ).execute ()) {
226+ parseTokenResponse (response );
227+ }
215228 }
216- Response response = requestToken (grant , assertion ).execute ();
217- parseTokenResponse (response );
218- response .close ();
219229 }
220230
221231 /**
@@ -225,16 +235,24 @@ protected void createAccessToken()
225235 * @throws ApiException response for new access token with bad status code
226236 * @throws JsonSyntaxException can not parse new access token
227237 */
228- protected synchronized void createAccessTokenWithRefreshToken ()
229- throws IOException , JsonSyntaxException , ApiException {
230- String refreshToken = token .refreshToken ;
231- Response response = requestToken (REFRESH_TOKEN , refreshToken ).execute ();
232- parseTokenResponse (response );
233- response .close ();
238+ @ SuppressWarnings ("PMD.AvoidSynchronizedStatement" )
239+ protected void createAccessTokenWithRefreshToken () throws IOException , ApiException {
240+ synchronized (tokenRefreshMonitor ) {
241+ String refreshToken = token .refreshToken ;
242+ try (Response response = requestToken (REFRESH_TOKEN , refreshToken ).execute ()) {
243+ parseTokenResponse (response );
244+ }
245+ }
234246 }
235247
236- private synchronized void parseTokenResponse (Response response )
237- throws ApiException , JsonSyntaxException , IOException {
248+ /**
249+ * Parses the token response from the server
250+ *
251+ * @param response HTTP response containing the token
252+ * @throws ApiException if the response has a bad status code
253+ * @throws JsonSyntaxException if the response body cannot be parsed
254+ */
255+ private void parseTokenResponse (Response response ) throws ApiException {
238256 if (response .code () != HttpURLConnection .HTTP_OK ) {
239257 String body = null ;
240258 if (response .body () != null ) {
@@ -256,10 +274,10 @@ private synchronized void parseTokenResponse(Response response)
256274 response .body ().close ();
257275 }
258276
259- private Call requestToken (String grant , String assertionValue ) throws IOException {
277+ private Call requestToken (String grant , String assertionValue ) {
260278 FormBody .Builder bodyBuilder = new FormBody .Builder ();
261279 bodyBuilder .addEncoded ("grant_type" , grant );
262- String assertionKey = grant .equals (REFRESH_TOKEN ) ? REFRESH_TOKEN : ASSERTION ;
280+ String assertionKey = REFRESH_TOKEN .equals (grant ) ? REFRESH_TOKEN : ASSERTION ;
263281 bodyBuilder .addEncoded (assertionKey , assertionValue );
264282 FormBody body = bodyBuilder .build ();
265283
@@ -289,7 +307,7 @@ private String generateSelfSignedJWT()
289307 prvKey = saKey .getCredentials ().getPrivateKeyParsed ();
290308 Algorithm algorithm = Algorithm .RSA512 (prvKey );
291309
292- Map <String , Object > jwtHeader = new HashMap <>();
310+ Map <String , Object > jwtHeader = new ConcurrentHashMap <>();
293311 jwtHeader .put ("kid" , saKey .getCredentials ().getKid ());
294312
295313 return JWT .create ()
0 commit comments