Skip to content

Commit c0ce083

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

File tree

1 file changed

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

1 file changed

+82
-33
lines changed

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

Lines changed: 82 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@
1616

1717
package io.helidon.security.providers.oidc;
1818

19+
import io.helidon.security.SecurityResponse;
1920
import java.lang.System.Logger.Level;
2021
import java.lang.annotation.Annotation;
22+
import java.time.Duration;
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;
@@ -41,7 +44,6 @@
4144
import io.helidon.security.OutboundSecurityResponse;
4245
import io.helidon.security.ProviderRequest;
4346
import io.helidon.security.SecurityEnvironment;
44-
import io.helidon.security.SecurityResponse;
4547
import io.helidon.security.Subject;
4648
import io.helidon.security.abac.scope.ScopeValidator;
4749
import io.helidon.security.providers.common.OutboundConfig;
@@ -87,7 +89,10 @@ public final class OidcProvider implements AuthenticationProvider, OutboundSecur
8789
private final boolean propagate;
8890
private final OidcOutboundConfig outboundConfig;
8991
private final boolean useJwtGroups;
92+
private final ReentrantLock tokenCacheLock = new ReentrantLock();
93+
private CachedToken cachedToken;
9094
private final LruCache<String, TenantAuthenticationHandler> tenantAuthHandlers = LruCache.create();
95+
private static final Duration DEFAULT_TOKEN_LIFETIME = Duration.ofMinutes(5);
9196

9297
private OidcProvider(Builder builder, OidcOutboundConfig oidcOutboundConfig) {
9398
this.optional = builder.optional;
@@ -245,46 +250,74 @@ private OutboundSecurityResponse propagateAccessToken(ProviderRequest providerRe
245250
return OutboundSecurityResponse.empty();
246251
}
247252

253+
/**
254+
* Obtains an access token using the client credentials flow and propagates it in the outbound request headers.
255+
* <p>
256+
* The client credentials flow is used to obtain an access token when the client is acting on its own behalf,
257+
* not on behalf of a user. The obtained access token is then propagated in the outbound request headers using
258+
* the token handler configured for the outbound target.
259+
*
260+
* @param providerRequest the provider request context
261+
* @param outboundEnv the security environment for the outbound request
262+
* @return an {@link OutboundSecurityResponse} with the propagated access token in the headers, or an empty response
263+
* if propagation is not enabled for the outbound target, or a failure response if an error occurs while
264+
* obtaining the access token
265+
*/
248266
private OutboundSecurityResponse clientCredentials(ProviderRequest providerRequest, SecurityEnvironment outboundEnv) {
249267
OidcOutboundTarget target = outboundConfig.findTarget(outboundEnv);
250-
boolean enabled = target.propagate;
251-
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);
268+
if (!target.propagate) {
269+
return OutboundSecurityResponse.empty();
270+
}
264271

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");
272+
String accessToken;
273+
tokenCacheLock.lock();
274+
try {
275+
if (Objects.nonNull(cachedToken) && cachedToken.isValid()) {
276+
accessToken = cachedToken.token;
277+
} else {
278+
Parameters.Builder formBuilder = Parameters.builder("oidc-form-params")
279+
.add("grant_type", "client_credentials");
280+
281+
if (!oidcConfig.baseScopes().isEmpty()) {
282+
formBuilder.add("scope", oidcConfig.baseScopes());
283+
}
269284

270-
Map<String, List<String>> headers = new HashMap<>(outboundEnv.headers());
271-
target.tokenHandler.header(headers, accessToken);
272-
return OutboundSecurityResponse.withHeaders(headers);
273-
} else {
285+
HttpClientRequest postRequest = oidcConfig.appWebClient()
286+
.post()
287+
.uri(oidcConfig.tokenEndpointUri());
288+
289+
OidcUtil.updateRequest(OidcConfig.RequestType.ID_AND_SECRET_TO_TOKEN, oidcConfig, formBuilder, postRequest);
290+
291+
try (var response = postRequest.submit(formBuilder.build())) {
292+
if (response.status().family() == Status.Family.SUCCESSFUL) {
293+
JsonObject jsonObject = response.as(JsonObject.class);
294+
accessToken = jsonObject.getString("access_token");
295+
long expiresIn = jsonObject.containsKey("expires_in")
296+
? jsonObject.getJsonNumber("expires_in").longValue() * 1000
297+
: DEFAULT_TOKEN_LIFETIME.toMillis();
298+
long expiresAt = System.currentTimeMillis() + expiresIn;
299+
cachedToken = new CachedToken(accessToken, expiresAt);
300+
} else {
301+
return OutboundSecurityResponse.builder()
302+
.status(SecurityResponse.SecurityStatus.FAILURE)
303+
.description("Could not obtain access token from the identity server")
304+
.build();
305+
}
306+
} catch (Exception e) {
274307
return OutboundSecurityResponse.builder()
275-
.status(SecurityResponse.SecurityStatus.FAILURE)
276-
.description("Could not obtain access token from the identity server")
277-
.build();
308+
.status(SecurityResponse.SecurityStatus.FAILURE)
309+
.description("An error occurred while obtaining access token from the identity server")
310+
.throwable(e)
311+
.build();
278312
}
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();
285313
}
314+
} finally {
315+
tokenCacheLock.unlock();
286316
}
287-
return OutboundSecurityResponse.empty();
317+
318+
Map<String, List<String>> headers = new HashMap<>(outboundEnv.headers());
319+
target.tokenHandler.header(headers, accessToken);
320+
return OutboundSecurityResponse.withHeaders(headers);
288321
}
289322

290323
/**
@@ -589,5 +622,21 @@ private OidcOutboundTarget(boolean propagate, TokenHandler handler) {
589622
tokenHandler = handler;
590623
}
591624
}
625+
626+
private static final class CachedToken {
627+
628+
static final Duration DEFAULT_BUFFER_TIME = Duration.ofSeconds(10);
629+
final String token;
630+
final long expiresAtMillis;
631+
632+
CachedToken(String token, long expiresAtMillis) {
633+
this.token = token;
634+
this.expiresAtMillis = expiresAtMillis;
635+
}
636+
637+
boolean isValid() {
638+
return System.currentTimeMillis() < (expiresAtMillis - DEFAULT_BUFFER_TIME.toMillis());
639+
}
640+
}
592641
}
593642

0 commit comments

Comments
 (0)