This is an automated email from the ASF dual-hosted git repository. luigidemasi pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
commit b658dba63b945034c279e3300ece2e13599f2a9f Author: Luigi De Masi <[email protected]> AuthorDate: Tue Jun 9 15:01:50 2026 +0200 CAMEL-23685: Refine OAuth token validation factory resolution Move OAuth token validation profile property resolution out of the SPI interface, add registry-based validation factory lookup with profile-specific bean references, and document the validation result property, JWKS cache scope, and refresh token grant request parameter correction. --- components/camel-oauth/src/main/docs/oauth.adoc | 15 ++- .../java/org/apache/camel/oauth/JwksCache.java | 3 + .../DefaultOAuthTokenValidationFactoryTest.java | 4 +- .../src/main/docs/platform-http-component.adoc | 11 ++ .../platform/http/PlatformHttpConstants.java | 7 + .../platform/http/PlatformHttpEndpoint.java | 3 +- .../http/PlatformHttpOAuthProfileTest.java | 139 ++++++++++++++++++++ .../spi/OAuthTokenValidationConfigResolver.java | 144 +++++++++++++++++++++ .../camel/spi/OAuthTokenValidationFactory.java | 122 +---------------- .../java/org/apache/camel/support/OAuthHelper.java | 65 +++++++++- .../ROOT/pages/camel-4x-upgrade-guide-4_21.adoc | 9 ++ 11 files changed, 399 insertions(+), 123 deletions(-) diff --git a/components/camel-oauth/src/main/docs/oauth.adoc b/components/camel-oauth/src/main/docs/oauth.adoc index 647b48d372a3..6f9324ff9895 100644 --- a/components/camel-oauth/src/main/docs/oauth.adoc +++ b/components/camel-oauth/src/main/docs/oauth.adoc @@ -280,6 +280,19 @@ camel.oauth.myprofile.connect-timeout-seconds=5 camel.oauth.myprofile.read-timeout-seconds=10 ---- +To use a runtime-specific or application-specific validator for one profile, bind an +`OAuthTokenValidationFactory` bean and reference it from the profile: + +[source,properties] +---- +camel.oauth.myprofile.validation-factory=#bean:myTokenValidationFactory +---- + +For the default unnamed profile, use `camel.oauth.validation-factory`. + +If this property is configured, the referenced bean must exist and implement +`OAuthTokenValidationFactory`; otherwise the route fails to start. + Use HTTPS endpoints for OIDC discovery, JWKS, and introspection in production. Plain HTTP endpoints are rejected by default; set `allow-insecure-http=true` only for local testing or other trusted development environments. @@ -341,7 +354,7 @@ import org.apache.camel.spi.OAuthTokenValidationFactory; import org.apache.camel.spi.OAuthTokenValidationResult; import org.apache.camel.support.OAuthHelper; -OAuthTokenValidationFactory factory = OAuthHelper.resolveOAuthTokenValidationFactory(camelContext); +OAuthTokenValidationFactory factory = OAuthHelper.resolveOAuthTokenValidationFactory(camelContext, "myprofile"); OAuthTokenValidationConfig config = new OAuthTokenValidationConfig() .setJwksEndpoint("https://idp.example.com/.well-known/jwks.json") diff --git a/components/camel-oauth/src/main/java/org/apache/camel/oauth/JwksCache.java b/components/camel-oauth/src/main/java/org/apache/camel/oauth/JwksCache.java index e363cfb54df3..252a4ee9ffc6 100644 --- a/components/camel-oauth/src/main/java/org/apache/camel/oauth/JwksCache.java +++ b/components/camel-oauth/src/main/java/org/apache/camel/oauth/JwksCache.java @@ -30,6 +30,9 @@ import org.slf4j.LoggerFactory; /** * Thread-safe JWKS cache with TTL and key-rotation-aware re-fetch. + * <p/> + * The cache is a JVM-wide singleton shared by all Camel contexts in the same classloader. Entries are keyed by JWKS + * endpoint URL and contain public key material only. */ final class JwksCache { diff --git a/components/camel-oauth/src/test/java/org/apache/camel/oauth/DefaultOAuthTokenValidationFactoryTest.java b/components/camel-oauth/src/test/java/org/apache/camel/oauth/DefaultOAuthTokenValidationFactoryTest.java index 996f568f0fd2..e1fbee9476bd 100644 --- a/components/camel-oauth/src/test/java/org/apache/camel/oauth/DefaultOAuthTokenValidationFactoryTest.java +++ b/components/camel-oauth/src/test/java/org/apache/camel/oauth/DefaultOAuthTokenValidationFactoryTest.java @@ -52,7 +52,7 @@ import org.apache.camel.CamelContext; import org.apache.camel.component.properties.PropertiesComponent; import org.apache.camel.impl.DefaultCamelContext; import org.apache.camel.spi.OAuthTokenValidationConfig; -import org.apache.camel.spi.OAuthTokenValidationFactory; +import org.apache.camel.spi.OAuthTokenValidationConfigResolver; import org.apache.camel.spi.OAuthTokenValidationResult; import org.apache.camel.spi.OAuthTokenValidationResult.ErrorCode; import org.junit.jupiter.api.AfterEach; @@ -293,7 +293,7 @@ class DefaultOAuthTokenValidationFactoryTest { context.setPropertiesComponent(pc); context.start(); - OAuthTokenValidationConfig config = OAuthTokenValidationFactory.resolveProfileConfig(context, "myprofile"); + OAuthTokenValidationConfig config = OAuthTokenValidationConfigResolver.resolveProfileConfig(context, "myprofile"); assertEquals("at+jwt", config.getExpectedTokenType()); } diff --git a/components/camel-platform-http/src/main/docs/platform-http-component.adoc b/components/camel-platform-http/src/main/docs/platform-http-component.adoc index 4449a9b1e22c..dfdfe11bd857 100644 --- a/components/camel-platform-http/src/main/docs/platform-http-component.adoc +++ b/components/camel-platform-http/src/main/docs/platform-http-component.adoc @@ -96,6 +96,17 @@ the route fails to start. Add `camel-oauth` for the default provider or include provider from the platform integration. ==== +Camel first checks whether the selected profile has a profile-specific validation factory: + +[source,properties] +---- +camel.oauth.myprofile.validation-factory=#bean:myTokenValidationFactory +---- + +If this property is set, the referenced bean must exist and implement `OAuthTokenValidationFactory`. +Otherwise, Camel looks for a single `OAuthTokenValidationFactory` in the registry before falling back +to the classpath provider. + [source,java] ---- from("platform-http:/secure?oauthProfile=myprofile") diff --git a/components/camel-platform-http/src/main/java/org/apache/camel/component/platform/http/PlatformHttpConstants.java b/components/camel-platform-http/src/main/java/org/apache/camel/component/platform/http/PlatformHttpConstants.java index af155f605d2c..4b03a2389377 100644 --- a/components/camel-platform-http/src/main/java/org/apache/camel/component/platform/http/PlatformHttpConstants.java +++ b/components/camel-platform-http/src/main/java/org/apache/camel/component/platform/http/PlatformHttpConstants.java @@ -16,11 +16,18 @@ */ package org.apache.camel.component.platform.http; +import org.apache.camel.Exchange; + public final class PlatformHttpConstants { public static final String PLATFORM_HTTP_COMPONENT_NAME = "platform-http"; public static final String PLATFORM_HTTP_ENGINE_NAME = "platform-http-engine"; public static final String PLATFORM_HTTP_ENGINE_FACTORY = "platform-http-engine"; + /** + * Exchange property containing the OAuth token validation result for successfully authenticated platform-http + * requests. This intentionally does not use {@link Exchange#AUTHENTICATION}, so OAuth bearer-token validation does + * not overwrite or conflict with other Camel authentication mechanisms. + */ public static final String OAUTH_TOKEN_VALIDATION_RESULT = "CamelOAuthTokenValidationResult"; private PlatformHttpConstants() { diff --git a/components/camel-platform-http/src/main/java/org/apache/camel/component/platform/http/PlatformHttpEndpoint.java b/components/camel-platform-http/src/main/java/org/apache/camel/component/platform/http/PlatformHttpEndpoint.java index e8a3c017d324..3d19db493e67 100644 --- a/components/camel-platform-http/src/main/java/org/apache/camel/component/platform/http/PlatformHttpEndpoint.java +++ b/components/camel-platform-http/src/main/java/org/apache/camel/component/platform/http/PlatformHttpEndpoint.java @@ -201,7 +201,8 @@ public class PlatformHttpEndpoint extends DefaultEndpoint protected PlatformHttpSecurityHandler createSecurityHandler() throws Exception { if (ObjectHelper.isNotEmpty(oauthProfile)) { - OAuthTokenValidationFactory factory = OAuthHelper.resolveOAuthTokenValidationFactory(getCamelContext()); + OAuthTokenValidationFactory factory + = OAuthHelper.resolveOAuthTokenValidationFactory(getCamelContext(), oauthProfile); factory.validateConfiguration(getCamelContext(), oauthProfile); return new OAuthPlatformHttpSecurityHandler(oauthProfile, factory); } diff --git a/components/camel-platform-http/src/test/java/org/apache/camel/component/platform/http/PlatformHttpOAuthProfileTest.java b/components/camel-platform-http/src/test/java/org/apache/camel/component/platform/http/PlatformHttpOAuthProfileTest.java index f19968bd9ac2..eb566b62ee2f 100644 --- a/components/camel-platform-http/src/test/java/org/apache/camel/component/platform/http/PlatformHttpOAuthProfileTest.java +++ b/components/camel-platform-http/src/test/java/org/apache/camel/component/platform/http/PlatformHttpOAuthProfileTest.java @@ -17,7 +17,9 @@ package org.apache.camel.component.platform.http; import java.util.List; +import java.util.Map; +import org.apache.camel.CamelContext; import org.apache.camel.Endpoint; import org.apache.camel.Exchange; import org.apache.camel.Processor; @@ -27,6 +29,8 @@ import org.apache.camel.component.platform.http.spi.PlatformHttpConsumer; import org.apache.camel.component.platform.http.spi.PlatformHttpEngine; import org.apache.camel.component.platform.http.spi.PlatformHttpSecurityHandler; import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.spi.OAuthTokenValidationConfig; +import org.apache.camel.spi.OAuthTokenValidationFactory; import org.apache.camel.spi.OAuthTokenValidationResult; import org.apache.camel.support.DefaultConsumer; import org.apache.camel.support.DefaultExchange; @@ -46,6 +50,7 @@ class PlatformHttpOAuthProfileTest { @BeforeEach void resetStubFactory() { StubOAuthTokenValidationFactory.reset(); + ProfileBeanOAuthTokenValidationFactory.reset(); } @Test @@ -96,6 +101,111 @@ class PlatformHttpOAuthProfileTest { }); } + @Test + void oauthProfileUsesProfileSpecificValidationFactoryBean() throws Exception { + CapturingEngine engine = new CapturingEngine(); + + try (DefaultCamelContext context = new DefaultCamelContext()) { + context.getRegistry().bind("profileFactory", new ProfileBeanOAuthTokenValidationFactory()); + context.getPropertiesComponent().addInitialProperty( + "camel.oauth.myprofile.validation-factory", "#bean:profileFactory"); + + PlatformHttpComponent component = new PlatformHttpComponent(); + component.setEngine(engine); + context.addComponent("platform-http", component); + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("platform-http:/secure?oauthProfile=myprofile") + .setBody().constant("secured"); + } + }); + + context.start(); + + assertEquals("myprofile", ProfileBeanOAuthTokenValidationFactory.lastConfigurationProfileName); + assertNull(StubOAuthTokenValidationFactory.lastConfigurationProfileName); + assertNotNull(engine.securityHandler); + } + } + + @Test + void oauthProfileUsesSingleRegistryValidationFactoryBean() throws Exception { + CapturingEngine engine = new CapturingEngine(); + + try (DefaultCamelContext context = new DefaultCamelContext()) { + context.getRegistry().bind("registryFactory", new ProfileBeanOAuthTokenValidationFactory()); + + PlatformHttpComponent component = new PlatformHttpComponent(); + component.setEngine(engine); + context.addComponent("platform-http", component); + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("platform-http:/secure?oauthProfile=myprofile") + .setBody().constant("secured"); + } + }); + + context.start(); + + assertEquals("myprofile", ProfileBeanOAuthTokenValidationFactory.lastConfigurationProfileName); + assertNull(StubOAuthTokenValidationFactory.lastConfigurationProfileName); + assertNotNull(engine.securityHandler); + } + } + + @Test + void oauthProfileSpecificMissingValidationFactoryBeanFailsStartup() { + CapturingEngine engine = new CapturingEngine(); + + assertThrows(Exception.class, () -> { + try (DefaultCamelContext context = new DefaultCamelContext()) { + context.getPropertiesComponent().addInitialProperty( + "camel.oauth.myprofile.validation-factory", "#bean:missingFactory"); + + PlatformHttpComponent component = new PlatformHttpComponent(); + component.setEngine(engine); + context.addComponent("platform-http", component); + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("platform-http:/secure?oauthProfile=myprofile") + .setBody().constant("secured"); + } + }); + + context.start(); + } + }); + } + + @Test + void oauthProfileSpecificWrongTypeValidationFactoryBeanFailsStartup() { + CapturingEngine engine = new CapturingEngine(); + + assertThrows(Exception.class, () -> { + try (DefaultCamelContext context = new DefaultCamelContext()) { + context.getRegistry().bind("profileFactory", "not a token validation factory"); + context.getPropertiesComponent().addInitialProperty( + "camel.oauth.myprofile.validation-factory", "#bean:profileFactory"); + + PlatformHttpComponent component = new PlatformHttpComponent(); + component.setEngine(engine); + context.addComponent("platform-http", component); + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("platform-http:/secure?oauthProfile=myprofile") + .setBody().constant("secured"); + } + }); + + context.start(); + } + }); + } + @Test void fallbackSecurityHandlerRejectsMissingBearerToken() throws Exception { OAuthPlatformHttpSecurityHandler handler = new OAuthPlatformHttpSecurityHandler("myprofile"); @@ -285,4 +395,33 @@ class PlatformHttpOAuthProfileTest { return (PlatformHttpEndpoint) super.getEndpoint(); } } + + private static final class ProfileBeanOAuthTokenValidationFactory implements OAuthTokenValidationFactory { + + private static String lastConfigurationProfileName; + + private static void reset() { + lastConfigurationProfileName = null; + } + + @Override + public OAuthTokenValidationResult validateToken(OAuthTokenValidationConfig config, String token) { + return OAuthTokenValidationResult.valid( + "profile-bean-user", + "https://issuer.example", + List.of("camel-api"), + List.of("read"), + Map.of(), + 0); + } + + @Override + public void validateConfiguration(OAuthTokenValidationConfig config) { + } + + @Override + public void validateConfiguration(CamelContext context, String profileName) { + lastConfigurationProfileName = profileName; + } + } } diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/OAuthTokenValidationConfigResolver.java b/core/camel-api/src/main/java/org/apache/camel/spi/OAuthTokenValidationConfigResolver.java new file mode 100644 index 000000000000..2be9e66186aa --- /dev/null +++ b/core/camel-api/src/main/java/org/apache/camel/spi/OAuthTokenValidationConfigResolver.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.spi; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.camel.CamelContext; + +/** + * Resolves {@link OAuthTokenValidationConfig} instances from Camel OAuth profile properties. + * + * @since 4.21 + */ +public final class OAuthTokenValidationConfigResolver { + + private OAuthTokenValidationConfigResolver() { + } + + /** + * Resolves a named token validation profile from Camel properties. + * + * @param context the CamelContext to resolve properties from + * @param profileName the named profile + * @return the resolved profile configuration + * @since 4.21 + */ + public static OAuthTokenValidationConfig resolveProfileConfig(CamelContext context, String profileName) { + return resolveProfileConfigWithPrefix(context, "camel.oauth." + profileName + "."); + } + + /** + * Resolves the default token validation profile from Camel properties. + * + * @param context the CamelContext to resolve properties from + * @return the resolved profile configuration + * @since 4.21 + */ + public static OAuthTokenValidationConfig resolveDefaultProfileConfig(CamelContext context) { + return resolveProfileConfigWithPrefix(context, "camel.oauth."); + } + + private static OAuthTokenValidationConfig resolveProfileConfigWithPrefix(CamelContext context, String prefix) { + OAuthTokenValidationConfig config = new OAuthTokenValidationConfig(); + + resolveOptionalProperty(context, prefix + "jwks-endpoint") + .ifPresent(config::setJwksEndpoint); + resolveOptionalProperty(context, prefix + "introspection-endpoint") + .ifPresent(config::setIntrospectionEndpoint); + + resolveOptionalProperty(context, prefix + "introspection-client-id") + .or(() -> resolveOptionalProperty(context, prefix + "client-id")) + .ifPresent(config::setIntrospectionClientId); + resolveOptionalProperty(context, prefix + "introspection-client-secret") + .or(() -> resolveOptionalProperty(context, prefix + "client-secret")) + .ifPresent(config::setIntrospectionClientSecret); + + resolveOptionalProperty(context, prefix + "expected-issuer") + .ifPresent(config::setExpectedIssuer); + resolveOptionalProperty(context, prefix + "expected-audience") + .map(OAuthTokenValidationConfigResolver::parseStringSet) + .ifPresent(config::setExpectedAudiences); + resolveOptionalProperty(context, prefix + "expected-token-type") + .ifPresent(config::setExpectedTokenType); + resolveOptionalProperty(context, prefix + "clock-skew-seconds") + .map(Integer::parseInt) + .ifPresent(config::setClockSkewSeconds); + resolveOptionalProperty(context, prefix + "jwks-cache-ttl-seconds") + .map(Long::parseLong) + .ifPresent(config::setJwksCacheTtlSeconds); + resolveOptionalProperty(context, prefix + "oidc-discovery-cache-ttl-seconds") + .map(Long::parseLong) + .ifPresent(config::setOidcDiscoveryCacheTtlSeconds); + resolveOptionalProperty(context, prefix + "connect-timeout-seconds") + .map(Integer::parseInt) + .ifPresent(config::setConnectTimeoutSeconds); + resolveOptionalProperty(context, prefix + "read-timeout-seconds") + .map(Integer::parseInt) + .ifPresent(config::setReadTimeoutSeconds); + resolveOptionalProperty(context, prefix + "require-expiration") + .map(Boolean::parseBoolean) + .ifPresent(config::setRequireExpiration); + resolveOptionalProperty(context, prefix + "allowed-jws-algorithms") + .map(OAuthTokenValidationConfigResolver::parseStringSet) + .filter(algorithms -> !algorithms.isEmpty()) + .ifPresent(config::setAllowedJwsAlgorithms); + resolveOptionalProperty(context, prefix + "allow-missing-audience") + .map(Boolean::parseBoolean) + .ifPresent(config::setAllowMissingAudience); + resolveOptionalProperty(context, prefix + "allow-missing-issuer") + .map(Boolean::parseBoolean) + .ifPresent(config::setAllowMissingIssuer); + resolveOptionalProperty(context, prefix + "allow-insecure-http") + .map(Boolean::parseBoolean) + .ifPresent(config::setAllowInsecureHttp); + + resolveOptionalProperty(context, prefix + "base-uri") + .map(String::trim) + .filter(baseUri -> !baseUri.isEmpty()) + .ifPresent(baseUri -> { + String normalizedBaseUri = removeTrailingSlash(baseUri); + if (config.getExpectedIssuer() == null) { + config.setExpectedIssuer(normalizedBaseUri); + } + config.setOidcDiscoveryUrl(normalizedBaseUri + "/.well-known/openid-configuration"); + }); + + return config; + } + + private static Optional<String> resolveOptionalProperty(CamelContext context, String key) { + return context.getPropertiesComponent().resolveProperty(key); + } + + private static Set<String> parseStringSet(String value) { + LinkedHashSet<String> values = Arrays.stream(value.split(",")) + .map(String::trim) + .filter(entry -> !entry.isEmpty()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + return Collections.unmodifiableSet(values); + } + + private static String removeTrailingSlash(String uri) { + return uri.endsWith("/") ? uri.substring(0, uri.length() - 1) : uri; + } +} diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/OAuthTokenValidationFactory.java b/core/camel-api/src/main/java/org/apache/camel/spi/OAuthTokenValidationFactory.java index 40835dc4da42..60319d90b380 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/OAuthTokenValidationFactory.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/OAuthTokenValidationFactory.java @@ -16,13 +16,6 @@ */ package org.apache.camel.spi; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - import org.apache.camel.CamelContext; /** @@ -114,7 +107,7 @@ public interface OAuthTokenValidationFactory { * @since 4.21 */ default OAuthTokenValidationResult validateToken(CamelContext context, String profileName, String token) { - return validateToken(resolveProfileConfig(context, profileName), token); + return validateToken(OAuthTokenValidationConfigResolver.resolveProfileConfig(context, profileName), token); } /** @@ -130,7 +123,7 @@ public interface OAuthTokenValidationFactory { * @since 4.21 */ default OAuthTokenValidationResult validateToken(CamelContext context, String token) { - return validateToken(resolveDefaultProfileConfig(context), token); + return validateToken(OAuthTokenValidationConfigResolver.resolveDefaultProfileConfig(context), token); } /** @@ -154,7 +147,7 @@ public interface OAuthTokenValidationFactory { * @since 4.21 */ default void validateConfiguration(CamelContext context, String profileName) { - validateConfiguration(resolveProfileConfig(context, profileName)); + validateConfiguration(OAuthTokenValidationConfigResolver.resolveProfileConfig(context, profileName)); } /** @@ -165,113 +158,6 @@ public interface OAuthTokenValidationFactory { * @since 4.21 */ default void validateConfiguration(CamelContext context) { - validateConfiguration(resolveDefaultProfileConfig(context)); - } - - /** - * Resolves a named token validation profile from Camel properties. - * - * @param context the CamelContext to resolve properties from - * @param profileName the named profile - * @return the resolved profile configuration - * @since 4.21 - */ - static OAuthTokenValidationConfig resolveProfileConfig(CamelContext context, String profileName) { - return resolveProfileConfigWithPrefix(context, "camel.oauth." + profileName + "."); - } - - /** - * Resolves the default token validation profile from Camel properties. - * - * @param context the CamelContext to resolve properties from - * @return the resolved profile configuration - * @since 4.21 - */ - static OAuthTokenValidationConfig resolveDefaultProfileConfig(CamelContext context) { - return resolveProfileConfigWithPrefix(context, "camel.oauth."); - } - - private static OAuthTokenValidationConfig resolveProfileConfigWithPrefix(CamelContext context, String prefix) { - OAuthTokenValidationConfig config = new OAuthTokenValidationConfig(); - - resolveOptionalProperty(context, prefix + "jwks-endpoint") - .ifPresent(config::setJwksEndpoint); - resolveOptionalProperty(context, prefix + "introspection-endpoint") - .ifPresent(config::setIntrospectionEndpoint); - - resolveOptionalProperty(context, prefix + "introspection-client-id") - .or(() -> resolveOptionalProperty(context, prefix + "client-id")) - .ifPresent(config::setIntrospectionClientId); - resolveOptionalProperty(context, prefix + "introspection-client-secret") - .or(() -> resolveOptionalProperty(context, prefix + "client-secret")) - .ifPresent(config::setIntrospectionClientSecret); - - resolveOptionalProperty(context, prefix + "expected-issuer") - .ifPresent(config::setExpectedIssuer); - resolveOptionalProperty(context, prefix + "expected-audience") - .map(OAuthTokenValidationFactory::parseStringSet) - .ifPresent(config::setExpectedAudiences); - resolveOptionalProperty(context, prefix + "expected-token-type") - .ifPresent(config::setExpectedTokenType); - resolveOptionalProperty(context, prefix + "clock-skew-seconds") - .map(Integer::parseInt) - .ifPresent(config::setClockSkewSeconds); - resolveOptionalProperty(context, prefix + "jwks-cache-ttl-seconds") - .map(Long::parseLong) - .ifPresent(config::setJwksCacheTtlSeconds); - resolveOptionalProperty(context, prefix + "oidc-discovery-cache-ttl-seconds") - .map(Long::parseLong) - .ifPresent(config::setOidcDiscoveryCacheTtlSeconds); - resolveOptionalProperty(context, prefix + "connect-timeout-seconds") - .map(Integer::parseInt) - .ifPresent(config::setConnectTimeoutSeconds); - resolveOptionalProperty(context, prefix + "read-timeout-seconds") - .map(Integer::parseInt) - .ifPresent(config::setReadTimeoutSeconds); - resolveOptionalProperty(context, prefix + "require-expiration") - .map(Boolean::parseBoolean) - .ifPresent(config::setRequireExpiration); - resolveOptionalProperty(context, prefix + "allowed-jws-algorithms") - .map(OAuthTokenValidationFactory::parseStringSet) - .filter(algorithms -> !algorithms.isEmpty()) - .ifPresent(config::setAllowedJwsAlgorithms); - resolveOptionalProperty(context, prefix + "allow-missing-audience") - .map(Boolean::parseBoolean) - .ifPresent(config::setAllowMissingAudience); - resolveOptionalProperty(context, prefix + "allow-missing-issuer") - .map(Boolean::parseBoolean) - .ifPresent(config::setAllowMissingIssuer); - resolveOptionalProperty(context, prefix + "allow-insecure-http") - .map(Boolean::parseBoolean) - .ifPresent(config::setAllowInsecureHttp); - - resolveOptionalProperty(context, prefix + "base-uri") - .map(String::trim) - .filter(baseUri -> !baseUri.isEmpty()) - .ifPresent(baseUri -> { - String normalizedBaseUri = removeTrailingSlash(baseUri); - if (config.getExpectedIssuer() == null) { - config.setExpectedIssuer(normalizedBaseUri); - } - config.setOidcDiscoveryUrl(normalizedBaseUri + "/.well-known/openid-configuration"); - }); - - return config; - } - - private static Optional<String> resolveOptionalProperty(CamelContext context, String key) { - return context.getPropertiesComponent().resolveProperty(key); - } - - private static Set<String> parseStringSet(String value) { - LinkedHashSet<String> values = Arrays.stream(value.split(",")) - .map(String::trim) - .filter(entry -> !entry.isEmpty()) - .collect(Collectors.toCollection(LinkedHashSet::new)); - return Collections.unmodifiableSet(values); - } - - private static String removeTrailingSlash(String uri) { - return uri.endsWith("/") ? uri.substring(0, uri.length() - 1) : uri; + validateConfiguration(OAuthTokenValidationConfigResolver.resolveDefaultProfileConfig(context)); } } diff --git a/core/camel-support/src/main/java/org/apache/camel/support/OAuthHelper.java b/core/camel-support/src/main/java/org/apache/camel/support/OAuthHelper.java index 4663babb529b..9ad7689240a6 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/OAuthHelper.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/OAuthHelper.java @@ -17,6 +17,7 @@ package org.apache.camel.support; import org.apache.camel.CamelContext; +import org.apache.camel.CamelContextAware; import org.apache.camel.spi.OAuthClientAuthenticationFactory; import org.apache.camel.spi.OAuthTokenValidationFactory; import org.apache.camel.spi.OAuthTokenValidationResult; @@ -31,6 +32,9 @@ import org.apache.camel.spi.OAuthTokenValidationResult; */ public final class OAuthHelper { + private static final String OAUTH_PROFILE_PREFIX = "camel.oauth."; + private static final String VALIDATION_FACTORY_PROPERTY = "validation-factory"; + private OAuthHelper() { } @@ -55,12 +59,71 @@ public final class OAuthHelper { /** * Resolves the OAuth token validation factory. + * <p/> + * The default profile can reference a registry bean with {@code camel.oauth.validation-factory}. If configured, the + * referenced bean must exist and implement {@link OAuthTokenValidationFactory}. * * @param context the CamelContext * @return the token validation factory * @since 4.21 */ public static OAuthTokenValidationFactory resolveOAuthTokenValidationFactory(CamelContext context) { + OAuthTokenValidationFactory factory + = resolveConfiguredValidationFactory(context, OAUTH_PROFILE_PREFIX + VALIDATION_FACTORY_PROPERTY); + if (factory == null) { + factory = context.getRegistry() + .lookupByNameAndType(OAuthTokenValidationFactory.FACTORY, OAuthTokenValidationFactory.class); + } + if (factory == null) { + factory = context.getRegistry().findSingleByType(OAuthTokenValidationFactory.class); + } + if (factory == null) { + factory = resolveBootstrapValidationFactory(context); + } + return CamelContextAware.trySetCamelContext(factory, context); + } + + /** + * Resolves the OAuth token validation factory for the given profile. + * <p/> + * The profile-specific {@code camel.oauth.<profileName>.validation-factory} property can reference a registry bean + * by name. If configured, the referenced bean must exist and implement {@link OAuthTokenValidationFactory}. + * + * @param context the CamelContext + * @param profileName the OAuth profile name + * @return the token validation factory + * @since 4.21 + */ + public static OAuthTokenValidationFactory resolveOAuthTokenValidationFactory(CamelContext context, String profileName) { + if (profileName != null && !profileName.isBlank()) { + OAuthTokenValidationFactory factory = resolveConfiguredValidationFactory( + context, OAUTH_PROFILE_PREFIX + profileName + "." + VALIDATION_FACTORY_PROPERTY); + if (factory != null) { + return CamelContextAware.trySetCamelContext(factory, context); + } + } + return resolveOAuthTokenValidationFactory(context); + } + + private static OAuthTokenValidationFactory resolveConfiguredValidationFactory(CamelContext context, String propertyKey) { + String factoryReference = context.getPropertiesComponent().resolveProperty(propertyKey) + .map(String::trim) + .filter(value -> !value.isEmpty()) + .orElse(null); + if (factoryReference == null) { + return null; + } + + Object factory = EndpointHelper.resolveReferenceParameter(context, factoryReference, Object.class); + if (factory instanceof OAuthTokenValidationFactory tokenValidationFactory) { + return tokenValidationFactory; + } + throw new IllegalArgumentException( + "OAuth token validation factory property " + propertyKey + " references " + factoryReference + + " which is not an " + OAuthTokenValidationFactory.class.getName()); + } + + private static OAuthTokenValidationFactory resolveBootstrapValidationFactory(CamelContext context) { return ResolverHelper.resolveMandatoryBootstrapService( context, OAuthTokenValidationFactory.FACTORY, @@ -84,7 +147,7 @@ public final class OAuthHelper { */ public static OAuthTokenValidationResult validateOAuthToken( CamelContext context, String profileName, String token) { - return resolveOAuthTokenValidationFactory(context).validateToken(context, profileName, token); + return resolveOAuthTokenValidationFactory(context, profileName).validateToken(context, profileName, token); } /** diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc index 225a46d26b64..978783c2a994 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc @@ -811,6 +811,12 @@ aligning the component with the rest of the Camel component catalog (`camel-kafk names from NATS messages can supply a custom `headerFilterStrategy` to restore the previous behaviour. +=== camel-oauth + +`OAuthTokenRequest.refreshTokenGrant(...)` now sends the RFC 6749 `refresh_token` form parameter for +the refresh token grant instead of the incorrect `token` parameter. +Custom test servers or identity-provider mocks that expected the old request body should be updated. + === camel-platform-http The `platform-http` consumer endpoint now has an `oauthProfile` option for opt-in validation of incoming @@ -818,6 +824,9 @@ The `platform-http` consumer endpoint now has an `oauthProfile` option for opt-i Existing routes are unchanged unless this option is set. Routes that enable `oauthProfile` need an `OAuthTokenValidationFactory`; `camel-oauth` provides the default implementation and runtimes can provide their own implementation backed by native security. +Applications can also select a profile-specific validator with +`camel.oauth.<profile>.validation-factory=#bean:myTokenValidationFactory`; if configured, the referenced +bean must exist and implement `OAuthTokenValidationFactory`. The default `camel-oauth` provider requires `expected-issuer`/`base-uri`, `expected-audience`, and HTTPS identity-provider endpoints unless the new explicit opt-out properties are set. JWT validation profiles can also set `expected-token-type` to require a specific JWT `typ` header
