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

davsclaus 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 919fbfbeb03d CAMEL-25638: camel-graphql - bind accessToken and basic 
credentials to the configured httpUri authority (#27819)
919fbfbeb03d is described below

commit 919fbfbeb03d7e4377013746833956567293b05d
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Oct 14 21:19:37 2026 +0200

    CAMEL-25638: camel-graphql - bind accessToken and basic credentials to the 
configured httpUri authority (#27819)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/component/graphql/GraphqlEndpoint.java   |  77 +++++++++-
 .../camel/component/graphql/GraphqlProducer.java   |  10 +-
 .../graphql/GraphqlCredentialsAuthorityTest.java   | 166 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  14 ++
 4 files changed, 260 insertions(+), 7 deletions(-)

diff --git 
a/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlEndpoint.java
 
b/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlEndpoint.java
index 759b19570ebd..1a0fb874023c 100644
--- 
a/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlEndpoint.java
+++ 
b/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlEndpoint.java
@@ -19,7 +19,7 @@ package org.apache.camel.component.graphql;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
-import java.util.Arrays;
+import java.net.URISyntaxException;
 import java.util.Map;
 
 import org.apache.camel.Category;
@@ -46,9 +46,14 @@ import 
org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
 import org.apache.hc.client5.http.classic.HttpClient;
 import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
 import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+import org.apache.hc.core5.http.Header;
 import org.apache.hc.core5.http.HttpHeaders;
 import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
 import org.apache.hc.core5.http.message.BasicHeader;
+import org.apache.hc.core5.net.URIAuthority;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Send GraphQL queries and mutations to external systems.
@@ -57,6 +62,8 @@ import org.apache.hc.core5.http.message.BasicHeader;
              category = { Category.API }, producerOnly = true, 
lenientProperties = true)
 public class GraphqlEndpoint extends DefaultEndpoint implements 
EndpointServiceLocation {
 
+    private static final Logger LOG = 
LoggerFactory.getLogger(GraphqlEndpoint.class);
+
     @UriPath
     @Metadata(required = true)
     private URI httpUri;
@@ -147,24 +154,84 @@ public class GraphqlEndpoint extends DefaultEndpoint 
implements EndpointServiceL
                 int port = Integer.parseInt(parts[1]);
                 httpClientBuilder.setProxy(new HttpHost(hostname, port));
             }
+            boolean basicAuth = username != null && password != null;
+            if (accessToken == null && !basicAuth) {
+                return;
+            }
+            // the credentials belong to the authority of httpUri, as 
camel-http does for its own (CAMEL-24452)
+            String scheme = httpUri.getScheme();
+            URIAuthority authority = httpUriAuthority();
+            if (scheme == null || authority == null) {
+                LOG.warn("Cannot read the scheme, host and port of httpUri on 
{}: the access token and the basic "
+                         + "credentials are not sent",
+                        GraphqlEndpoint.this);
+                return;
+            }
+            String host = authority.getHostName();
+            int port = effectivePort(scheme, authority.getPort());
             if (accessToken != null) {
                 String authType = "Bearer";
                 if (jwtAuthorizationType != null) {
                     authType = jwtAuthorizationType;
                 }
-                httpClientBuilder.setDefaultHeaders(
-                        Arrays.asList(new 
BasicHeader(HttpHeaders.AUTHORIZATION, authType + " " + accessToken)));
+                Header authorization = new 
BasicHeader(HttpHeaders.AUTHORIZATION, authType + " " + accessToken);
+                // added per request rather than as a default header, which 
would go with every request the client
+                // executes
+                httpClientBuilder.addRequestInterceptorLast((request, entity, 
context) -> {
+                    if (!request.containsHeader(HttpHeaders.AUTHORIZATION) && 
isAuthority(request, scheme, host, port)) {
+                        request.addHeader(authorization);
+                    }
+                });
             }
-            if (username != null && password != null) {
+            if (basicAuth) {
                 CredentialsStore credentialsProvider = new 
BasicCredentialsProvider();
                 credentialsProvider.setCredentials(
-                        new AuthScope(null, -1),
+                        new AuthScope(scheme, host, port, null, null),
                         new UsernamePasswordCredentials(username, 
password.toCharArray()));
                 
httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
             }
         }
     }
 
