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