This is an automated email from the ASF dual-hosted git repository.

Croway pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 3a6c27bfae04 CAMEL-24452: camel-http - do not send credentials to an 
authority the endpoint was not configured with (#25830)
3a6c27bfae04 is described below

commit 3a6c27bfae04e528f5a76494db6d6d97b9234551
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Aug 28 11:52:39 2026 +0200

    CAMEL-24452: camel-http - do not send credentials to an authority the 
endpoint was not configured with (#25830)
    
    CAMEL-24452: camel-http - do not send credentials to a host the endpoint 
was not configured with
    
    Two paths handed credentials to a redirect target, which is a host chosen 
by the remote
    server rather than by the route, once followRedirects=true.
    
    OAuth2ClientConfigurer registers its interceptor with 
addRequestInterceptorFirst, and
    HttpClient runs protocol-level request interceptors inside ProtocolExec, 
which sits below
    RedirectExec in the exec chain. The interceptor therefore ran again for 
every redirect hop
    and re-attached Authorization: Bearer <token> to whatever host the Location 
header named.
    It now attaches the token only for the endpoint's own host.
    
    HttpCredentialsHelper.getCredentialsProvider() was called with the 
endpoint's authHost,
    which is optional and null in the common basic-auth configuration, making 
the scope
    new AuthScope(null, -1) - any host, any port, any scheme. HttpClient then 
offered the
    credentials to whichever host issued a 401 challenge. The scope now falls 
back to the
    endpoint's host when authHost is not set; an explicit authHost still takes 
precedence.
    
    Both need to know the host the endpoint addresses, which 
createHttpClientConfigurer did
    not receive. Rather than change that protected signature, a three argument 
overload
    carries the target URI and the existing two argument form delegates to it 
with null, so
    any subclass overriding or calling it keeps the previous behaviour.
    
    The added test drives a real redirect from a server answering to localhost 
to a second one
    answering to 127.0.0.1 - a single server will not do, because the bootstrap 
sets a
    canonical host name and answers 421 to a mismatched Host. Without the fix 
the first case
    delivers "Bearer xxx.yyy.zzz" and the second "Basic c2NvdHQ6dGlnZXI=" to 
the redirect
    target.
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    Signed-off-by: Croway <[email protected]>
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
    Co-authored-by: Croway <[email protected]>
---
 .../DefaultAuthenticationHttpClientConfigurer.java |  12 +-
 .../apache/camel/component/http/HttpComponent.java |  66 +++++++--
 .../component/http/HttpCredentialsHelper.java      |  10 +-
 .../component/http/OAuth2ClientConfigurer.java     |  53 +++++++
 .../http/HttpClientConfigurerOverrideTest.java     |  48 +++++++
 .../http/HttpOAuth2RedirectTokenLeakTest.java      | 155 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  21 +++
 7 files changed, 354 insertions(+), 11 deletions(-)

diff --git 
a/components/camel-http/src/main/java/org/apache/camel/component/http/DefaultAuthenticationHttpClientConfigurer.java
 
b/components/camel-http/src/main/java/org/apache/camel/component/http/DefaultAuthenticationHttpClientConfigurer.java
index 8bcad925eb64..13a1a9b4f160 100644
--- 
a/components/camel-http/src/main/java/org/apache/camel/component/http/DefaultAuthenticationHttpClientConfigurer.java
+++ 
b/components/camel-http/src/main/java/org/apache/camel/component/http/DefaultAuthenticationHttpClientConfigurer.java
@@ -37,16 +37,26 @@ public class DefaultAuthenticationHttpClientConfigurer 
implements HttpClientConf
     private final String username;
     private final char[] password;
     private final String domain;
+    private final String scheme;
     private final String host;
+    private final Integer port;
     private final String bearerToken;
     private final HttpCredentialsHelper credentialsHelper;
 
     public DefaultAuthenticationHttpClientConfigurer(String user, String pwd, 
String domain, String host, String bearerToken,
                                                      HttpCredentialsHelper 
credentialsHelper) {
+        this(user, pwd, domain, null, host, null, bearerToken, 
credentialsHelper);
+    }
+
+    DefaultAuthenticationHttpClientConfigurer(String user, String pwd, String 
domain, String scheme, String host,
+                                              Integer port, String bearerToken,
+                                              HttpCredentialsHelper 
credentialsHelper) {
         this.username = user;
         this.password = pwd == null ? new char[0] : pwd.toCharArray();
         this.domain = domain;
+        this.scheme = scheme;
         this.host = host;
+        this.port = port;
         this.bearerToken = bearerToken;
         this.credentialsHelper = credentialsHelper;
     }
@@ -80,7 +90,7 @@ public class DefaultAuthenticationHttpClientConfigurer 
implements HttpClientConf
             defaultcreds = new UsernamePasswordCredentials(username, password);
         }
         clientBuilder.setDefaultCredentialsProvider(credentialsHelper
-                .getCredentialsProvider(host, null, defaultcreds));
+                .getCredentialsProvider(scheme, host, port, defaultcreds));
     }
 
 }