+    private static boolean isAuthority(HttpRequest request, String scheme, 
String host, int port) {
+        URIAuthority target = request.getAuthority();
+        return target != null
+                && scheme.equalsIgnoreCase(request.getScheme())
+                && host.equalsIgnoreCase(target.getHostName())
+                && port == effectivePort(scheme, target.getPort());
+    }
+
+    /**
+     * The authority of httpUri as HttpClient reads it, which also accepts a 
host name that java.net.URI does not parse,
+     * such as one with an underscore.
+     */
+    private URIAuthority httpUriAuthority() {
+        if (httpUri.getHost() != null) {
+            return new URIAuthority(httpUri.getHost(), httpUri.getPort());
+        }
+        if (httpUri.getRawAuthority() == null) {
+            return null;
+        }
+        try {
+            return URIAuthority.create(httpUri.getRawAuthority());
+        } catch (URISyntaxException e) {
+            return null;
+        }
+    }
+
+    private static int effectivePort(String scheme, int port) {
+        if (port >= 0) {
+            return port;
+        }
+        if ("https".equalsIgnoreCase(scheme)) {
+            return 443;
+        }
+        if ("http".equalsIgnoreCase(scheme)) {
+            return 80;
+        }
+        return -1;
+    }
+
     public URI getHttpUri() {
         return httpUri;
     }
diff --git 
a/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlProducer.java
 
