Skip to content

Commit 7610edb

Browse files
committed
Cache access token for client credentials flow
1 parent 8b27b99 commit 7610edb

File tree

1 file changed

+82
-28
lines changed
  • security/providers/oidc/src/main/java/io/helidon/security/providers/oidc

1 file changed

+82
-28
lines changed

security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java

Lines changed: 82 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@
1818

1919
import java.lang.System.Logger.Level;
2020
import java.lang.annotation.Annotation;
21+
import java.time.Duration;
22+
import java.time.Instant;
2123
import java.util.Collection;
2224
import java.util.HashMap;
2325
import java.util.LinkedList;
2426
import java.util.List;
2527
import java.util.Map;
28+
import java.util.Objects;
2629
import java.util.Optional;
2730
import java.util.ServiceLoader;
2831
import java.util.Set;
@@ -88,6 +91,8 @@ public final class OidcProvider implements AuthenticationProvider, OutboundSecur
8891
private final boolean propagate;
8992
private final OidcOutboundConfig outboundConfig;
9093
private final boolean useJwtGroups;
94+
private final ReentrantLock tokenCacheLock = new ReentrantLock();
95+
private CachedToken cachedToken;
9196
private final LruCache<String, TenantAuthenticationHandler> tenantAuthHandlers = LruCache.create();
9297

9398
private OidcProvider(Builder builder, OidcOutboundConfig oidcOutboundConfig) {
@@ -246,47 +251,80 @@ private OutboundSecurityResponse propagateAccessToken(ProviderRequest providerRe
246251
return OutboundSecurityResponse.empty();
247252
}
248253

254+
/**
255+
* Retrieves a client-credentials access token and injects it into the outbound
256+
* headers. The first successful call is cached in {@code cachedToken}; while the
257+
* token is still {@linkplain CachedToken#isValid() valid} every subsequent call
258+
* simply reuses it, protected by {@link #tokenCacheLock} to avoid concurrent
259+
* refreshes. Only when the cached entry is close to expiry is the identity
260+
* server contacted again and the cache updated.
261+
*/
249262
private OutboundSecurityResponse clientCredentials(ProviderRequest providerRequest, SecurityEnvironment outboundEnv) {
250263
OidcOutboundTarget target = outboundConfig.findTarget(outboundEnv);
251264
boolean enabled = target.propagate;
252265
if (enabled) {
253-
ClientCredentialsConfig clientCredentialsConfig = oidcConfig.clientCredentialsConfig();
254-
Parameters.Builder formBuilder = Parameters.builder("oidc-form-params")
255-
.add("grant_type", "client_credentials");
256-
257-
clientCredentialsConfig.scope().ifPresent(scope -> formBuilder.add("scope", scope));
258-
259-
HttpClientRequest postRequest = oidcConfig.appWebClient()
260-
.post()
261-
.uri(oidcConfig.tokenEndpointUri());
262-
263-
OidcUtil.updateRequest(OidcConfig.RequestType.ID_AND_SECRET_TO_TOKEN, oidcConfig, formBuilder, postRequest);
264-
265-
try (var response = postRequest.submit(formBuilder.build())) {
266-
if (response.status().family() == Status.Family.SUCCESSFUL) {
267-
JsonObject jsonObject = response.as(JsonObject.class);
268-
String accessToken = jsonObject.getString("access_token");
269-
266+
tokenCacheLock.lock();
267+
try {
268+
if (Objects.nonNull(cachedToken) && cachedToken.isValid()) {
270269
Map<String, List<String>> headers = new HashMap<>(outboundEnv.headers());
271-
target.tokenHandler.header(headers, accessToken);
270+
target.tokenHandler.header(headers, cachedToken.token);
272271
return OutboundSecurityResponse.withHeaders(headers);
273272
} else {
274-
return OutboundSecurityResponse.builder()
275-
.status(SecurityResponse.SecurityStatus.FAILURE)
276-
.description("Could not obtain access token from the identity server")
277-
.build();
273+
ClientCredentialsConfig clientCredentialsConfig = oidcConfig.clientCredentialsConfig();
274+
Parameters.Builder formBuilder = Parameters.builder("oidc-form-params")
275+
.add("grant_type", "client_credentials");
276+
277+
clientCredentialsConfig.scope().ifPresent(scope -> formBuilder.add("scope", scope));
278+
279+
280+
HttpClientRequest postRequest = oidcConfig.appWebClient()
281+
.post()
282+
.uri(oidcConfig.tokenEndpointUri());
283+
284+
OidcUtil.updateRequest(OidcConfig.RequestType.ID_AND_SECRET_TO_TOKEN, oidcConfig, formBuilder, postRequest);
285+
286+
try (var response = postRequest.submit(formBuilder.build())) {
287+
if (response.status().family() == Status.Family.SUCCESSFUL) {
288+
JsonObject jsonObject = response.as(JsonObject.class);
289+
String accessToken = jsonObject.getString("access_token");
290+
cacheTokenWithExpiry(jsonObject, accessToken);
291+
Map<String, List<String>> headers = new HashMap<>(outboundEnv.headers());
292+
target.tokenHandler.header(headers, accessToken);
293+
return OutboundSecurityResponse.withHeaders(headers);
294+
} else {
295+
return OutboundSecurityResponse.builder()
296+
.status(SecurityResponse.SecurityStatus.FAILURE)
297+
.description("Could not obtain access token from the identity server")
298+
.build();
299+
}
300+
} catch (Exception e) {
301+
return OutboundSecurityResponse.builder()
302+
.status(SecurityResponse.SecurityStatus.FAILURE)
303+
.description("An error occurred while obtaining access token from the identity server")
304+
.throwable(e)
305+
.build();
306+
}
278307
}
279-
} catch (Exception e) {
280-
return OutboundSecurityResponse.builder()
281-
.status(SecurityResponse.SecurityStatus.FAILURE)
282-
.description("An error occurred while obtaining access token from the identity server")
283-
.throwable(e)
284-
.build();
308+
} finally {
309+
tokenCacheLock.unlock();
285310
}
311+
286312
}
313+
314+
287315
return OutboundSecurityResponse.empty();
288316
}
289317

318+
private void cacheTokenWithExpiry(JsonObject jsonObject, String accessToken) {
319+
if (jsonObject.containsKey("expires_in")) {
320+
Duration expiresIn = Duration.ofSeconds(jsonObject.getJsonNumber("expires_in").longValueExact());
321+
Instant expiresAt = Instant.now().plus(expiresIn);
322+
cachedToken = new CachedToken(accessToken, expiresAt);
323+
} else {
324+
cachedToken = null;
325+
}
326+
}
327+
290328
/**
291329
* Builder for {@link OidcProvider}.
292330
*/
@@ -589,5 +627,21 @@ private OidcOutboundTarget(boolean propagate, TokenHandler handler) {
589627
tokenHandler = handler;
590628
}
591629
}
630+
631+
private static final class CachedToken {
632+
633+
private static final Duration DEFAULT_BUFFER_TIME = Duration.ofSeconds(30);
634+
private final String token;
635+
private final Instant expiresAt;
636+
637+
CachedToken(String token, Instant expiresAt) {
638+
this.token = token;
639+
this.expiresAt = expiresAt;
640+
}
641+
642+
private boolean isValid() {
643+
return Instant.now().isBefore(expiresAt.minus(DEFAULT_BUFFER_TIME));
644+
}
645+
}
592646
}
593647

0 commit comments

Comments
 (0)