|
18 | 18 |
|
19 | 19 | import java.lang.System.Logger.Level; |
20 | 20 | import java.lang.annotation.Annotation; |
| 21 | +import java.time.Duration; |
| 22 | +import java.time.Instant; |
21 | 23 | import java.util.Collection; |
22 | 24 | import java.util.HashMap; |
23 | 25 | import java.util.LinkedList; |
24 | 26 | import java.util.List; |
25 | 27 | import java.util.Map; |
| 28 | +import java.util.Objects; |
26 | 29 | import java.util.Optional; |
27 | 30 | import java.util.ServiceLoader; |
28 | 31 | import java.util.Set; |
@@ -88,6 +91,8 @@ public final class OidcProvider implements AuthenticationProvider, OutboundSecur |
88 | 91 | private final boolean propagate; |
89 | 92 | private final OidcOutboundConfig outboundConfig; |
90 | 93 | private final boolean useJwtGroups; |
| 94 | + private final ReentrantLock tokenCacheLock = new ReentrantLock(); |
| 95 | + private CachedToken cachedToken; |
91 | 96 | private final LruCache<String, TenantAuthenticationHandler> tenantAuthHandlers = LruCache.create(); |
92 | 97 |
|
93 | 98 | private OidcProvider(Builder builder, OidcOutboundConfig oidcOutboundConfig) { |
@@ -246,47 +251,80 @@ private OutboundSecurityResponse propagateAccessToken(ProviderRequest providerRe |
246 | 251 | return OutboundSecurityResponse.empty(); |
247 | 252 | } |
248 | 253 |
|
| 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 | + */ |
249 | 262 | private OutboundSecurityResponse clientCredentials(ProviderRequest providerRequest, SecurityEnvironment outboundEnv) { |
250 | 263 | OidcOutboundTarget target = outboundConfig.findTarget(outboundEnv); |
251 | 264 | boolean enabled = target.propagate; |
252 | 265 | 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()) { |
270 | 269 | Map<String, List<String>> headers = new HashMap<>(outboundEnv.headers()); |
271 | | - target.tokenHandler.header(headers, accessToken); |
| 270 | + target.tokenHandler.header(headers, cachedToken.token); |
272 | 271 | return OutboundSecurityResponse.withHeaders(headers); |
273 | 272 | } 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 | + } |
278 | 307 | } |
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(); |
285 | 310 | } |
| 311 | + |
286 | 312 | } |
| 313 | + |
| 314 | + |
287 | 315 | return OutboundSecurityResponse.empty(); |
288 | 316 | } |
289 | 317 |
|
| 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 | + |
290 | 328 | /** |
291 | 329 | * Builder for {@link OidcProvider}. |
292 | 330 | */ |
@@ -589,5 +627,21 @@ private OidcOutboundTarget(boolean propagate, TokenHandler handler) { |
589 | 627 | tokenHandler = handler; |
590 | 628 | } |
591 | 629 | } |
| 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 | + } |
592 | 646 | } |
593 | 647 |
|
0 commit comments