From 138137c46b7036a69de8c438a8266d03ff97ace7 Mon Sep 17 00:00:00 2001 From: rudsi Date: Mon, 28 Jul 2025 03:01:33 +0530 Subject: [PATCH 1/3] feat(oidc): Add caching for client credentials token Caches the token to improve performance. Closes #10422. --- .../security/providers/oidc/OidcProvider.java | 75 ++++++++++++++----- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java index da0b3895833..842d9a51421 100644 --- a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java +++ b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java @@ -18,6 +18,7 @@ import java.lang.System.Logger.Level; import java.lang.annotation.Annotation; +import java.time.Instant; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; @@ -80,6 +81,11 @@ public final class OidcProvider implements AuthenticationProvider, OutboundSecurityProvider { private static final System.Logger LOGGER = System.getLogger(OidcProvider.class.getName()); + private record CachedToken(String accessToken, Instant expiration){ + boolean isValid(){ + return accessToken != null && Instant.now().isBefore(expiration); + } + } private final boolean optional; private final OidcConfig oidcConfig; private final List tenantIdFinders; @@ -88,6 +94,9 @@ public final class OidcProvider implements AuthenticationProvider, OutboundSecur private final OidcOutboundConfig outboundConfig; private final boolean useJwtGroups; private final LruCache tenantAuthHandlers = LruCache.create(); + private final ReentrantLock tokenLock = new ReentrantLock(); + private volatile CachedToken cachedToken; + private OidcProvider(Builder builder, OidcOutboundConfig oidcOutboundConfig) { this.optional = builder.optional; @@ -249,40 +258,70 @@ private OutboundSecurityResponse clientCredentials(ProviderRequest providerReque OidcOutboundTarget target = outboundConfig.findTarget(outboundEnv); boolean enabled = target.propagate; if (enabled) { - Parameters.Builder formBuilder = Parameters.builder("oidc-form-params") + if (clientAccessToken != null && Instant.now().isBefore(tokenExpiration)) { + Map> headers = new HashMap<>(outboundEnv.headers()); + target.tokenHandler.header(headers, clientAccessToken); + return OutboundSecurityResponse.withHeaders(headers); + } + + tokenLock.lock(); + + try { + if (clientAccessToken != null && Instant.now().isBefore(tokenExpiration)) { + Map> headers = new HashMap<>(outboundEnv.headers()); + target.tokenHandler.header(headers, clientAccessToken); + return OutboundSecurityResponse.withHeaders(headers); + } + + Parameters.Builder formBuilder = Parameters.builder("oidc-form-params") .add("grant_type", "client_credentials"); - if (!oidcConfig.baseScopes().isEmpty()) { - formBuilder.add("scope", oidcConfig.baseScopes()); - } + if (!oidcConfig.baseScopes().isEmpty()) { + formBuilder.add("scope", oidcConfig.baseScopes()); + } - HttpClientRequest postRequest = oidcConfig.appWebClient() + HttpClientRequest postRequest = oidcConfig.appWebClient() .post() .uri(oidcConfig.tokenEndpointUri()); - OidcUtil.updateRequest(OidcConfig.RequestType.ID_AND_SECRET_TO_TOKEN, oidcConfig, formBuilder, postRequest); + OidcUtil.updateRequest(OidcConfig.RequestType.ID_AND_SECRET_TO_TOKEN, oidcConfig, formBuilder, postRequest); - try (var response = postRequest.submit(formBuilder.build())) { - if (response.status().family() == Status.Family.SUCCESSFUL) { - JsonObject jsonObject = response.as(JsonObject.class); - String accessToken = jsonObject.getString("access_token"); + try (var response = postRequest.submit(formBuilder.build())) { + if (response.status().family() == Status.Family.SUCCESSFUL) { + JsonObject jsonObject = response.as(JsonObject.class); + String accessToken = jsonObject.getString("access_token"); - Map> headers = new HashMap<>(outboundEnv.headers()); - target.tokenHandler.header(headers, accessToken); - return OutboundSecurityResponse.withHeaders(headers); - } else { - return OutboundSecurityResponse.builder() + long expiresIn = jsonObject.getInt("expires_in", 3600); + this.clientAccessToken = accessToken; + this.tokenExpiration = Instant.now().plusSeconds(expiresIn - 30); + + Map> headers = new HashMap<>(outboundEnv.headers()); + target.tokenHandler.header(headers, accessToken); + return OutboundSecurityResponse.withHeaders(headers); + } else { + + this.clientAccessToken = null; + this.tokenExpiration = null; + return OutboundSecurityResponse.builder() .status(SecurityResponse.SecurityStatus.FAILURE) .description("Could not obtain access token from the identity server") .build(); - } - } catch (Exception e) { - return OutboundSecurityResponse.builder() + } + } catch (Exception e) { + + this.clientAccessToken = null; + this.tokenExpiration = null; + + return OutboundSecurityResponse.builder() .status(SecurityResponse.SecurityStatus.FAILURE) .description("An error occurred while obtaining access token from the identity server") .throwable(e) .build(); + } + } finally { + tokenLock.unlock(); } + } return OutboundSecurityResponse.empty(); } From 38b263f15a766ea7eea0f159fd2956ce8ccc6673 Mon Sep 17 00:00:00 2001 From: rudsi Date: Tue, 29 Jul 2025 23:49:03 +0530 Subject: [PATCH 2/3] resolved checkstyle --- .../security/providers/oidc/OidcProvider.java | 126 ++++++++++-------- 1 file changed, 70 insertions(+), 56 deletions(-) diff --git a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java index 842d9a51421..7f74ece2cf2 100644 --- a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java +++ b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java @@ -70,22 +70,27 @@ * * IDCS specific notes: *
    - *
  • If you want to use JWK to validate tokens, you must give access to the endpoint (by default only admin can access it)
  • - *
  • If you want to use introspect endpoint to validate tokens, you must give rights to the application to do so (Client + *
  • If you want to use JWK to validate tokens, you must give access to the + * endpoint (by default only admin can access it)
  • + *
  • If you want to use introspect endpoint to validate tokens, you must give + * rights to the application to do so (Client * Configuration/Allowed Operations)
  • - *
  • If you want to retrieve groups when using IDCS, you must add "Client Credentials" in "Allowed Grant Types" in - * application configuration, as well as "Grant the client access to Identity Cloud Service Admin APIs." configured to "User + *
  • If you want to retrieve groups when using IDCS, you must add "Client + * Credentials" in "Allowed Grant Types" in + * application configuration, as well as "Grant the client access to Identity + * Cloud Service Admin APIs." configured to "User * Administrator"
  • *
*/ public final class OidcProvider implements AuthenticationProvider, OutboundSecurityProvider { private static final System.Logger LOGGER = System.getLogger(OidcProvider.class.getName()); - private record CachedToken(String accessToken, Instant expiration){ - boolean isValid(){ + private record CachedToken(String accessToken, Instant expiration) { + boolean isValid() { return accessToken != null && Instant.now().isBefore(expiration); } } + private final boolean optional; private final OidcConfig oidcConfig; private final List tenantIdFinders; @@ -96,7 +101,6 @@ boolean isValid(){ private final LruCache tenantAuthHandlers = LruCache.create(); private final ReentrantLock tokenLock = new ReentrantLock(); private volatile CachedToken cachedToken; - private OidcProvider(Builder builder, OidcOutboundConfig oidcOutboundConfig) { this.optional = builder.optional; @@ -168,9 +172,9 @@ private AuthenticationResponse authenticateWithTenant(String tenantId, ProviderR .orElse(oidcConfig.tenantConfig(tenantId)); Tenant tenant = Tenant.create(oidcConfig, possibleConfig); TenantAuthenticationHandler handler = new TenantAuthenticationHandler(oidcConfig, - tenant, - useJwtGroups, - optional); + tenant, + useJwtGroups, + optional); return tenantAuthHandlers.computeValue(tenantId, () -> Optional.of(handler)).get() .authenticate(tenantId, providerRequest); } @@ -201,8 +205,8 @@ private String findTenantIdFromRedirects(ProviderRequest providerRequest) { } else { if (LOGGER.isLoggable(Level.DEBUG)) { LOGGER.log(Level.DEBUG, - "Missing tenant id, could not find in either of: " + missingLocations - + "Falling back to the default tenant id: " + DEFAULT_TENANT_ID); + "Missing tenant id, could not find in either of: " + missingLocations + + "Falling back to the default tenant id: " + DEFAULT_TENANT_ID); } return DEFAULT_TENANT_ID; } @@ -210,8 +214,8 @@ private String findTenantIdFromRedirects(ProviderRequest providerRequest) { @Override public boolean isOutboundSupported(ProviderRequest providerRequest, - SecurityEnvironment outboundEnv, - EndpointConfig outboundConfig) { + SecurityEnvironment outboundEnv, + EndpointConfig outboundConfig) { if (!propagate) { return false; } @@ -221,8 +225,8 @@ public boolean isOutboundSupported(ProviderRequest providerRequest, @Override public OutboundSecurityResponse outboundSecurity(ProviderRequest providerRequest, - SecurityEnvironment outboundEnv, - EndpointConfig outboundEndpointConfig) { + SecurityEnvironment outboundEnv, + EndpointConfig outboundEndpointConfig) { return switch (oidcConfig.outboundType()) { case USER_JWT -> propagateAccessToken(providerRequest, outboundEnv); case CLIENT_CREDENTIALS -> clientCredentials(providerRequest, outboundEnv); @@ -230,7 +234,7 @@ public OutboundSecurityResponse outboundSecurity(ProviderRequest providerRequest } private OutboundSecurityResponse propagateAccessToken(ProviderRequest providerRequest, - SecurityEnvironment outboundEnv) { + SecurityEnvironment outboundEnv) { Optional user = providerRequest.securityContext().user(); if (user.isPresent()) { @@ -254,7 +258,8 @@ private OutboundSecurityResponse propagateAccessToken(ProviderRequest providerRe return OutboundSecurityResponse.empty(); } - private OutboundSecurityResponse clientCredentials(ProviderRequest providerRequest, SecurityEnvironment outboundEnv) { + private OutboundSecurityResponse clientCredentials(ProviderRequest providerRequest, + SecurityEnvironment outboundEnv) { OidcOutboundTarget target = outboundConfig.findTarget(outboundEnv); boolean enabled = target.propagate; if (enabled) { @@ -274,17 +279,18 @@ private OutboundSecurityResponse clientCredentials(ProviderRequest providerReque } Parameters.Builder formBuilder = Parameters.builder("oidc-form-params") - .add("grant_type", "client_credentials"); + .add("grant_type", "client_credentials"); if (!oidcConfig.baseScopes().isEmpty()) { formBuilder.add("scope", oidcConfig.baseScopes()); } HttpClientRequest postRequest = oidcConfig.appWebClient() - .post() - .uri(oidcConfig.tokenEndpointUri()); + .post() + .uri(oidcConfig.tokenEndpointUri()); - OidcUtil.updateRequest(OidcConfig.RequestType.ID_AND_SECRET_TO_TOKEN, oidcConfig, formBuilder, postRequest); + OidcUtil.updateRequest(OidcConfig.RequestType.ID_AND_SECRET_TO_TOKEN, oidcConfig, formBuilder, + postRequest); try (var response = postRequest.submit(formBuilder.build())) { if (response.status().family() == Status.Family.SUCCESSFUL) { @@ -303,9 +309,9 @@ private OutboundSecurityResponse clientCredentials(ProviderRequest providerReque this.clientAccessToken = null; this.tokenExpiration = null; return OutboundSecurityResponse.builder() - .status(SecurityResponse.SecurityStatus.FAILURE) - .description("Could not obtain access token from the identity server") - .build(); + .status(SecurityResponse.SecurityStatus.FAILURE) + .description("Could not obtain access token from the identity server") + .build(); } } catch (Exception e) { @@ -313,10 +319,10 @@ private OutboundSecurityResponse clientCredentials(ProviderRequest providerReque this.tokenExpiration = null; return OutboundSecurityResponse.builder() - .status(SecurityResponse.SecurityStatus.FAILURE) - .description("An error occurred while obtaining access token from the identity server") - .throwable(e) - .build(); + .status(SecurityResponse.SecurityStatus.FAILURE) + .description("An error occurred while obtaining access token from the identity server") + .throwable(e) + .build(); } } finally { tokenLock.unlock(); @@ -329,9 +335,8 @@ private OutboundSecurityResponse clientCredentials(ProviderRequest providerReque /** * Builder for {@link OidcProvider}. */ - @Configured(prefix = OidcProviderService.PROVIDER_CONFIG_KEY, - description = "Open ID Connect security provider", - provides = {AuthenticationProvider.class, SecurityProvider.class}) + @Configured(prefix = OidcProviderService.PROVIDER_CONFIG_KEY, description = "Open ID Connect security provider", provides = { + AuthenticationProvider.class, SecurityProvider.class }) public static final class Builder implements io.helidon.common.Builder { private static final int BUILDER_WEIGHT = 300; @@ -347,7 +352,8 @@ public static final class Builder implements io.helidon.common.Builder tenantIdFinders; private List tenantConfigFinders; - // identity propagation is disabled by default. In general we should not reuse the same token + // identity propagation is disabled by default. In general we should not reuse + // the same token // for outbound calls, unless it is the same audience private Boolean propagate; private boolean useJwtGroups = true; @@ -390,28 +396,34 @@ public OidcProvider build() { * * * - * - * - * + * + * + * * * - * - * - * + * + * + * * * - * - * - * + * + * + * * * - * - * - * + * + * + * * *
Optional configuration parameters
keydefault valuedescriptionkeydefault valuedescription
  The current config node is used to construct {@link io.helidon.security.providers.oidc.common.OidcConfig}.  The current config node is used to construct + * {@link io.helidon.security.providers.oidc.common.OidcConfig}.
propagatefalseWhether to propagate token (overall configuration). If set to false, propagation will - * not be done at all.propagatefalseWhether to propagate token (overall configuration). If set to false, + * propagation will + * not be done at all.
outbound Configuration of {@link io.helidon.security.providers.common.OutboundConfig}. - * In addition you can use {@code propagate} to disable propagation for an outbound target, - * and {@code token} to configure outbound {@link io.helidon.security.util.TokenHandler} for an - * outbound target. Default token handler uses {@code Authorization} header with a {@code bearer } prefixoutbound Configuration of + * {@link io.helidon.security.providers.common.OutboundConfig}. + * In addition you can use {@code propagate} to disable propagation for an + * outbound target, + * and {@code token} to configure outbound + * {@link io.helidon.security.util.TokenHandler} for an + * outbound target. Default token handler uses {@code Authorization} header with + * a {@code bearer } prefix
* @@ -428,7 +440,8 @@ public Builder config(Config config) { } config.get("propagate").asBoolean().ifPresent(this::propagate); if (null == outboundConfig) { - // the OutboundConfig.create() expects the provider configuration, not the outbound configuration + // the OutboundConfig.create() expects the provider configuration, not the + // outbound configuration Config outboundConfig = config.get("outbound"); if (outboundConfig.exists()) { outboundConfig(OutboundConfig.create(config)); @@ -494,9 +507,11 @@ public Builder optional(boolean optional) { /** * Claim {@code groups} from JWT will be used to automatically add - * groups to current subject (may be used with {@link jakarta.annotation.security.RolesAllowed} annotation). + * groups to current subject (may be used with + * {@link jakarta.annotation.security.RolesAllowed} annotation). * - * @param useJwtGroups whether to use {@code groups} claim from JWT to retrieve roles + * @param useJwtGroups whether to use {@code groups} claim from JWT to retrieve + * roles * @return updated builder instance */ @ConfiguredOption("true") @@ -529,7 +544,6 @@ public Builder discoverTenantIdProviders(boolean discoverIdProviders) { return this; } - /** * Add specific {@link TenantConfigFinder} implementation. * Priority {@link #BUILDER_WEIGHT} is used. @@ -542,10 +556,11 @@ public Builder addTenantConfigFinder(TenantConfigFinder configFinder) { } /** - * Add specific {@link TenantConfigFinder} implementation with specific priority. + * Add specific {@link TenantConfigFinder} implementation with specific + * priority. * * @param configFinder config finder implementation - * @param priority finder priority + * @param priority finder priority * @return updated builder instance */ public Builder addTenantConfigFinder(TenantConfigFinder configFinder, int priority) { @@ -629,4 +644,3 @@ private OidcOutboundTarget(boolean propagate, TokenHandler handler) { } } } - From 1d748de6a14a1f37b024ffa3eafd72cf1135119b Mon Sep 17 00:00:00 2001 From: rudsi Date: Wed, 30 Jul 2025 07:42:53 +0530 Subject: [PATCH 3/3] removed unused variables from earlier approach --- .../security/providers/oidc/OidcProvider.java | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java index 7f74ece2cf2..9634641124f 100644 --- a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java +++ b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java @@ -263,18 +263,20 @@ private OutboundSecurityResponse clientCredentials(ProviderRequest providerReque OidcOutboundTarget target = outboundConfig.findTarget(outboundEnv); boolean enabled = target.propagate; if (enabled) { - if (clientAccessToken != null && Instant.now().isBefore(tokenExpiration)) { + CachedToken token = cachedToken; + if (token != null && token.isValid()) { Map> headers = new HashMap<>(outboundEnv.headers()); - target.tokenHandler.header(headers, clientAccessToken); + target.tokenHandler.header(headers, token.accessToken()); return OutboundSecurityResponse.withHeaders(headers); } tokenLock.lock(); try { - if (clientAccessToken != null && Instant.now().isBefore(tokenExpiration)) { + token = cachedToken; + if (token != null && token.isValid()) { Map> headers = new HashMap<>(outboundEnv.headers()); - target.tokenHandler.header(headers, clientAccessToken); + target.tokenHandler.header(headers, token.accessToken()); return OutboundSecurityResponse.withHeaders(headers); } @@ -296,28 +298,20 @@ private OutboundSecurityResponse clientCredentials(ProviderRequest providerReque if (response.status().family() == Status.Family.SUCCESSFUL) { JsonObject jsonObject = response.as(JsonObject.class); String accessToken = jsonObject.getString("access_token"); - long expiresIn = jsonObject.getInt("expires_in", 3600); - this.clientAccessToken = accessToken; - this.tokenExpiration = Instant.now().plusSeconds(expiresIn - 30); - + this.cachedToken = new CachedToken(accessToken, Instant.now().plusSeconds(expiresIn - 30)); Map> headers = new HashMap<>(outboundEnv.headers()); target.tokenHandler.header(headers, accessToken); return OutboundSecurityResponse.withHeaders(headers); } else { - - this.clientAccessToken = null; - this.tokenExpiration = null; + this.cachedToken = null; return OutboundSecurityResponse.builder() .status(SecurityResponse.SecurityStatus.FAILURE) .description("Could not obtain access token from the identity server") .build(); } } catch (Exception e) { - - this.clientAccessToken = null; - this.tokenExpiration = null; - + this.cachedToken = null; return OutboundSecurityResponse.builder() .status(SecurityResponse.SecurityStatus.FAILURE) .description("An error occurred while obtaining access token from the identity server")