Skip to content

Commit 5f53c21

Browse files
committed
fix(java): offset via requestOptions
1 parent d5a11ee commit 5f53c21

5 files changed

Lines changed: 91 additions & 45 deletions

File tree

README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -189,15 +189,14 @@ Default behaviour:
189189
- Calls to `getAccessToken()` (or `getAccessToken(false)`) reuse the cached token while it is still considered valid under this adjusted expiry.
190190
- `getAccessToken(true)` always forces a fresh token (bypasses cache).
191191

192-
You can override the proactive offset by supplying a custom value (milliseconds) via the constructor overload.
192+
You can override the proactive offset by configuring it in `RequestOptions`:
193193

194194
#### Example
195195
```java
196-
ConfidentialClient client10s = new ConfidentialClient(
197-
"./path/to/config.json",
198-
RequestOptions.builder().build(),
199-
10000L // 10 second proactive expiry offset
200-
);
196+
RequestOptions options10s = RequestOptions.builder()
197+
.accessTokenExpiryOffsetMillis(90_000L) // 90 seconds
198+
.build();
199+
ConfidentialClient client10s = new ConfidentialClient("./path/to/config.json", options10s);
201200
```
202201

203202
## Modules

src/main/java/com/factset/sdk/utils/authentication/ConfidentialClient.java

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public class ConfidentialClient implements OAuth2Client {
6767
public ConfidentialClient(final String configPath)
6868
throws AuthServerMetadataContentException, AuthServerMetadataException,
6969
ConfigurationException {
70-
this(new Configuration(configPath), RequestOptions.builder().build(), Constants.DEFAULT_ACCESS_TOKEN_EXPIRY_OFFSET_MILLIS);
70+
this(new Configuration(configPath), RequestOptions.builder().build());
7171
}
7272

7373
/**
@@ -85,19 +85,7 @@ public ConfidentialClient(final String configPath)
8585
public ConfidentialClient(final String configPath, RequestOptions requestOptions)
8686
throws AuthServerMetadataContentException, AuthServerMetadataException,
8787
ConfigurationException {
88-
this(new Configuration(configPath), requestOptions, Constants.DEFAULT_ACCESS_TOKEN_EXPIRY_OFFSET_MILLIS);
89-
}
90-
91-
/**
92-
* Creates a new ConfidentialClient with a custom proactive expiry offset.
93-
* @param configPath path to config file
94-
* @param requestOptions request options (proxy/ssl)
95-
* @param accessTokenExpiryOffsetMillis milliseconds subtracted from server expiry (non-negative)
96-
*/
97-
public ConfidentialClient(final String configPath, RequestOptions requestOptions, long accessTokenExpiryOffsetMillis)
98-
throws AuthServerMetadataContentException, AuthServerMetadataException,
99-
ConfigurationException {
100-
this(new Configuration(configPath), requestOptions, accessTokenExpiryOffsetMillis);
88+
this(new Configuration(configPath), requestOptions);
10189
}
10290

10391
/**
@@ -112,7 +100,7 @@ public ConfidentialClient(final String configPath, RequestOptions requestOptions
112100
*/
113101
public ConfidentialClient(final Configuration config)
114102
throws AuthServerMetadataContentException, AuthServerMetadataException {
115-
this(config, RequestOptions.builder().build(), Constants.DEFAULT_ACCESS_TOKEN_EXPIRY_OFFSET_MILLIS);
103+
this(config, RequestOptions.builder().build());
116104
}
117105

118106
/**
@@ -128,22 +116,11 @@ public ConfidentialClient(final Configuration config)
128116
*/
129117
public ConfidentialClient(final Configuration config, RequestOptions requestOptions)
130118
throws AuthServerMetadataContentException, AuthServerMetadataException {
131-
this(config, requestOptions, Constants.DEFAULT_ACCESS_TOKEN_EXPIRY_OFFSET_MILLIS);
132-
}
133-
134-
/**
135-
* Core constructor with configurable access token proactive expiry offset.
136-
* @param config configuration
137-
* @param requestOptions request options
138-
* @param accessTokenExpiryOffsetMillis milliseconds to subtract from token lifetime when computing internal expiry
139-
*/
140-
public ConfidentialClient(final Configuration config, RequestOptions requestOptions, long accessTokenExpiryOffsetMillis)
141-
throws AuthServerMetadataContentException, AuthServerMetadataException {
142119
Objects.requireNonNull(config, "Configuration object must not be null");
143120
this.config = config;
144121
LOGGER.debug("Finished initialising configuration");
145122
this.requestOptions = requestOptions == null ? RequestOptions.builder().build() : requestOptions;
146-
this.accessTokenExpiryOffsetMillis = accessTokenExpiryOffsetMillis;
123+
this.accessTokenExpiryOffsetMillis = this.requestOptions.getAccessTokenExpiryOffsetMillis();
147124
this.requestProviderMetadata();
148125
}
149126

@@ -163,7 +140,7 @@ protected ConfidentialClient(final String configPath, final TokenRequestBuilder
163140
throws AuthServerMetadataContentException,
164141
AuthServerMetadataException,
165142
ConfigurationException {
166-
this(new Configuration(configPath), RequestOptions.builder().build(), Constants.DEFAULT_ACCESS_TOKEN_EXPIRY_OFFSET_MILLIS);
143+
this(new Configuration(configPath), RequestOptions.builder().build());
167144
this.tokenRequestBuilder = tokReqBuilder.uri(this.providerMetadata.getTokenEndpointURI());
168145
}
169146

@@ -181,7 +158,7 @@ protected ConfidentialClient(final String configPath, final TokenRequestBuilder
181158
protected ConfidentialClient(final Configuration config, final TokenRequestBuilder tokReqBuilder)
182159
throws AuthServerMetadataContentException,
183160
AuthServerMetadataException {
184-
this(config, RequestOptions.builder().build(), Constants.DEFAULT_ACCESS_TOKEN_EXPIRY_OFFSET_MILLIS);
161+
this(config, RequestOptions.builder().build());
185162
this.tokenRequestBuilder = tokReqBuilder.uri(this.providerMetadata.getTokenEndpointURI());
186163
}
187164

@@ -200,7 +177,7 @@ protected ConfidentialClient(final Configuration config, final TokenRequestBuild
200177
protected ConfidentialClient(final Configuration config, final TokenRequestBuilder tokReqBuilder, RequestOptions requestOptions)
201178
throws AuthServerMetadataContentException,
202179
AuthServerMetadataException {
203-
this(config, requestOptions, Constants.DEFAULT_ACCESS_TOKEN_EXPIRY_OFFSET_MILLIS);
180+
this(config, requestOptions);
204181
this.tokenRequestBuilder = tokReqBuilder.uri(this.providerMetadata.getTokenEndpointURI());
205182
}
206183

@@ -306,9 +283,21 @@ private String fetchAccessToken() throws AccessTokenException, SigningJwsExcepti
306283

307284
if (tokenRes.indicatesSuccess()) {
308285
this.accessToken = tokenRes.toSuccessResponse().getTokens().getAccessToken();
309-
this.accessTokenExpireTime =
310-
this.jwsIssuedAt + TimeUnit.SECONDS.toMillis(this.accessToken.getLifetime()) - this.accessTokenExpiryOffsetMillis;
311-
LOGGER.info("Fetched access token which expires in: {} seconds (buffered)", this.accessToken.getLifetime());
286+
long lifetimeMillis = TimeUnit.SECONDS.toMillis(this.accessToken.getLifetime());
287+
long rawOffset = this.accessTokenExpiryOffsetMillis;
288+
289+
long clampedOffset;
290+
if (rawOffset >= 899_000L) {
291+
clampedOffset = 899_000L - 1;
292+
LOGGER.warn("Proactive expiry offset {}ms >= 899 seconds. Clamped to {}ms.", rawOffset, clampedOffset);
293+
} else {
294+
clampedOffset = rawOffset;
295+
}
296+
297+
long effectiveLifetime = lifetimeMillis - clampedOffset;
298+
this.accessTokenExpireTime = this.jwsIssuedAt + effectiveLifetime;
299+
LOGGER.info("Fetched access token (serverLifetime={}s, offsetApplied={}ms, effectiveLifetime={}ms)",
300+
this.accessToken.getLifetime(), clampedOffset, effectiveLifetime);
312301
return this.accessToken.toString();
313302
}
314303

src/main/java/com/factset/sdk/utils/authentication/Constants.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@ public final class Constants {
1313
// default values
1414
public static final String FACTSET_WELL_KNOWN_URI = "https://auth.factset.com/.well-known/openid-configuration";
1515

16-
/**
17-
* Default buffer (in milliseconds) to refresh access token before actual expiry.
18-
*/
19-
public static final long DEFAULT_ACCESS_TOKEN_EXPIRY_OFFSET_MILLIS = 30000L;
20-
2116
private Constants() {
2217
throw new IllegalStateException("Utility class");
2318
}

src/main/java/com/factset/sdk/utils/authentication/RequestOptions.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,7 @@ public class RequestOptions {
2222

2323
@Builder.Default
2424
String userAgent = "fds-sdk/java/utils/1.1.5 (" + System.getProperty("os.name") + "; Java" + System.getProperty("java.version") + ")";
25+
26+
@Builder.Default
27+
long accessTokenExpiryOffsetMillis = 30_000L;
2528
}

src/test/java/com/factset/sdk/utils/authentication/ConfidentialClientTest.java

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,62 @@ void accessTokenFiftySecondOffsetTriggersRefetchAfterEarlyExpirySingleToken() th
361361
verify(harness.httpRequestMock, times(2)).send();
362362
}
363363

364+
@Test
365+
void accessTokenDefaultOffsetUsesThirtySeconds() throws Exception {
366+
RequestOptions defaultOptions = RequestOptions.builder().build();
367+
TestHarness harness = createClientTokenCustomOffset(30_000L);
368+
369+
String token = harness.client.getAccessToken();
370+
assertEquals("tokenSingle", token);
371+
372+
assertEquals(30_000L, defaultOptions.getAccessTokenExpiryOffsetMillis(), "RequestOptions should have default 30s offset");
373+
374+
java.lang.reflect.Field issuedAtField = ConfidentialClient.class.getDeclaredField("jwsIssuedAt");
375+
java.lang.reflect.Field expiryField = ConfidentialClient.class.getDeclaredField("accessTokenExpireTime");
376+
issuedAtField.setAccessible(true);
377+
expiryField.setAccessible(true);
378+
379+
long issuedAt = (long) issuedAtField.get(harness.client);
380+
long internalExpiry = (long) expiryField.get(harness.client);
381+
long expectedDelta = 899_000L - 30_000L;
382+
assertEquals(expectedDelta, internalExpiry - issuedAt, "Internal expiry should be lifetime - default offset");
383+
}
384+
385+
@Test
386+
void accessTokenNegativeOffsetExtendsLifetime() throws Exception {
387+
TestHarness harness = createClientTokenCustomOffset(-10_000L);
388+
389+
String first = harness.client.getAccessToken();
390+
assertEquals("tokenSingle", first);
391+
verify(harness.httpRequestMock, times(1)).send();
392+
393+
java.lang.reflect.Field issuedAtField = ConfidentialClient.class.getDeclaredField("jwsIssuedAt");
394+
java.lang.reflect.Field expiryField = ConfidentialClient.class.getDeclaredField("accessTokenExpireTime");
395+
issuedAtField.setAccessible(true);
396+
expiryField.setAccessible(true);
397+
long issuedAt = (long) issuedAtField.get(harness.client);
398+
long internalExpiry = (long) expiryField.get(harness.client);
399+
long expectedDelta = 899_000L - (-10_000L);
400+
assertEquals(expectedDelta, internalExpiry - issuedAt, "Negative offset should extend lifetime");
401+
}
402+
403+
@Test
404+
void accessTokenLargeOffsetGetsClampedToUnder899Seconds() throws Exception {
405+
TestHarness harness = createClientTokenCustomOffset(900_000L);
406+
407+
String first = harness.client.getAccessToken();
408+
assertEquals("tokenSingle", first);
409+
410+
java.lang.reflect.Field issuedAtField = ConfidentialClient.class.getDeclaredField("jwsIssuedAt");
411+
java.lang.reflect.Field expiryField = ConfidentialClient.class.getDeclaredField("accessTokenExpireTime");
412+
issuedAtField.setAccessible(true);
413+
expiryField.setAccessible(true);
414+
long issuedAt = (long) issuedAtField.get(harness.client);
415+
long internalExpiry = (long) expiryField.get(harness.client);
416+
long expectedDelta = 899_000L - (899_000L - 1);
417+
assertEquals(expectedDelta, internalExpiry - issuedAt, "Large offset should be clamped, leaving 1ms effective lifetime");
418+
}
419+
364420
private static TestHarness createClientTokenCustomOffset(long offsetMillis) throws Exception {
365421
HttpURLConnection mockedConn = mock(HttpURLConnection.class);
366422
URL mockedURL = getUrlMockResponse("exampleResponseWellKnownUri.txt", mockedConn);
@@ -380,7 +436,11 @@ private static TestHarness createClientTokenCustomOffset(long offsetMillis) thro
380436
doReturn(httpRequestMock).when(tokenRequestMock).toHTTPRequest();
381437
when(httpRequestMock.send()).thenReturn(res, res);
382438

383-
ConfidentialClient client = new ConfidentialClient(configurationMock, RequestOptions.builder().build(), offsetMillis);
439+
RequestOptions requestOptionsWithOffset = RequestOptions.builder()
440+
.accessTokenExpiryOffsetMillis(offsetMillis)
441+
.build();
442+
443+
ConfidentialClient client = new ConfidentialClient(configurationMock, requestOptionsWithOffset);
384444
java.lang.reflect.Field f = ConfidentialClient.class.getDeclaredField("tokenRequestBuilder");
385445
f.setAccessible(true);
386446
f.set(client, tokenRequestBuilderSpy);

0 commit comments

Comments
 (0)