b/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlProducer.java
index 2783ffab6170..1dff0d91e688 100644
--- 
a/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlProducer.java
+++ 
b/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlProducer.java
@@ -45,9 +45,14 @@ public class GraphqlProducer extends DefaultAsyncProducer {
         Registry registry = getEndpoint().getCamelContext().getRegistry();
         String hash = 
Integer.toHexString(getEndpoint().getEndpointUri().hashCode());
 
-        StringBuilder sb = new StringBuilder(getEndpoint().getServiceUrl());
-        sb.append("?httpMethod=POST");
+        String serviceUrl = getEndpoint().getServiceUrl();
+        StringBuilder sb = new StringBuilder(serviceUrl);
+        // httpUri has a query of its own when the endpoint has lenient 
parameters (such as an api key)
+        sb.append(serviceUrl.indexOf('?') < 0 ? '?' : 
'&').append("httpMethod=POST");
         
sb.append("&throwExceptionOnFailure=").append(getEndpoint().isThrowExceptionOnFailure());
+        // the request always goes to httpUri: CamelHttpUri, CamelHttpPath and 
the other camel-http control headers
+        // of the message must not change it
+        sb.append("&skipControlHeaders=true");
 
         String clientConfigurerName = "graphqlHttpClientConfigurer-" + hash;
         registry.bind(clientConfigurerName, 
getEndpoint().createHttpClientConfigurer());
@@ -86,6 +91,7 @@ public class GraphqlProducer extends DefaultAsyncProducer {
     public boolean process(Exchange exchange, AsyncCallback callback) {
 
         Exchange httpExchange = getEndpoint().createExchange();
+        // not read by camel-http (skipControlHeaders=true): the httpMethod 
option of the internal endpoint decides
         httpExchange.getIn().setHeader(HttpConstants.HTTP_METHOD, "post");
         httpExchange.getIn().setHeader("Content-Type", "application/json");
         httpExchange.getIn().setHeader("Accept", "application/json");
diff --git 
a/components/camel-graphql/src/test/java/org/apache/camel/component/graphql/GraphqlCredentialsAuthorityTest.java
 
b/components/camel-graphql/src/test/java/org/apache/camel/component/graphql/GraphqlCredentialsAuthorityTest.java
new file mode 100644
index 000000000000..30503706d13d
--- /dev/null
+++ 
b/components/camel-graphql/src/test/java/org/apache/camel/component/graphql/GraphqlCredentialsAuthorityTest.java
@@ -0,0 +1,166 @@
+/*
+ * 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.graphql;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.component.http.HttpComponent;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpStatus;
+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.HttpRequestHandler;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.io.CloseMode;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * The access token and the basic credentials belong to the authority of 
httpUri: they are sent to its scheme, host and
+ * port only. The tests cover both a different host and a different port, and 
the message headers that camel-http reads
+ * to build the request URL.
+ */
+class GraphqlCredentialsAuthorityTest extends BaseGraphqlTest {
+
+    private static final String ACCESS_TOKEN = "token";
+    private static final String USERNAME = "camel";
+    private static final String PASSWORD = "password";
+
+    private final AtomicReference<String> authorizationSeenByHttpUri = new 
AtomicReference<>();
+    private final AtomicReference<String> authorizationSeenElsewhere = new 
AtomicReference<>();
+    private final AtomicInteger requestsElsewhere = new AtomicInteger();
+
+    private HttpServer localServer;
+    private HttpServer differentHost;
+    private HttpServer differentPort;
+
+    @Override
+    public void setupResources() throws Exception {
+        differentHost = createOtherAuthority("127.0.0.1");
+        differentHost.start();
+        differentPort = createOtherAuthority("localhost");
+        differentPort.start();
+
+        localServer = ServerBootstrap.bootstrap()
+                
.setCanonicalHostName("localhost").setHttpProcessor(getBasicHttpProcessor())
+                .register("/graphql", (request, response, context) -> {
+                    authorizationSeenByHttpUri.set(authorization(request));
+                    response.setCode(HttpStatus.SC_OK);
+                    response.setEntity(new StringEntity(getExpectedContent()));
+                })
+                .register("/moved-to-different-host",
+                        movedTo("http://127.0.0.1:"; + 
differentHost.getLocalPort() + "/graphql"))
+                .register("/moved-to-different-port",
+                        movedTo("http://localhost:"; + 
differentPort.getLocalPort() + "/graphql"))
+                .create();
+        localServer.start();
+    }
+
+    @Override
+    public void cleanupResources() {
+        // the client of the endpoint keeps its connections open until the 
context stops
+        for (HttpServer server : new HttpServer[] { localServer, 
differentHost, differentPort }) {
+            if (server != null) {
+                server.close(CloseMode.IMMEDIATE);
+            }
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = { true, false })
+    void accessTokenIsSentToTheHttpUriAuthorityOnly(boolean toDifferentHost) {
+        context.getComponent("http", 
HttpComponent.class).setFollowRedirects(true);
+
+        template.request(graphqlUri(toDifferentHost) + "&accessToken=" + 
ACCESS_TOKEN, exchange -> {
+        });
+
+        assertEquals(1, requestsElsewhere.get(), "the other authority should 
have been called");
+        assertNull(authorizationSeenElsewhere.get(), "the access token must 
not be sent to another authority");
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = { true, false })
+    void basicCredentialsAreOfferedToTheHttpUriAuthorityOnly(boolean 
toDifferentHost) {
+        context.getComponent("http", 
HttpComponent.class).setFollowRedirects(true);
+
+        template.request(graphqlUri(toDifferentHost) + "&username=" + USERNAME 
+ "&password=" + PASSWORD, exchange -> {
+        });
+
+        assertEquals(1, requestsElsewhere.get(), "the other authority should 
have been called once, without credentials");
+        assertNull(authorizationSeenElsewhere.get(), "the basic credentials 
must not be offered to another authority");
+    }
+
+    @Test
+    void controlHeadersDoNotChangeTheTarget() {
+        Exchange exchange = template.request(
+                "graphql://http://localhost:"; + localServer.getLocalPort() + 
"/graphql?query={books{id name}}"
+                                             + "&accessToken=" + ACCESS_TOKEN,
+                e -> {
+                    e.getIn().setHeader(Exchange.HTTP_URI,
+                            "http://127.0.0.1:"; + differentHost.getLocalPort() 
+ "/graphql");
+                    e.getIn().setHeader(Exchange.HTTP_PATH, "/other");
+                    e.getIn().setHeader(Exchange.HTTP_QUERY, "other=true");
+                });
+
+        assertNull(exchange.getException());
+        assertEquals("Bearer " + ACCESS_TOKEN, 
authorizationSeenByHttpUri.get());
+        assertEquals(0, requestsElsewhere.get(), "the request must go to 
httpUri");
+    }
+
+    private String graphqlUri(boolean toDifferentHost) {
+        String path = toDifferentHost ? "/moved-to-different-host" : 
"/moved-to-different-port";
+        return "graphql://http://localhost:"; + localServer.getLocalPort() + 
path
+               + "?query={books{id name}}&throwExceptionOnFailure=false";
+    }
+
+    private HttpServer createOtherAuthority(String canonicalHostName) {
+        // answers 401 with a Basic challenge when no credentials are sent, as 
a server asking for them would
+        return ServerBootstrap.bootstrap()
+                
.setCanonicalHostName(canonicalHostName).setHttpProcessor(getBasicHttpProcessor())
+                .register("/graphql", (request, response, context) -> {
+                    requestsElsewhere.incrementAndGet();
+                    String authorization = authorization(request);
+                    authorizationSeenElsewhere.set(authorization);
+                    if (authorization == null) {
+                        response.setCode(HttpStatus.SC_UNAUTHORIZED);
+                    } else {
+                        response.setCode(HttpStatus.SC_OK);
+                        response.setEntity(new 
StringEntity(getExpectedContent()));
+                    }
+                })
+                .create();
+    }
+
+    private static HttpRequestHandler movedTo(String location) {
+        return (request, response, context) -> {
+            response.setHeader(HttpHeaders.LOCATION, location);
+            response.setCode(HttpStatus.SC_MOVED_TEMPORARILY);
+        };
+    }
+
+    private static String authorization(ClassicHttpRequest request) {
+        return request.containsHeader(HttpHeaders.AUTHORIZATION)
+                ? request.getFirstHeader(HttpHeaders.AUTHORIZATION).getValue() 
: null;
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 7876caf40a53..c06170b02292 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -416,6 +416,20 @@ A shard created by a split or a merge while the consumer 
is running is now read
 `iteratorType`. Previously, with `LATEST`, the records stored in the new shard 
before the consumer listed it were
 skipped. The shards that exist when the consumer starts are read from the 
configured `iteratorType`, as before.
 
+=== camel-graphql - the credentials are only sent to the authority of httpUri
+
+The `accessToken` (with its `jwtAuthorizationType`) is now added to a request 
only when the request goes to the
+scheme, host and port of `httpUri`, and the `username` and `password` are 
registered for that authority only.
+Previously neither was bound to the authority of `httpUri`. A request to 
another authority, including the same host
+with another scheme or port, is now sent without them. A route that needs to 
authenticate to another authority must
+address it in `httpUri`. When `proxyHost` is set, the `username` and 
`password` are no longer offered to the proxy when
+it asks for authentication.
+
+The producer also no longer takes the request URL from the `CamelHttpUri`, 
`CamelHttpPath`, `CamelHttpQuery` and other
+camel-http control headers of the message. It did so since Camel 4.19, when 
the component moved onto camel-http; the
+request now always goes to `httpUri`, as it did before. For the same reason 
the `CamelHttpMethod` and
+`CamelHttpProtocolVersion` headers no longer apply, and the request is always 
a POST.
+
 === camel-servlet and camel-jetty - gzip request body that arrives after the 
headers
 
 The servlet and Jetty consumers uncompressed a request body with 
`Content-Encoding: gzip` only when some of its bytes

Reply via email to