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

davsclaus pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/camel-4.18.x by this push:
     new 70a5bb6b6f14 [backport camel-4.18.x] CAMEL-25638: camel-graphql - bind 
accessToken and basic credentials to the configured httpUri authority (#27866)
70a5bb6b6f14 is described below

commit 70a5bb6b6f14eb82d260e82eaf70892b6c3985c5
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Oct 15 14:02:59 2026 +0200

    [backport camel-4.18.x] CAMEL-25638: camel-graphql - bind accessToken and 
basic credentials to the configured httpUri authority (#27866)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/component/graphql/GraphqlEndpoint.java   |  83 ++++++++-
 .../graphql/GraphqlCredentialsAuthorityTest.java   | 194 +++++++++++++++++++++
 2 files changed, 271 insertions(+), 6 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 ba6ce89b7be6..62e4ec603206 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;
@@ -47,9 +47,14 @@ import 
org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
 import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
 import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
 import org.apache.hc.client5.http.impl.classic.HttpClients;
+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.
@@ -58,6 +63,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;
@@ -143,22 +150,86 @@ public class GraphqlEndpoint extends DefaultEndpoint 
implements EndpointServiceL
             int port = Integer.parseInt(parts[1]);
             httpClientBuilder.setProxy(new HttpHost(hostname, port));
         }
+        configureAuthentication(httpClientBuilder);
+        return httpClientBuilder.build();
+    }
+
+    private void configureAuthentication(HttpClientBuilder httpClientBuilder) {
+        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",
+                    this);
+            return;
+        }
+        String host = authority.getHostName();
+        int port = effectivePort(scheme, authority.getPort());
         if (accessToken != null) {
             String authType = "Bearer";
             if (this.jwtAuthorizationType != null) {
                 authType = this.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);
         }
-        return httpClientBuilder.build();
+    }
+
+    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() {
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..e6b029617358
--- /dev/null
+++ 
b/components/camel-graphql/src/test/java/org/apache/camel/component/graphql/GraphqlCredentialsAuthorityTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+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.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+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.
+ */
+class GraphqlCredentialsAuthorityTest extends CamelTestSupport {
+
+    private static final String ACCESS_TOKEN = "token";
+    private static final String USERNAME = "camel";
+    private static final String PASSWORD = "password";
+    private static final String CONTENT = "{\"data\":{}}";
+
+    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;
+
+    @BeforeEach
+    void startServers() throws IOException {
+        differentHost = createOtherAuthority("127.0.0.1");
+        differentHost.start();
+        differentPort = createOtherAuthority("localhost");
+        differentPort.start();
+
+        localServer = ServerBootstrap.bootstrap()
+                .setCanonicalHostName("localhost")
+                .register("/graphql", (request, response, context) -> {
+                    authorizationSeenByHttpUri.set(authorization(request));
+                    ok(response);
+                })
+                .register("/protected", (request, response, context) -> {
+                    String authorization = authorization(request);
+                    authorizationSeenByHttpUri.set(authorization);
+                    if (authorization == null) {
+                        challenge(response);
+                    } else {
+                        ok(response);
+                    }
+                })
+                .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();
+    }
+
+    @AfterEach
+    void stopServers() {
+        // 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);
+            }
+        }
+    }
+
+    @Test
+    void accessTokenIsSentToTheHttpUriAuthority() {
+        Exchange exchange = template.request(
+                "graphql://http://localhost:"; + localServer.getLocalPort() + 
"/graphql?query={books{id name}}"
+                                             + "&accessToken=" + ACCESS_TOKEN,
+                e -> {
+                });
+
+        assertNull(exchange.getException());
+        assertEquals("Bearer " + ACCESS_TOKEN, 
authorizationSeenByHttpUri.get());
+    }
+
+    @Test
+    void basicCredentialsAreSentToTheHttpUriAuthority() {
+        Exchange exchange = template.request(
+                "graphql://http://localhost:"; + localServer.getLocalPort() + 
"/protected?query={books{id name}}"
+                                             + "&username=" + USERNAME + 
"&password=" + PASSWORD,
+                e -> {
+                });
+
+        assertNull(exchange.getException());
+        String expected = Base64.getEncoder()
+                .encodeToString((USERNAME + ":" + 
PASSWORD).getBytes(StandardCharsets.US_ASCII));
+        assertEquals("Basic " + expected, authorizationSeenByHttpUri.get());
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = { true, false })
+    void accessTokenIsSentToTheHttpUriAuthorityOnly(boolean toDifferentHost) {
+        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) {
+        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");
+    }
+
+    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)
+                .register("/graphql", (request, response, context) -> {
+                    requestsElsewhere.incrementAndGet();
+                    String authorization = authorization(request);
+                    authorizationSeenElsewhere.set(authorization);
+                    if (authorization == null) {
+                        challenge(response);
+                    } else {
+                        ok(response);
+                    }
+                })
+                .create();
+    }
+
+    private static HttpRequestHandler movedTo(String location) {
+        return (request, response, context) -> {
+            response.setHeader(HttpHeaders.LOCATION, location);
+            response.setCode(HttpStatus.SC_MOVED_TEMPORARILY);
+        };
+    }
+
+    private static void ok(ClassicHttpResponse response) {
+        response.setCode(HttpStatus.SC_OK);
+        response.setEntity(new StringEntity(CONTENT));
+    }
+
+    private static void challenge(ClassicHttpResponse response) {
+        response.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic 
realm=\"graphql\"");
+        response.setCode(HttpStatus.SC_UNAUTHORIZED);
+    }
+
+    private static String authorization(ClassicHttpRequest request) {
+        return request.containsHeader(HttpHeaders.AUTHORIZATION)
+                ? request.getFirstHeader(HttpHeaders.AUTHORIZATION).getValue() 
: null;
+    }
+}

Reply via email to