diff --git a/README.md b/README.md index 0068615..476c33f 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ The `ConfidentialClient` refreshes access tokens proactively before their actual Default behaviour: - A 30 second (30,000 ms) proactive offset is applied automatically. - Calls to `getAccessToken()` (or `getAccessToken(false)`) reuse the cached token while it is still considered valid under this adjusted expiry. -- `getAccessToken(true)` forces a fresh token unless one was very recently refreshed (within 5 seconds) to avoid unnecessary duplicate requests. +- `getAccessToken(true)` forces a fresh token unless one was very recently refreshed (within 5 seconds) to avoid unnecessary duplicate requests from concurrent threads. You can override the proactive offset by configuring it in `RequestOptions`: diff --git a/src/main/java/com/factset/sdk/utils/authentication/ConfidentialClient.java b/src/main/java/com/factset/sdk/utils/authentication/ConfidentialClient.java index db17e32..fa72c43 100644 --- a/src/main/java/com/factset/sdk/utils/authentication/ConfidentialClient.java +++ b/src/main/java/com/factset/sdk/utils/authentication/ConfidentialClient.java @@ -69,7 +69,7 @@ public class ConfidentialClient implements OAuth2Client { public ConfidentialClient(final String configPath) throws AuthServerMetadataContentException, AuthServerMetadataException, ConfigurationException { - this(new Configuration(configPath)); + this(new Configuration(configPath), RequestOptions.builder().build()); } /** @@ -142,7 +142,7 @@ protected ConfidentialClient(final String configPath, final TokenRequestBuilder throws AuthServerMetadataContentException, AuthServerMetadataException, ConfigurationException { - this(new Configuration(configPath)); + this(new Configuration(configPath), RequestOptions.builder().build()); this.tokenRequestBuilder = tokReqBuilder.uri(this.providerMetadata.getTokenEndpointURI()); } @@ -160,7 +160,7 @@ protected ConfidentialClient(final String configPath, final TokenRequestBuilder protected ConfidentialClient(final Configuration config, final TokenRequestBuilder tokReqBuilder) throws AuthServerMetadataContentException, AuthServerMetadataException { - this(config); + this(config, RequestOptions.builder().build()); this.tokenRequestBuilder = tokReqBuilder.uri(this.providerMetadata.getTokenEndpointURI()); } @@ -194,7 +194,7 @@ protected ConfidentialClient(final Configuration config, final TokenRequestBuild * @throws AccessTokenException If it can't make a successful request or parse the TokenRequest. * @throws SigningJwsException If the signing of the JWS fails. */ - public String getAccessToken(boolean forceRefresh) throws AccessTokenException, SigningJwsException { + public synchronized String getAccessToken(boolean forceRefresh) throws AccessTokenException, SigningJwsException { if (this.isCachedTokenValid()) { if (!forceRefresh) { LOGGER.info("Retrieved access token which expires in: {} seconds", TimeUnit.MILLISECONDS.toSeconds(this.accessTokenExpireTime - System.currentTimeMillis())); diff --git a/src/test/java/com/factset/sdk/utils/authentication/ConfidentialClientTest.java b/src/test/java/com/factset/sdk/utils/authentication/ConfidentialClientTest.java index ea30e97..0e55065 100644 --- a/src/test/java/com/factset/sdk/utils/authentication/ConfidentialClientTest.java +++ b/src/test/java/com/factset/sdk/utils/authentication/ConfidentialClientTest.java @@ -326,6 +326,31 @@ void getAccessTokenForceRefreshThenCachedReturnsCorrectTokens() throws Exception verify(harness.httpRequestMock, times(1)).send(); } + @Test + void getAccessTokenTwoDifferentThreadsSimultaneouslyOnlyFetchesOnce() throws Exception { + TestHarness harness = createClientWithTokens(899, "threadedToken"); + + Runnable task = () -> { + String token; + try { + token = harness.client.getAccessToken(); + } catch (AccessTokenException | SigningJwsException e) { + throw new RuntimeException(e); + } + assertEquals("threadedToken", token); + }; + + Thread thread1 = new Thread(task); + Thread thread2 = new Thread(task); + + thread1.start(); + thread2.start(); + + thread1.join(); + thread2.join(); + + verify(harness.httpRequestMock, times(1)).send(); + } @Test void forceRefreshWithinGracePeriodReturnsCachedToken() throws Exception {