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


Reply via email to