diff --git 
a/components/camel-http/src/main/java/org/apache/camel/component/http/HttpComponent.java
 
b/components/camel-http/src/main/java/org/apache/camel/component/http/HttpComponent.java
index d7706108022e..72f74b56e534 100644
--- 
a/components/camel-http/src/main/java/org/apache/camel/component/http/HttpComponent.java
+++ 
b/components/camel-http/src/main/java/org/apache/camel/component/http/HttpComponent.java
@@ -84,6 +84,7 @@ import org.slf4j.LoggerFactory;
 public class HttpComponent extends HttpCommonComponent implements 
RestProducerFactory, SSLContextParametersAware {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(HttpComponent.class);
+    private static final String TARGET_URI_PARAMETER = 
HttpComponent.class.getName() + ".targetUri";
 
     @Metadata(label = "advanced",
               description = "To use the custom HttpClientConfigurer to perform 
configuration of the HttpClient that will be used.")
@@ -247,6 +248,12 @@ public class HttpComponent extends HttpCommonComponent 
implements RestProducerFa
      * @throws Exception  is thrown if error creating configurer
      */
     protected HttpClientConfigurer createHttpClientConfigurer(Map<String, 
Object> parameters, boolean secure) throws Exception {
+        URI targetUri = (URI) parameters.remove(TARGET_URI_PARAMETER);
+        return createHttpClientConfigurer(parameters, secure, targetUri);
+    }
+
+    private HttpClientConfigurer createHttpClientConfigurer(Map<String, 
Object> parameters, boolean secure, URI targetUri)
+            throws Exception {
         // prefer to use endpoint configured over component configured
         HttpClientConfigurer configurer
                 = resolveAndRemoveReferenceParameter(parameters, 
"httpClientConfigurer", HttpClientConfigurer.class);
@@ -255,15 +262,15 @@ public class HttpComponent extends HttpCommonComponent 
implements RestProducerFa
             configurer = getHttpClientConfigurer();
         }
         HttpCredentialsHelper credentialsProvider = new 
HttpCredentialsHelper();
-        configurer = configureBasicAuthentication(parameters, configurer, 
credentialsProvider);
+        configurer = configureBasicAuthentication(parameters, configurer, 
credentialsProvider, targetUri);
         configurer = configureHttpProxy(parameters, configurer, secure, 
credentialsProvider);
-        configurer = configureOAuth2Authentication(parameters, configurer);
+        configurer = configureOAuth2Authentication(parameters, configurer, 
targetUri);
 
         return configurer;
     }
 
     private HttpClientConfigurer configureOAuth2Authentication(
-            Map<String, Object> parameters, HttpClientConfigurer configurer) {
+            Map<String, Object> parameters, HttpClientConfigurer configurer, 
URI targetUri) {
 
         String clientId = getParameter(parameters, "oauth2ClientId", 
String.class);
         String clientSecret = getParameter(parameters, "oauth2ClientSecret", 
String.class);
@@ -302,14 +309,15 @@ public class HttpComponent extends HttpCommonComponent 
implements RestProducerFa
                             cacheTokens,
                             cachedTokensDefaultExpirySeconds,
                             cachedTokensExpirationMarginSeconds,
-                            useBodyAuthentication));
+                            useBodyAuthentication,
+                            targetUri));
         }
         return configurer;
     }
 
     private HttpClientConfigurer configureBasicAuthentication(
             Map<String, Object> parameters, HttpClientConfigurer configurer,
-            HttpCredentialsHelper credentialsProvider) {
+            HttpCredentialsHelper credentialsProvider, URI targetUri) {
         String authUsername = getParameter(parameters, "authUsername", 
String.class);
         String authPassword = getParameter(parameters, "authPassword", 
String.class);
 
@@ -319,7 +327,9 @@ public class HttpComponent extends HttpCommonComponent 
implements RestProducerFa
 
             return CompositeHttpConfigurer.combineConfigurers(configurer,
                     new DefaultAuthenticationHttpClientConfigurer(
-                            authUsername, authPassword, authDomain, authHost, 
null, credentialsProvider));
+                            authUsername, authPassword, authDomain, 
authScopeScheme(authHost, targetUri),
+                            authScopeHost(authHost, targetUri), 
authScopePort(authHost, targetUri), null,
+                            credentialsProvider));
         } else if (this.httpConfiguration != null) {
             if 
("basic".equalsIgnoreCase(this.httpConfiguration.getAuthMethod())
                     || 
"bearer".equalsIgnoreCase(this.httpConfiguration.getAuthMethod())) {
@@ -327,7 +337,10 @@ public class HttpComponent extends HttpCommonComponent 
implements RestProducerFa
                         new DefaultAuthenticationHttpClientConfigurer(
                                 this.httpConfiguration.getAuthUsername(),
                                 this.httpConfiguration.getAuthPassword(), 
this.httpConfiguration.getAuthDomain(),
-                                this.httpConfiguration.getAuthHost(), 
this.httpConfiguration.getAuthBearerToken(),
+                                
authScopeScheme(this.httpConfiguration.getAuthHost(), targetUri),
+                                
authScopeHost(this.httpConfiguration.getAuthHost(), targetUri),
+                                
authScopePort(this.httpConfiguration.getAuthHost(), targetUri),
+                                this.httpConfiguration.getAuthBearerToken(),
                                 credentialsProvider));
             }
         }
