Skip to content

Commit be8b32a

Browse files
committed
Cache access token for client credentials flow
1 parent b48be75 commit be8b32a

File tree

1 file changed

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

1 file changed

+82
-29
lines changed

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

Lines changed: 82 additions & 29 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;
@@ -87,6 +90,8 @@ public final class OidcProvider implements AuthenticationProvider, OutboundSecur
8790
private final boolean propagate;
8891
private final OidcOutboundConfig outboundConfig;
8992
private final boolean useJwtGroups;
93+
private final ReentrantLock tokenCacheLock = new ReentrantLock();
94+
private CachedToken cachedToken;
9095
private final LruCache<String, TenantAuthenticationHandler> tenantAuthHandlers = LruCache.create();
9196

9297
private OidcProvider(Builder builder, OidcOutboundConfig oidcOutboundConfig) {
@@ -245,48 +250,80 @@ private OutboundSecurityResponse propagateAccessToken(ProviderRequest providerRe
245250
return OutboundSecurityResponse.empty();
246251
}
247252

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

317+
private void cacheTokenWithExpiry(JsonObject jsonObject, String accessToken) {
318+
if (jsonObject.containsKey("expires_in")) {
319+
Duration expiresIn = Duration.ofSeconds(jsonObject.getJsonNumber("expires_in").longValueExact());
320+
Instant expiresAt = Instant.now().plus(expiresIn);
321+
cachedToken = new CachedToken(accessToken, expiresAt);
322+
} else {
323+
cachedToken = null;
324+
}
325+
}
326+
290327
/**
291328
* Builder for {@link OidcProvider}.
292329
*/
@@ -589,5 +626,21 @@ private OidcOutboundTarget(boolean propagate, TokenHandler handler) {
589626
tokenHandler = handler;
590627
}
591628
}
629+
630+
private static final class CachedToken {
631+
632+
private static final Duration DEFAULT_BUFFER_TIME = Duration.ofSeconds(30);
633+
private final String token;
634+
private final Instant expiresAt;
635+
636+
CachedToken(String token, Instant expiresAt) {
637+
this.token = token;
638+
this.expiresAt = expiresAt;
639+
}
640+
641+
private boolean isValid() {
642+
return Instant.now().isBefore(expiresAt.minus(DEFAULT_BUFFER_TIME));
643+
}
644+
}
592645
}
593646

0 commit comments

Comments
 (0)