Skip to content

Commit ec6eab5

Browse files
committed
feat: added auto refresh token #388 (#389)
1 parent ec249df commit ec6eab5

31 files changed

Lines changed: 321 additions & 102 deletions

README.md

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -135,28 +135,29 @@ switcher.poolsize=2
135135

136136
### Configuration Properties Reference
137137

138-
| Property | Required | Default | Description |
139-
|-------------------------------------|----------|---------|--------------------------------------------------------------------------------------|
140-
| `switcher.context` || - | Fully qualified class name extending SwitcherContext |
141-
| `switcher.url` || - | Switcher API endpoint URL |
142-
| `switcher.apikey` || - | API key for authentication |
143-
| `switcher.component` || - | Your application/component identifier |
144-
| `switcher.domain` || - | Domain name in Switcher API |
145-
| `switcher.environment` || default | Environment name (dev, staging, default) |
146-
| `switcher.local` || false | Enable local-only mode |
147-
| `switcher.check` || false | Validate switcher keys on startup |
148-
| `switcher.relay.restrict` || true | Defines if client will trigger local snapshot relay verification |
149-
| `switcher.snapshot.location` || - | Directory for snapshot files |
150-
| `switcher.snapshot.auto` || false | Auto-load snapshots on startup |
151-
| `switcher.snapshot.skipvalidation` || false | Skip snapshot validation on load |
152-
| `switcher.snapshot.updateinterval` || - | Interval for automatic snapshot updates (e.g., "5s", "2m") |
153-
| `switcher.snapshot.watcher` || false | Monitor snapshot files for changes |
154-
| `switcher.silent` || - | Enable silent mode (e.g., "5s", "2m") |
155-
| `switcher.timeout` || 3000 | API timeout in milliseconds |
156-
| `switcher.poolsize` || 2 | Thread pool size for API calls |
157-
| `switcher.regextimeout` (v1-only) || 3000 | Time in ms given to Timed Match Worker used for local Regex (ReDoS safety mechanism) |
158-
| `switcher.truststore.path` || - | Path to custom truststore file |
159-
| `switcher.truststore.password` || - | Password for custom truststore |
138+
| Property | Required | Default | Description |
139+
|------------------------------------|----------|---------|--------------------------------------------------------------------------------------|
140+
| `switcher.context` || - | Fully qualified class name extending SwitcherContext |
141+
| `switcher.url` || - | Switcher API endpoint URL |
142+
| `switcher.apikey` || - | API key for authentication |
143+
| `switcher.component` || - | Your application/component identifier |
144+
| `switcher.domain` || - | Domain name in Switcher API |
145+
| `switcher.environment` || default | Environment name (dev, staging, default) |
146+
| `switcher.local` || false | Enable local-only mode |
147+
| `switcher.check` || false | Validate switcher keys on startup |
148+
| `switcher.autorefreshtoken` || false | Automatically refresh API token before expiration |
149+
| `switcher.relay.restrict` || true | Defines if client will trigger local snapshot relay verification |
150+
| `switcher.snapshot.location` || - | Directory for snapshot files |
151+
| `switcher.snapshot.auto` || false | Auto-load snapshots on startup |
152+
| `switcher.snapshot.skipvalidation` || false | Skip snapshot validation on load |
153+
| `switcher.snapshot.updateinterval` || - | Interval for automatic snapshot updates (e.g., "5s", "2m") |
154+
| `switcher.snapshot.watcher` || false | Monitor snapshot files for changes |
155+
| `switcher.silent` || - | Enable silent mode (e.g., "5s", "2m") |
156+
| `switcher.timeout` || 3000 | API timeout in milliseconds |
157+
| `switcher.poolsize` || 2 | Thread pool size for API calls |
158+
| `switcher.regextimeout` (v1-only) || 3000 | Time in ms given to Timed Match Worker used for local Regex (ReDoS safety mechanism) |
159+
| `switcher.truststore.path` || - | Path to custom truststore file |
160+
| `switcher.truststore.password` || - | Password for custom truststore |
160161

161162
> 💡 **Environment Variables**: Use `${ENV_VAR:default_value}` syntax for environment variable substitution.
162163