@@ -335,6 +348,35 @@ public class HttpComponent extends HttpCommonComponent 
implements RestProducerFa
         return configurer;
     }
 
+    /**
+     * The host the credentials are scoped to.
+     * <p>
+     * {@code authHost} is optional and is unset in the common basic-auth 
configuration, which made the scope
+     * {@code new AuthScope(null, -1)} - matching any host, any port, any 
scheme. HttpClient then offers the credentials
+     * to whichever host issues a 401 challenge, so with {@code 
followRedirects=true} a redirect chosen by the remote
+     * server could collect them. Fall back to the authority the endpoint 
actually addresses.
+     */
+    private static String authScopeHost(String authHost, URI targetUri) {
+        if (authHost != null) {
+            return authHost;
+        }
+        return targetUri != null ? targetUri.getHost() : null;
+    }
+
+    private static String authScopeScheme(String authHost, URI targetUri) {
+        return authHost == null && targetUri != null ? targetUri.getScheme() : 
null;
+    }
+
+    private static Integer authScopePort(String authHost, URI targetUri) {
+        if (authHost != null || targetUri == null) {
+            return null;
+        }
+        if (targetUri.getPort() >= 0) {
+            return targetUri.getPort();
+        }
+        return "https".equalsIgnoreCase(targetUri.getScheme()) ? 443 : 80;
+    }
+
     private HttpClientConfigurer configureHttpProxy(
             Map<String, Object> parameters, HttpClientConfigurer configurer, 
boolean secure,
             HttpCredentialsHelper credentialsProvider) {
@@ -450,8 +492,14 @@ public class HttpComponent extends HttpCommonComponent 
implements RestProducerFa
         // uri part should be without protocol as that was how this component 
was originally created
         uri = 
org.apache.camel.component.http.HttpUtil.removeHttpOrHttpsProtocol(uri);
 
-        // create the configurer to use for this endpoint
-        HttpClientConfigurer configurer = 
createHttpClientConfigurer(parameters, secure);
+        // Keep dispatching through the existing protected method so 
subclasses overriding it continue to be invoked.
+        HttpClientConfigurer configurer;
+        parameters.put(TARGET_URI_PARAMETER, uriHttpUriAddress);
+        try {
+            configurer = createHttpClientConfigurer(parameters, secure);
+        } finally {
+            parameters.remove(TARGET_URI_PARAMETER);
+        }
         URI endpointUri = URISupport.createRemainingURI(uriHttpUriAddress, 
httpClientParameters);
 
         endpointUri = URISupport.createRemainingURI(
diff --git 
a/components/camel-http/src/main/java/org/apache/camel/component/http/HttpCredentialsHelper.java
 
b/components/camel-http/src/main/java/org/apache/camel/component/http/HttpCredentialsHelper.java
index 6185b6e9da8e..8d9032af31d9 100644
--- 
a/components/camel-http/src/main/java/org/apache/camel/component/http/HttpCredentialsHelper.java
+++ 
b/components/camel-http/src/main/java/org/apache/camel/component/http/HttpCredentialsHelper.java
@@ -39,9 +39,17 @@ public final class HttpCredentialsHelper {
 
     public CredentialsProvider getCredentialsProvider(
             String host, Integer port, Credentials credentials) {
+        return getCredentialsProvider(null, host, port, credentials);
+    }
+
+    CredentialsProvider getCredentialsProvider(
+            String scheme, String host, Integer port, Credentials credentials) 
{
         this.credentialsProvider.setCredentials(new AuthScope(
+                scheme,
                 host,
-                Objects.requireNonNullElse(port, -1)), credentials);
+                Objects.requireNonNullElse(port, -1),
+                null,
+                null), credentials);
         return credentialsProvider;
     }
 
diff --git 
a/components/camel-http/src/main/java/org/apache/camel/component/http/OAuth2ClientConfigurer.java
 
b/components/camel-http/src/main/java/org/apache/camel/component/http/OAuth2ClientConfigurer.java
index 8edb187e02f2..c98d980a3ac6 100644
--- 
a/components/camel-http/src/main/java/org/apache/camel/component/http/OAuth2ClientConfigurer.java
+++ 
b/components/camel-http/src/main/java/org/apache/camel/component/http/OAuth2ClientConfigurer.java
@@ -41,9 +41,13 @@ import org.apache.hc.core5.http.HttpRequest;
 import org.apache.hc.core5.http.io.entity.EntityUtils;
 import org.apache.hc.core5.http.io.entity.StringEntity;
 import org.apache.hc.core5.http.protocol.HttpContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class OAuth2ClientConfigurer extends ServiceSupport implements 
HttpClientConfigurer {
 
+    private static final Logger LOG = 
LoggerFactory.getLogger(OAuth2ClientConfigurer.class);
+
     private final String clientId;
     private final String clientSecret;
     private final String tokenEndpoint;
@@ -54,12 +58,27 @@ public class OAuth2ClientConfigurer extends ServiceSupport 
implements HttpClient
     private final static ConcurrentMap<OAuth2URIAndCredentials, TokenCache> 
tokenCache = new ConcurrentHashMap<>();
     private final boolean useBodyAuthentication;
     private final String resourceIndicator;
+    private final URI targetUri;
     private HttpClient httpClient;
 
     public OAuth2ClientConfigurer(String clientId, String clientSecret, String 
tokenEndpoint, String resourceIndicator,
                                   String scope, boolean cacheTokens,
                                   long cachedTokensDefaultExpirySeconds, long 
cachedTokensExpirationMarginSeconds,
                                   boolean useBodyAuthentication) {
+        this(clientId, clientSecret, tokenEndpoint, resourceIndicator, scope, 
cacheTokens,
+             cachedTokensDefaultExpirySeconds, 
cachedTokensExpirationMarginSeconds, useBodyAuthentication, null);
+    }
+
+    /**
+     * @param targetUri the URI the endpoint addresses. The bearer token is 
only attached to requests for the same
+     *                  authority, so that a redirect chosen by the remote 
server cannot collect it. Null keeps the
+     *                  previous behaviour of attaching it to whatever 
authority the request names.
+     */
+    OAuth2ClientConfigurer(String clientId, String clientSecret, String 
tokenEndpoint, String resourceIndicator,
+                           String scope, boolean cacheTokens,
+                           long cachedTokensDefaultExpirySeconds, long 
cachedTokensExpirationMarginSeconds,
+                           boolean useBodyAuthentication, URI targetUri) {
+        this.targetUri = targetUri;
         this.clientId = clientId;
         this.clientSecret = clientSecret;
         this.tokenEndpoint = tokenEndpoint;
@@ -78,6 +97,14 @@ public class OAuth2ClientConfigurer extends ServiceSupport 
implements HttpClient
 
         clientBuilder.addRequestInterceptorFirst((HttpRequest request, 
EntityDetails entity, HttpContext context) -> {
             URI requestUri = getUriFromRequest(request);
+            if (!isTargetAuthority(requestUri)) {
+                // HttpClient runs protocol-level request interceptors inside 
ProtocolExec, which sits below
+                // RedirectExec, so this runs again for every redirect hop. 
Without this check the bearer token is
+                // re-attached to whichever authority the Location header 
named.
+                LOG.debug("Not attaching the OAuth2 bearer token to {}, which 
is not the endpoint's authority {}",
+                        requestUri, targetUri);
+                return;
+            }
             OAuth2URIAndCredentials uriAndCredentials = new 
OAuth2URIAndCredentials(
                     requestUri, clientId, clientSecret, tokenEndpoint, scope, 
resourceIndicator);
             if (cacheTokens) {
@@ -103,6 +130,32 @@ public class OAuth2ClientConfigurer extends ServiceSupport 
implements HttpClient
         });
     }
 
+    private boolean isTargetAuthority(URI requestUri) {
+        if (targetUri == null) {
+            return true;
+        }
+        if (targetUri.getScheme() == null || targetUri.getHost() == null
+                || requestUri == null || requestUri.getScheme() == null || 
requestUri.getHost() == null) {
+            return false;
+        }
+        return targetUri.getScheme().equalsIgnoreCase(requestUri.getScheme())
+                && targetUri.getHost().equalsIgnoreCase(requestUri.getHost())
+                && effectivePort(targetUri) == effectivePort(requestUri);
+    }
+
+    private static int effectivePort(URI uri) {
+        if (uri.getPort() >= 0) {
+            return uri.getPort();
+        }
+        if ("http".equalsIgnoreCase(uri.getScheme())) {
+            return 80;
+        }
+        if ("https".equalsIgnoreCase(uri.getScheme())) {
+            return 443;
+        }
+        return -1;
+    }
+
     private JsonObject getAccessTokenResponse(HttpClient httpClient) throws 
IOException {
         String bodyStr = "grant_type=client_credentials";
         if (scope != null) {
diff --git 
a/components/camel-http/src/test/java/org/apache/camel/component/http/HttpClientConfigurerOverrideTest.java
 
b/components/camel-http/src/test/java/org/apache/camel/component/http/HttpClientConfigurerOverrideTest.java
new file mode 100644
index 000000000000..074cd08cbba9
--- /dev/null
+++ 
b/components/camel-http/src/test/java/org/apache/camel/component/http/HttpClientConfigurerOverrideTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.component.http;
+
+import java.util.Map;
+
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class HttpClientConfigurerOverrideTest extends CamelTestSupport {
+
+    @Test
+    public void existingTwoArgumentOverrideIsStillInvoked() {
+        TrackingHttpComponent component = new TrackingHttpComponent();
+        context.addComponent("http-tracking", component);
+
+        
assertThat(context.getEndpoint("http-tracking://localhost:8080")).isNotNull();
+        assertThat(component.invoked).isTrue();
+    }
+
+    private static final class TrackingHttpComponent extends HttpComponent {
+
+        private boolean invoked;
+
+        @Override
+        protected HttpClientConfigurer createHttpClientConfigurer(Map<String, 
Object> parameters, boolean secure)
+                throws Exception {
+            invoked = true;
+            return super.createHttpClientConfigurer(parameters, secure);
+        }
+    }
+}
diff --git 
a/components/camel-http/src/test/java/org/apache/camel/component/http/HttpOAuth2RedirectTokenLeakTest.java
 
b/components/camel-http/src/test/java/org/apache/camel/component/http/HttpOAuth2RedirectTokenLeakTest.java
new file mode 100644
index 000000000000..13b6b72c5a5e
--- /dev/null
+++ 
b/components/camel-http/src/test/java/org/apache/camel/component/http/HttpOAuth2RedirectTokenLeakTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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.component.http;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.component.http.handler.OAuth2TokenRequestHandler;
+import org.apache.camel.util.IOHelper;
+import org.apache.hc.core5.http.impl.bootstrap.HttpServer;
+import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * HttpClient runs protocol-level request interceptors inside {@code 
ProtocolExec}, which sits below
+ * {@code RedirectExec} in the exec chain, so the OAuth2 interceptor runs once 
per redirect hop. Without a check it
+ * re-attaches the bearer token to whichever host the {@code Location} header 
named - a host chosen by the remote
+ * server, not by the route.
+ * <p>
+ * The tests cover both a different host and a different port because 
credentials are scoped to an authority, not just a
+ * host name.
+ */
+public class HttpOAuth2RedirectTokenLeakTest extends BaseHttpTest {
+
+    private static final String FAKE_TOKEN = "xxx.yyy.zzz";
+    private static final String CLIENT_ID = "test-client";
+    private static final String CLIENT_SECRET = "test-secret";
+
+    private final AtomicReference<String> authorizationSeenAfterRedirect = new 
AtomicReference<>();
+
+    private HttpServer localServer;
+    private HttpServer differentHostRedirectTarget;
+    private HttpServer differentPortRedirectTarget;
+
+    @Override
+    public void setupResources() throws Exception {
+        differentHostRedirectTarget = createRedirectTarget("127.0.0.1");
+        differentHostRedirectTarget.start();
+        differentPortRedirectTarget = createRedirectTarget("localhost");
+        differentPortRedirectTarget.start();
+
+        localServer = ServerBootstrap.bootstrap()
+                
.setCanonicalHostName("localhost").setHttpProcessor(getBasicHttpProcessor())
+                
.setConnectionReuseStrategy(getConnectionReuseStrategy()).setResponseFactory(getHttpResponseFactory())
+                .setSslContext(getSSLContext())
+                .register("/token", new OAuth2TokenRequestHandler(FAKE_TOKEN, 
CLIENT_ID, CLIENT_SECRET))
+                .register("/redirect-to-different-host", (request, response, 
context) -> {
+                    response.setHeader("Location",
+                            "http://127.0.0.1:"; + 
differentHostRedirectTarget.getLocalPort() + "/elsewhere");
+                    response.setCode(302);
+                })
+                .register("/redirect-to-different-port", (request, response, 
context) -> {
+                    response.setHeader("Location",
+                            "http://localhost:"; + 
differentPortRedirectTarget.getLocalPort() + "/elsewhere");
+                    response.setCode(302);
+                })
+                .register("/challenge-on-different-host", (request, response, 
context) -> {
+                    response.setHeader("Location",
+                            "http://127.0.0.1:"; + 
differentHostRedirectTarget.getLocalPort() + "/challenge");
+                    response.setCode(302);
+                })
+                .register("/challenge-on-different-port", (request, response, 
context) -> {
+                    response.setHeader("Location",
+                            "http://localhost:"; + 
differentPortRedirectTarget.getLocalPort() + "/challenge");
+                    response.setCode(302);
+                })
+                .create();
+
+        localServer.start();
+    }
+
+    private HttpServer createRedirectTarget(String canonicalHostName) {
+        return ServerBootstrap.bootstrap()
+                
.setCanonicalHostName(canonicalHostName).setHttpProcessor(getBasicHttpProcessor())
+                
.setConnectionReuseStrategy(getConnectionReuseStrategy()).setResponseFactory(getHttpResponseFactory())
+                .register("/elsewhere", (request, response, context) -> {
+                    authorizationSeenAfterRedirect.set(
+                            request.containsHeader("Authorization")
+                                    ? 
request.getFirstHeader("Authorization").getValue() : null);
+                    response.setCode(200);
+                    response.setEntity(new StringEntity("Bye World"));
+                })
+                .register("/challenge", (request, response, context) -> {
+                    authorizationSeenAfterRedirect.set(
+                            request.containsHeader("Authorization")
+                                    ? 
request.getFirstHeader("Authorization").getValue() : null);
+                    response.setHeader("WWW-Authenticate", "Basic 
realm=\"elsewhere\"");
+                    response.setCode(401);
+                })
+                .create();
+    }
+
+    @Override
+    public void cleanupResources() throws Exception {
+        IOHelper.close(localServer, differentHostRedirectTarget, 
differentPortRedirectTarget);
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = { true, false })
+    public void theBearerTokenIsNotSentToARedirectTarget(boolean 
differentHost) {
+        HttpComponent http = context.getComponent("http", HttpComponent.class);
+        http.setFollowRedirects(true);
+
+        String tokenEndpoint = "http://localhost:"; + 
localServer.getLocalPort() + "/token";
+        String redirectPath = differentHost ? "/redirect-to-different-host" : 
"/redirect-to-different-port";
+        String uri = "http://localhost:"; + localServer.getLocalPort() + 
redirectPath + "?oauth2ClientId=" + CLIENT_ID
+                     + "&oauth2ClientSecret=" + CLIENT_SECRET + 
"&oauth2TokenEndpoint=" + tokenEndpoint;
+
+        String body = fluentTemplate.to(uri).request(String.class);
+
+        assertThat(body).as("the redirect should still have been 
followed").isEqualTo("Bye World");
+        assertThat(authorizationSeenAfterRedirect.get())
+                .as("the bearer token must not be re-attached to the authority 
the Location header named").isNull();
+    }
+
+    /**
+     * The basic-auth half of the same problem: authHost is optional and unset 
in the common configuration, which made
+     * the credentials scope {@code new AuthScope(null, -1)} - any host, any 
port, any scheme. HttpClient then offers
+     * the credentials to whichever host issues a 401 challenge, including one 
reached by following a redirect the
+     * remote server chose.
+     */
+    @ParameterizedTest
+    @ValueSource(booleans = { true, false })
+    public void basicCredentialsAreNotOfferedToARedirectTarget(boolean 
differentHost) {
+        HttpComponent http = context.getComponent("http", HttpComponent.class);
+        http.setFollowRedirects(true);
+
+        String challengePath = differentHost ? "/challenge-on-different-host" 
: "/challenge-on-different-port";
+        String uri = "http://localhost:"; + localServer.getLocalPort() + 
challengePath
+                     + "?throwExceptionOnFailure=false"
+                     + "&authUsername=scott&authPassword=tiger";
+
+        fluentTemplate.to(uri).request(String.class);
+
+        assertThat(authorizationSeenAfterRedirect.get())
+                .as("basic credentials must not be offered to the authority 
the Location header named").isNull();
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 3fcc5b76c9b6..afb004c9e82b 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -564,6 +564,27 @@ is truncated rather than recreated and would otherwise 
keep its original permiss
 Deployments where another account legitimately reads these files — a sidecar 
or a backup agent running as
 a different user — need to run as the owner, or use a group-aware key store 
instead.
 
+=== camel-http
+
+Credentials are no longer sent to an authority the endpoint was not configured 
with. Two paths reached that
+outcome once `followRedirects=true`, since a redirect target is chosen by the 
remote server rather than
+by the route:
+
+* The OAuth2 interceptor is registered with `addRequestInterceptorFirst`, and 
HttpClient runs
+protocol-level request interceptors inside `ProtocolExec`, which sits below 
`RedirectExec` — so it ran
+again for every redirect hop and re-attached `Authorization: Bearer <token>` 
to whatever authority the
+`Location` header named. The token is now attached only for the endpoint's own 
scheme, host and effective port.
+* `authHost` is optional and unset in the common basic-auth configuration, 
which made the credentials
+scope `new AuthScope(null, -1)` — any host, any port, any scheme — so 
HttpClient offered the credentials
+to whichever host issued a `401` challenge. The scope now falls back to the 
endpoint's host when
+`authHost` is not set and is restricted to the endpoint's scheme and effective 
port.
+
+Routes that relied on credentials following a redirect to a different 
authority must set `authHost`
+explicitly, which continues to take precedence and preserves the previous 
any-port behaviour.
+
+`HttpComponent.createHttpClientConfigurer(Map, boolean)` remains the protected 
customization point and is still invoked
+when an endpoint is created, so existing subclasses continue to behave as 
before.
+
 === camel-jetty
 
 `enableCORS=true` added `new CrossOriginFilter()` with no init parameters, so 
Jetty's own defaults

Reply via email to