@@ -15,6 +15,7 @@ import com.google.common.util.concurrent.UncheckedExecutionException
1515import com.google.gson.Gson
1616import com.google.gson.JsonSyntaxException
1717import dev.failsafe.Failsafe
18+ import dev.failsafe.FailsafeException
1819import dev.failsafe.RetryPolicy
1920import dev.failsafe.event.EventListener
2021import dev.failsafe.event.ExecutionAttemptedEvent
@@ -61,6 +62,8 @@ class TowerFusionToken implements FusionToken {
6162 private static final int DEFAULT_RETRY_POLICY_MAX_ATTEMPTS = 10
6263 private static final double DEFAULT_RETRY_POLICY_JITTER = 0.5
6364
65+ private CookieManager cookieManager = new CookieManager ()
66+
6467 // The HttpClient instance used to send requests
6568 private final HttpClient httpClient = newDefaultHttpClient()
6669
@@ -79,7 +82,9 @@ class TowerFusionToken implements FusionToken {
7982 private String endpoint
8083
8184 // Platform access token to use for requests
82- private String accessToken
85+ private volatile String accessToken
86+
87+ private volatile String refreshToken
8388
8489 // Platform workflowId
8590 private String workspaceId
@@ -92,6 +97,7 @@ class TowerFusionToken implements FusionToken {
9297 final env = SysEnv . get()
9398 this . endpoint = PlatformHelper . getEndpoint(config, env)
9499 this . accessToken = PlatformHelper . getAccessToken(config, env)
100+ this . refreshToken = PlatformHelper . getRefreshToken(config, env)
95101 this . workflowId = env. get(' TOWER_WORKFLOW_ID' )
96102 this . workspaceId = PlatformHelper . getWorkspaceId(config, env)
97103 }
@@ -181,11 +187,11 @@ class TowerFusionToken implements FusionToken {
181187 * Create a new HttpClient instance with default settings
182188 * @return The new HttpClient instance
183189 */
184- private static HttpClient newDefaultHttpClient () {
190+ private HttpClient newDefaultHttpClient () {
185191 final builder = HttpClient . newBuilder()
186192 .version(HttpClient.Version . HTTP_1_1 )
187193 .followRedirects(HttpClient.Redirect . NEVER )
188- .cookieHandler(new CookieManager () )
194+ .cookieHandler(cookieManager )
189195 .connectTimeout(DEFAULT_CONNECTION_TIMEOUT )
190196 // use virtual threads executor if enabled
191197 if ( Threads . useVirtual() ) {
@@ -235,8 +241,17 @@ class TowerFusionToken implements FusionToken {
235241 * @param req The HttpRequest to send
236242 * @return The HttpResponse received
237243 */
238- private <T> HttpResponse<String > safeHttpSend (HttpRequest req , RetryPolicy<T> policy ) {
239- return Failsafe . with(policy). get(
244+ private <T> HttpResponse<String > safeHttpSend (HttpRequest req ) {
245+ try {
246+ safeApply(req)
247+ }
248+ catch (FailsafeException e) {
249+ throw e. cause
250+ }
251+ }
252+
253+ private <T> HttpResponse<String > safeApply (HttpRequest req ) {
254+ return Failsafe . with(retryPolicy). get(
240255 () -> {
241256 log. debug " Http request: method=${ req.method()} ; uri=${ req.uri()} ; request=${ req} "
242257 final resp = httpClient. send(req, HttpResponse.BodyHandlers . ofString())
@@ -287,28 +302,35 @@ class TowerFusionToken implements FusionToken {
287302 /**
288303 * Request a license token from Platform.
289304 *
290- * @param req The LicenseTokenRequest object
305+ * @param request The LicenseTokenRequest object
291306 * @return The LicenseTokenResponse object
292- *
293- * @throws AbortOperationException if a Platform access token cannot be found
294- * @throws UnauthorizedException if the access token is invalid
295- * @throws BadResponseException if the response is not as expected
296- * @throws IllegalStateException if the request cannot be sent
297307 */
298- private GetLicenseTokenResponse sendRequest (GetLicenseTokenRequest req ) throws AbortOperationException , UnauthorizedException , BadResponseException , IllegalStateException {
308+ private GetLicenseTokenResponse sendRequest (GetLicenseTokenRequest request ) {
309+ return sendRequest0(request, 1 )
310+ }
299311
300- final httpReq = makeHttpRequest(req)
312+ private GetLicenseTokenResponse sendRequest0 (GetLicenseTokenRequest request , int attempt ) {
313+
314+ final httpReq = makeHttpRequest(request)
301315
302316 try {
303- final resp = safeHttpSend(httpReq, retryPolicy )
317+ final resp = safeHttpSend(httpReq)
304318
305319 if ( resp. statusCode() == 200 ) {
306320 final ret = parseLicenseTokenResponse(resp. body())
307321 return ret
308322 }
309323
310324 if ( resp. statusCode() == 401 ) {
311- throw new UnauthorizedException (" Unauthorized [401] - Verify you have provided a valid access token" )
325+ final shouldRetry = accessToken
326+ && refreshToken
327+ && attempt== 1
328+ && refreshJwtToken0(refreshToken)
329+ if ( shouldRetry ) {
330+ return sendRequest0(request, attempt+1 )
331+ }
332+ else
333+ throw new UnauthorizedException (" Unauthorized [401] - Verify you have provided a valid Seqera Platform access token" )
312334 }
313335
314336 throw new BadResponseException (" Invalid response: ${ httpReq.method()} ${ httpReq.uri()} [${ resp.statusCode()} ] ${ resp.body()} " )
@@ -317,4 +339,52 @@ class TowerFusionToken implements FusionToken {
317339 throw new IllegalStateException (" Unable to send request to '${ httpReq.uri()} ' : ${ e.message} " )
318340 }
319341 }
342+
343+ protected boolean refreshJwtToken0 (String refresh ) {
344+ log. debug " Token refresh request >> $refresh "
345+
346+ final req = HttpRequest . newBuilder()
347+ .uri(new URI (" ${ endpoint} /oauth/access_token" ))
348+ .headers(' Content-Type' ," application/x-www-form-urlencoded" )
349+ .POST (HttpRequest.BodyPublishers . ofString(" grant_type=refresh_token&refresh_token=${ URLEncoder.encode(refresh, 'UTF-8')} " ))
350+ .build()
351+
352+ final resp = safeHttpSend(req)
353+ final code = resp. statusCode()
354+ final body = resp. body()
355+ log. debug " Refresh cookie response: [${ code} ] ${ body} "
356+ if ( resp. statusCode() != 200 )
357+ return false
358+
359+ final authCookie = getCookie(' JWT' )
360+ final refreshCookie = getCookie(' JWT_REFRESH_TOKEN' )
361+
362+ // set the new bearer token in the current client session
363+ if ( authCookie?. value ) {
364+ log. trace " Updating http client bearer token=$authCookie . value "
365+ accessToken = authCookie. value
366+ }
367+ else {
368+ log. warn " Missing JWT cookie from refresh token response ~ $authCookie "
369+ }
370+
371+ // set the new refresh token
372+ if ( refreshCookie?. value ) {
373+ log. trace " Updating http client refresh token=$refreshCookie . value "
374+ refreshToken = refreshCookie. value
375+ }
376+ else {
377+ log. warn " Missing JWT_REFRESH_TOKEN cookie from refresh token response ~ $refreshCookie "
378+ }
379+
380+ return true
381+ }
382+
383+ private HttpCookie getCookie (final String cookieName ) {
384+ for ( HttpCookie it : cookieManager. cookieStore. cookies ) {
385+ if ( it. name == cookieName )
386+ return it
387+ }
388+ return null
389+ }
320390}
0 commit comments