|
16 | 16 |
|
17 | 17 | package io.helidon.security.providers.oidc; |
18 | 18 |
|
| 19 | +import io.helidon.security.SecurityResponse; |
19 | 20 | import java.lang.System.Logger.Level; |
20 | 21 | import java.lang.annotation.Annotation; |
| 22 | +import java.time.Duration; |
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; |
|
41 | 44 | import io.helidon.security.OutboundSecurityResponse; |
42 | 45 | import io.helidon.security.ProviderRequest; |
43 | 46 | import io.helidon.security.SecurityEnvironment; |
44 | | -import io.helidon.security.SecurityResponse; |
45 | 47 | import io.helidon.security.Subject; |
46 | 48 | import io.helidon.security.abac.scope.ScopeValidator; |
47 | 49 | import io.helidon.security.providers.common.OutboundConfig; |
@@ -87,7 +89,10 @@ public final class OidcProvider implements AuthenticationProvider, OutboundSecur |
87 | 89 | private final boolean propagate; |
88 | 90 | private final OidcOutboundConfig outboundConfig; |
89 | 91 | private final boolean useJwtGroups; |
| 92 | + private final ReentrantLock tokenCacheLock = new ReentrantLock(); |
| 93 | + private CachedToken cachedToken; |
90 | 94 | private final LruCache<String, TenantAuthenticationHandler> tenantAuthHandlers = LruCache.create(); |
| 95 | + private static final Duration DEFAULT_TOKEN_LIFETIME = Duration.ofMinutes(5); |
91 | 96 |
|
92 | 97 | private OidcProvider(Builder builder, OidcOutboundConfig oidcOutboundConfig) { |
93 | 98 | this.optional = builder.optional; |
@@ -245,46 +250,74 @@ private OutboundSecurityResponse propagateAccessToken(ProviderRequest providerRe |
245 | 250 | return OutboundSecurityResponse.empty(); |
246 | 251 | } |
247 | 252 |
|
| 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 | + */ |
248 | 266 | private OutboundSecurityResponse clientCredentials(ProviderRequest providerRequest, SecurityEnvironment outboundEnv) { |
249 | 267 | 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 | + } |
264 | 271 |
|
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 | + } |
269 | 284 |
|
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) { |
274 | 307 | 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(); |
278 | 312 | } |
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(); |
285 | 313 | } |
| 314 | + } finally { |
| 315 | + tokenCacheLock.unlock(); |
286 | 316 | } |
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); |
288 | 321 | } |
289 | 322 |
|
290 | 323 | /** |
@@ -589,5 +622,21 @@ private OidcOutboundTarget(boolean propagate, TokenHandler handler) { |
589 | 622 | tokenHandler = handler; |
590 | 623 | } |
591 | 624 | } |
| 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 | + } |
592 | 641 | } |
593 | 642 |
|
0 commit comments