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;
+ }
+}