src/main/java/com/switcherapi/client/ContextBuilder.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,4 +246,9 @@ public ContextBuilder poolConnectionSize(Integer poolSize) {
246246
Optional.ofNullable(poolSize).orElse(DEFAULT_POOL_SIZE));
247247
return this;
248248
}
249+
250+
public ContextBuilder autoRefreshToken(boolean autoRefreshToken) {
251+
switcherProperties.setValue(ContextKey.AUTO_REFRESH_TOKEN, autoRefreshToken);
252+
return this;
253+
}
249254
}

src/main/java/com/switcherapi/client/SwitcherConfig.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ abstract class SwitcherConfig {
1212

1313
protected boolean local;
1414
protected boolean check;
15+
protected boolean autoRefreshToken;
1516
protected String silent;
1617
protected Integer timeout;
1718
protected Integer regexTimeout;
@@ -39,6 +40,7 @@ protected void updateSwitcherConfig(SwitcherProperties properties) {
3940
setEnvironment(properties.getValue(ContextKey.ENVIRONMENT));
4041
setLocal(properties.getBoolean(ContextKey.LOCAL_MODE));
4142
setCheck(properties.getBoolean(ContextKey.CHECK_SWITCHERS));
43+
setAutoRefreshToken(properties.getBoolean(ContextKey.AUTO_REFRESH_TOKEN));
4244
setSilent(properties.getValue(ContextKey.SILENT_MODE));
4345
setTimeout(properties.getInt(ContextKey.TIMEOUT_MS));
4446
setPoolSize(properties.getInt(ContextKey.POOL_CONNECTION_SIZE));
@@ -106,6 +108,10 @@ public void setCheck(boolean check) {
106108
this.check = check;
107109
}
108110

111+
public void setAutoRefreshToken(boolean autoRefreshToken) {
112+
this.autoRefreshToken = autoRefreshToken;
113+
}
114+
109115
public void setSilent(String silent) {
110116
this.silent = silent;
111117
}

src/main/java/com/switcherapi/client/SwitcherContextBase.java

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ public abstract class SwitcherContextBase extends SwitcherConfig {
8787
protected static Set<String> switcherKeys;
8888
protected static Map<String, SwitcherRequest> switchers;
8989
protected static SwitcherExecutor switcherExecutor;
90-
private static ScheduledExecutorService scheduledExecutorService;
90+
private static ScheduledExecutorService scheduledSnapshotExecutorService;
91+
private static ScheduledExecutorService scheduledTokenExecutorService;
9192
private static ExecutorService watcherExecutorService;
9293
private static SnapshotWatcher watcherSnapshot;
9394
protected static SwitcherContextBase contextBase;
@@ -112,6 +113,7 @@ protected void configureClient() {
112113
.silentMode(silent)
113114
.regexTimeout(regexTimeout)
114115
.timeoutMs(timeout)
116+
.autoRefreshToken(autoRefreshToken)
115117
.poolConnectionSize(poolSize)
116118
.snapshotLocation(snapshot.getLocation())
117119
.snapshotAutoLoad(snapshot.isAuto())
@@ -198,9 +200,12 @@ public static void initializeClient() {
198200
* @return SwitcherExecutor instance
199201
*/
200202
private static SwitcherExecutor buildInstance() {
203+
initTokenExecutorService();
204+
201205
final ClientWS clientWS = initRemotePoolExecutorService();
202206
final SwitcherValidator validatorService = new ValidatorService();
203-
final ClientRemote clientRemote = new ClientRemoteService(clientWS, switcherProperties);
207+
final ClientRemote clientRemote = new ClientRemoteService(
208+
clientWS, switcherProperties, scheduledTokenExecutorService);
204209
final ClientLocal clientLocal = new ClientLocalService(validatorService);
205210

206211
if (contextBol(ContextKey.LOCAL_MODE)) {
@@ -307,7 +312,7 @@ public static ScheduledFuture<?> scheduleSnapshotAutoUpdate(String intervalValue
307312
return null;
308313
}
309314

310-
if (Objects.nonNull(scheduledExecutorService)) {
315+
if (Objects.nonNull(scheduledSnapshotExecutorService)) {
311316
terminateSnapshotAutoUpdateWorker();
312317
}
313318

@@ -325,7 +330,7 @@ public static ScheduledFuture<?> scheduleSnapshotAutoUpdate(String intervalValue
325330
};
326331

327332
initSnapshotExecutorService();
328-
return scheduledExecutorService.scheduleAtFixedRate(runnableSnapshotValidate, 0, interval, TimeUnit.MILLISECONDS);
333+
return scheduledSnapshotExecutorService.scheduleAtFixedRate(runnableSnapshotValidate, 0, interval, TimeUnit.MILLISECONDS);
329334
}
330335

331336
/**
@@ -340,17 +345,29 @@ public static ScheduledFuture<?> scheduleSnapshotAutoUpdate(String intervalValue
340345
}
341346

342347
/**
343-
* Configure Executor Service for Snapshot Update Worker
348+
* Configure Scheduled Executor Service for Snapshot Update Worker
344349
*/
345350
private static void initSnapshotExecutorService() {
346-
scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(r -> {
351+
scheduledSnapshotExecutorService = Executors.newSingleThreadScheduledExecutor(r -> {
347352
Thread thread = new Thread(r);
348353
thread.setName(WorkerName.SNAPSHOT_UPDATE_WORKER.toString());
349354
thread.setDaemon(true);
350355
return thread;
351356
});
352357
}
353358

359+
/**
360+
* Configure Scheduled Executor Service for Token Refresh Worker
361+
*/
362+
private static void initTokenExecutorService() {
363+
scheduledTokenExecutorService = Executors.newSingleThreadScheduledExecutor(r -> {
364+
Thread thread = new Thread(r);
365+
thread.setName(WorkerName.SWITCHER_TOKEN_WORKER.toString());
366+
thread.setDaemon(true);
367+
return thread;
368+
});
369+
}
370+
354371
/**
355372
* Configure Executor Service for Snapshot Watch Worker
356373
*/
@@ -539,9 +556,19 @@ public static void configure(ContextBuilder builder) {
539556
* Cancel existing scheduled task for updating local Snapshot
540557
*/
541558
public static void terminateSnapshotAutoUpdateWorker() {
542-
if (Objects.nonNull(scheduledExecutorService)) {
543-
scheduledExecutorService.shutdownNow();
544-
scheduledExecutorService = null;
559+
if (Objects.nonNull(scheduledSnapshotExecutorService)) {
560+
scheduledSnapshotExecutorService.shutdownNow();
561+
scheduledSnapshotExecutorService = null;
562+
}
563+
}
564+
565+
/**
566+
* Cancel existing scheduled task for token refresh
567+
*/
568+
public static void terminateTokenRefreshWorker() {
569+
if (Objects.nonNull(scheduledTokenExecutorService)) {
570+
scheduledTokenExecutorService.shutdownNow();
571+
scheduledTokenExecutorService = null;
545572
}
546573
}
547574

src/main/java/com/switcherapi/client/SwitcherPropertiesImpl.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ private void initDefaults() {
3030
setValue(ContextKey.LOCAL_MODE, false);
3131
setValue(ContextKey.CHECK_SWITCHERS, false);
3232
setValue(ContextKey.RESTRICT_RELAY, true);
33+
setValue(ContextKey.AUTO_REFRESH_TOKEN, false);
3334
}
3435

3536
@Override
@@ -49,6 +50,7 @@ public void loadFromProperties(Properties prop) {
4950
setValue(ContextKey.LOCAL_MODE, getBoolDefault(resolveProperties(ContextKey.LOCAL_MODE.getParam(), prop), false));
5051
setValue(ContextKey.CHECK_SWITCHERS, getBoolDefault(resolveProperties(ContextKey.CHECK_SWITCHERS.getParam(), prop), false));
5152
setValue(ContextKey.RESTRICT_RELAY, getBoolDefault(resolveProperties(ContextKey.RESTRICT_RELAY.getParam(), prop), true));
53+
setValue(ContextKey.AUTO_REFRESH_TOKEN, getBoolDefault(resolveProperties(ContextKey.AUTO_REFRESH_TOKEN.getParam(), prop), false));
5254
setValue(ContextKey.REGEX_TIMEOUT, getIntDefault(resolveProperties(ContextKey.REGEX_TIMEOUT.getParam(), prop), DEFAULT_REGEX_TIMEOUT));
5355
setValue(ContextKey.TRUSTSTORE_PATH, resolveProperties(ContextKey.TRUSTSTORE_PATH.getParam(), prop));
5456
setValue(ContextKey.TRUSTSTORE_PASSWORD, resolveProperties(ContextKey.TRUSTSTORE_PASSWORD.getParam(), prop));

src/main/java/com/switcherapi/client/model/ContextKey.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,13 @@ public enum ContextKey {
109109
/**
110110
* (Number) Defines a fixed number of threads for the pool connection (default is 2).
111111
*/
112-
POOL_CONNECTION_SIZE("switcher.poolsize");
113-
112+
POOL_CONNECTION_SIZE("switcher.poolsize"),
113+
114+
/**
115+
* (boolean) Enables automatic refresh of authentication token (default is false)
116+
*/
117+
AUTO_REFRESH_TOKEN("switcher.autorefreshtoken");
118+
114119
private final String param;
115120

116121
ContextKey(String param) {

src/main/java/com/switcherapi/client/remote/dto/AuthResponse.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ public boolean isExpired() {
2626
return (this.exp * 1000) < System.currentTimeMillis();
2727
}
2828

29+
public long getExp() {
30+
return this.exp;
31+
}
32+
2933
@Override
3034
public String toString() {
3135
return "AuthResponse{" +

src/main/java/com/switcherapi/client/service/WorkerName.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ public enum WorkerName {
66
SNAPSHOT_WATCH_WORKER("switcherapi-snapshot-watcher"),
77
SNAPSHOT_UPDATE_WORKER("switcherapi-snapshot-update"),
88
SWITCHER_REMOTE_WORKER("switcherapi-remote-pool"),
9-
SWITCHER_ASYNC_WORKER("switcherapi-async");
9+
SWITCHER_ASYNC_WORKER("switcherapi-async"),
10+
SWITCHER_TOKEN_WORKER("switcherapi-token-refresh");
1011

1112
private final String name;
1213

src/main/java/com/switcherapi/client/service/remote/ClientRemoteService.java

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,52 @@
66
import com.switcherapi.client.exception.SwitcherRemoteException;
77
import com.switcherapi.client.model.ContextKey;
88
import com.switcherapi.client.model.criteria.Snapshot;
9-
import com.switcherapi.client.remote.dto.*;
109
import com.switcherapi.client.remote.ClientWS;
10+
import com.switcherapi.client.remote.dto.AuthResponse;
11+
import com.switcherapi.client.remote.dto.CriteriaRequest;
12+
import com.switcherapi.client.remote.dto.CriteriaResponse;
13+
import com.switcherapi.client.remote.dto.SnapshotVersionResponse;
14+
import com.switcherapi.client.remote.dto.SwitchersCheck;
1115
import com.switcherapi.client.utils.SwitcherUtils;
1216
import org.apache.commons.lang3.StringUtils;
17+
import org.slf4j.Logger;
18+
import org.slf4j.LoggerFactory;
1319

1420
import java.util.Date;
21+
import java.util.Objects;
1522
import java.util.Optional;
1623
import java.util.Set;
24+
import java.util.concurrent.ScheduledExecutorService;
25+
import java.util.concurrent.ScheduledFuture;
26+
import java.util.concurrent.TimeUnit;
1727

1828
/**
1929
* @author Roger Floriano (petruki)
2030
* @since 2019-12-24
2131
*/
2232
public class ClientRemoteService implements ClientRemote {
2333

34+
private static final Logger log = LoggerFactory.getLogger(ClientRemoteService.class);
35+
36+
private final ScheduledExecutorService scheduledExecutorService;
37+
2438
private final SwitcherProperties switcherProperties;
25-
39+
2640
private final ClientWS clientWs;
2741

2842
private AuthResponse authResponse;
2943

44+
private ScheduledFuture<?> refreshFuture;
45+
3046
private enum TokenStatus {
3147
VALID, INVALID, SILENT
3248
}
3349

34-
public ClientRemoteService(ClientWS clientWs, SwitcherProperties switcherProperties) {
50+
public ClientRemoteService(ClientWS clientWs, SwitcherProperties switcherProperties,
51+
ScheduledExecutorService scheduledExecutorService) {
3552
this.clientWs = clientWs;
3653
this.switcherProperties = switcherProperties;
54+
this.scheduledExecutorService = scheduledExecutorService;
3755
}
3856

3957
@Override
@@ -92,7 +110,12 @@ public SwitchersCheck checkSwitchers(final Set<String> switchers) {
92110

93111
private void auth(TokenStatus tokenStatus) {
94112
if (tokenStatus == TokenStatus.INVALID) {
113+
log.debug("Auth token is invalid or expired. Attempting to authenticate...");
95114
this.authResponse = this.clientWs.auth().orElseGet(AuthResponse::new);
115+
116+
if (isAutoRefreshable()) {
117+
scheduleNextAuth();
118+
}
96119
}
97120

98121
if (tokenStatus == TokenStatus.SILENT) {
@@ -109,7 +132,7 @@ private TokenStatus isTokenValid() throws SwitcherRemoteException,
109132
return TokenStatus.INVALID;
110133
}
111134

112-
if (optAuthResponse.get().getToken().equals(ContextKey.SILENT_MODE.getParam())
135+
if (ContextKey.SILENT_MODE.getParam().equals(optAuthResponse.get().getToken())
113136
&& !optAuthResponse.get().isExpired()) {
114137
return TokenStatus.SILENT;
115138
}
@@ -129,4 +152,35 @@ private void setSilentModeExpiration() throws SwitcherInvalidDateTimeArgumentExc
129152
}
130153
}
131154

155+
private void scheduleNextAuth() {
156+
long msUntilExpiry = (authResponse.getExp() * 1000L) - (System.currentTimeMillis());
157+
long refreshAt = Math.max(msUntilExpiry - 5000, 0); // 5s before expiry
158+
159+
terminateAutoRefresh();
160+
refreshFuture = scheduledExecutorService.schedule(() -> {
161+
try {
162+
log.debug("Auto-refreshing auth token...");
163+
this.authResponse = this.clientWs.auth().orElseGet(AuthResponse::new);
164+
scheduleNextAuth();
165+
} catch (Exception e) {
166+
log.error("Failed to auto-refresh auth token: {}", e.getMessage());
167+
terminateAutoRefresh();
168+
}
169+
}, refreshAt, TimeUnit.MILLISECONDS);
170+
}
171+
172+
private boolean isAutoRefreshable() {
173+
return switcherProperties.getBoolean(ContextKey.AUTO_REFRESH_TOKEN) &&
174+
(Objects.isNull(refreshFuture) || refreshFuture.isDone());
175+
}
176+
177+
private void terminateAutoRefresh() {
178+
if (Objects.nonNull(refreshFuture)) {
179+
refreshFuture.cancel(true);
180+
refreshFuture = null;
181+
log.debug("Terminated existing auto-refresh task.");
182+
}
183+
}
132184
}
185+
186+

src/test/java/com/switcherapi/client/SwitcherBasicCriteriaResponseTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class SwitcherBasicCriteriaResponseTest extends MockWebServerHelper {
2424

2525
@BeforeAll
2626
static void setup() throws IOException {
27-
MockWebServerHelper.setupMockServer();
27+
setupMockServer();
2828

2929
Switchers.loadProperties(); // Load default properties from resources
3030
Switchers.configure(ContextBuilder.builder() // Override default properties
@@ -40,7 +40,7 @@ static void setup() throws IOException {
4040

4141
@AfterAll
4242
static void tearDown() {
43-
MockWebServerHelper.tearDownMockServer();
43+
tearDownMockServer();
4444
}
4545

4646
@BeforeEach

0 commit comments

Comments
 (0)