You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by gn...@apache.org on 2026/10/13 11:37:54 UTC
(camel) branch backport/27718-to-camel-4.22.x created (now 58e0b90ebd80)
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a change to branch backport/27718-to-camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git
at 58e0b90ebd80 [backport camel-4.22.x] CAMEL-25568: camel-oauth - make OAuthBearerTokenProcessor stateless and read credentials from HTTP request headers
This branch includes the following new commits:
new 58e0b90ebd80 [backport camel-4.22.x] CAMEL-25568: camel-oauth - make OAuthBearerTokenProcessor stateless and read credentials from HTTP request headers
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(camel) 01/01: [backport camel-4.22.x] CAMEL-25568: camel-oauth - make OAuthBearerTokenProcessor stateless and read credentials from HTTP request headers
Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch backport/27718-to-camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git
commit 58e0b90ebd80743d7cbdc77f308513225a311819
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Tue Oct 13 11:37:38 2026 +0000
[backport camel-4.22.x] CAMEL-25568: camel-oauth - make OAuthBearerTokenProcessor stateless and read credentials from HTTP request headers
---
components/camel-oauth/pom.xml | 22 ++
.../org/apache/camel/oauth/HttpRequestHeaders.java | 124 ++++++++++
.../apache/camel/oauth/InMemorySessionStore.java | 14 +-
.../camel/oauth/NettyHttpRequestHeaders.java | 38 ++++
.../main/java/org/apache/camel/oauth/OAuth.java | 7 +
.../camel/oauth/OAuthBearerTokenProcessor.java | 25 ++-
.../camel/oauth/ServletHttpRequestHeaders.java | 81 +++++++
.../camel/oauth/VertxHttpRequestHeaders.java | 38 ++++
.../http/springboot/PlatformHttpMessage.java | 43 ++++
.../oauth/AbstractOAuthRequestHeadersTest.java | 249 +++++++++++++++++++++
.../apache/camel/oauth/HttpRequestHeadersTest.java | 168 ++++++++++++++
.../camel/oauth/OAuthBearerTokenProcessorTest.java | 85 +++++++
.../oauth/OAuthRequestHeadersNettyHttpTest.java | 43 ++++
.../oauth/OAuthRequestHeadersServletTest.java | 66 ++++++
.../oauth/OAuthRequestHeadersUndertowTest.java | 59 +++++
.../camel/oauth/OAuthRequestHeadersVertxTest.java | 45 ++++
.../java/org/apache/camel/oauth/StubOAuth.java | 74 ++++++
17 files changed, 1173 insertions(+), 8 deletions(-)
diff --git a/components/camel-oauth/pom.xml b/components/camel-oauth/pom.xml
index bd374c4192fa..1b0373dda78a 100644
--- a/components/camel-oauth/pom.xml
+++ b/components/camel-oauth/pom.xml
@@ -66,6 +66,23 @@
<artifactId>camel-platform-http-main</artifactId>
<scope>provided</scope>
</dependency>
+ <!-- to read the request headers of the servlet based and the netty http consumers -->
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-http-common</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>jakarta.servlet</groupId>
+ <artifactId>jakarta.servlet-api</artifactId>
+ <version>${jakarta-servlet-api-version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-netty-http</artifactId>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5</artifactId>
@@ -89,6 +106,11 @@
<artifactId>camel-servlet</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-undertow</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-servlet</artifactId>
diff --git a/components/camel-oauth/src/main/java/org/apache/camel/oauth/HttpRequestHeaders.java b/components/camel-oauth/src/main/java/org/apache/camel/oauth/HttpRequestHeaders.java
new file mode 100644
index 000000000000..3cf1fe56e80a
--- /dev/null
+++ b/components/camel-oauth/src/main/java/org/apache/camel/oauth/HttpRequestHeaders.java
@@ -0,0 +1,124 @@
+/*
+ * 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.oauth;
+
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.Message;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Reads a header of the HTTP request a message was created from.
+ * <p>
+ * HTTP consumers also map query and form parameters into the message headers, so a message header is not necessarily
+ * the request header of that name. Credentials are therefore read from the HTTP request itself whenever the message
+ * carries it: platform-http on Vert.x and on Spring Boot, the servlet based consumers (camel-servlet, camel-jetty) and
+ * camel-netty-http. The message types are matched by name, and a transport specific class is only loaded for a message
+ * of its own type, so none of these transports has to be on the classpath.
+ * <p>
+ * Any other message falls back to the message headers, leaving out a value that also appears in the query string of the
+ * request: the consumer may have mapped it from a query parameter, and a credential does not belong in a url anyway
+ * (RFC 6750 section 5.3).
+ */
+final class HttpRequestHeaders {
+
+ private static final Logger LOG = LoggerFactory.getLogger(HttpRequestHeaders.class);
+
+ private static final String VERTX_HTTP_MESSAGE = "org.apache.camel.component.platform.http.vertx.HttpMessage";
+ private static final String SERVLET_HTTP_MESSAGE = "org.apache.camel.http.common.HttpMessage";
+ private static final String NETTY_HTTP_MESSAGE = "org.apache.camel.component.netty.http.NettyHttpMessage";
+ // from camel-platform-http-starter, which camel-oauth does not depend on; it gives the servlet request
+ private static final String SPRING_BOOT_PLATFORM_HTTP_MESSAGE
+ = "org.apache.camel.component.platform.http.springboot.PlatformHttpMessage";
+
+ private HttpRequestHeaders() {
+ }
+
+ /**
+ * The values of the named header of the HTTP request that the message comes from.
+ *
+ * @param message the message
+ * @param name the header name, matched ignoring case
+ * @return the header values, possibly none
+ */
+ static List<String> values(Message message, String name) {
+ return requestHeaderValues(message, name).orElseGet(() -> messageHeaderValues(message, name));
+ }
+
+ private static Optional<List<String>> requestHeaderValues(Message message, String name) {
+ for (Class<?> type = message.getClass(); type != null; type = type.getSuperclass()) {
+ if (VERTX_HTTP_MESSAGE.equals(type.getName())) {
+ return VertxHttpRequestHeaders.values(message, name);
+ }
+ if (SERVLET_HTTP_MESSAGE.equals(type.getName())) {
+ return ServletHttpRequestHeaders.values(message, name);
+ }
+ if (NETTY_HTTP_MESSAGE.equals(type.getName())) {
+ return NettyHttpRequestHeaders.values(message, name);
+ }
+ if (SPRING_BOOT_PLATFORM_HTTP_MESSAGE.equals(type.getName())) {
+ return ServletHttpRequestHeaders.valuesOfSpringBootMessage(message, name);
+ }
+ }
+ return Optional.empty();
+ }
+
+ private static List<String> messageHeaderValues(Message message, String name) {
+ List<String> values = new ArrayList<>();
+ Object header = message.getHeader(name);
+ if (header instanceof Iterable<?> iterable) {
+ iterable.forEach(value -> {
+ if (value != null) {
+ values.add(value.toString());
+ }
+ });
+ } else if (header != null) {
+ values.add(header.toString());
+ }
+
+ String query = message.getHeader(Exchange.HTTP_RAW_QUERY, String.class);
+ if (query == null) {
+ query = message.getHeader(Exchange.HTTP_QUERY, String.class);
+ }
+ if (query != null && !query.isEmpty() && !values.isEmpty()) {
+ String rawQuery = query;
+ String decodedQuery = decode(rawQuery);
+ if (decodedQuery == null) {
+ // a query string that cannot be decoded cannot be checked either, so none of the values is used
+ LOG.debug("The {} message header is not used: the query string of the request cannot be decoded", name);
+ values.clear();
+ } else if (values.removeIf(value -> rawQuery.contains(value) || decodedQuery.contains(value))) {
+ LOG.debug("A {} message header value is not used: the query string of the request holds it too", name);
+ }
+ }
+ return values;
+ }
+
+ private static String decode(String query) {
+ try {
+ return URLDecoder.decode(query, StandardCharsets.UTF_8);
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+}
diff --git a/components/camel-oauth/src/main/java/org/apache/camel/oauth/InMemorySessionStore.java b/components/camel-oauth/src/main/java/org/apache/camel/oauth/InMemorySessionStore.java
index 22244eae6c90..70c10047455c 100644
--- a/components/camel-oauth/src/main/java/org/apache/camel/oauth/InMemorySessionStore.java
+++ b/components/camel-oauth/src/main/java/org/apache/camel/oauth/InMemorySessionStore.java
@@ -80,13 +80,13 @@ public class InMemorySessionStore implements OAuthSessionStore {
private Map<String, String> getCookies(Exchange exchange) {
var msg = exchange.getMessage();
- var maybeCookie = Optional.ofNullable(msg.getHeader("Cookie"));
+ var maybeCookie = getCookieHeader(msg);
if (maybeCookie.isEmpty()) {
log.warn("No Cookie in HTTP request");
return Map.of();
}
- var value = maybeCookie.get().toString();
+ var value = maybeCookie.get();
var cookieMap = Arrays.stream(value.split(";"))
.map(String::trim)
.map(s -> s.split("=", 2))
@@ -97,6 +97,16 @@ public class InMemorySessionStore implements OAuthSessionStore {
return cookieMap;
}
+ /**
+ * The Cookie header of the request, read with {@link HttpRequestHeaders} because the message headers also hold the
+ * query and form parameters of the request.
+ */
+ private static Optional<String> getCookieHeader(Message msg) {
+ var values = HttpRequestHeaders.values(msg, "Cookie");
+ // HTTP/2 may send the cookies in several Cookie headers (RFC 9113 section 8.2.3)
+ return values.isEmpty() ? Optional.empty() : Optional.of(String.join("; ", values));
+ }
+
private void setSessionCookie(Message msg, OAuthSession session) {
var sessionId = session.getSessionId();
var cookieId = "%s=%s".formatted(CAMEL_OAUTH_COOKIE, sessionId);
diff --git a/components/camel-oauth/src/main/java/org/apache/camel/oauth/NettyHttpRequestHeaders.java b/components/camel-oauth/src/main/java/org/apache/camel/oauth/NettyHttpRequestHeaders.java
new file mode 100644
index 000000000000..914e3e6f9420
--- /dev/null
+++ b/components/camel-oauth/src/main/java/org/apache/camel/oauth/NettyHttpRequestHeaders.java
@@ -0,0 +1,38 @@
+/*
+ * 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.oauth;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.camel.Message;
+import org.apache.camel.component.netty.http.NettyHttpMessage;
+
+/**
+ * Reads request headers from a camel-netty-http message. Only loaded for a message of that type, see
+ * {@link HttpRequestHeaders}.
+ */
+final class NettyHttpRequestHeaders {
+
+ private NettyHttpRequestHeaders() {
+ }
+
+ static Optional<List<String>> values(Message message, String name) {
+ var request = ((NettyHttpMessage) message).getHttpRequest();
+ return request != null ? Optional.of(request.headers().getAll(name)) : Optional.empty();
+ }
+}
diff --git a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuth.java b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuth.java
index a88f472c6b54..3d2ce2583b98 100644
--- a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuth.java
+++ b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuth.java
@@ -43,6 +43,13 @@ public abstract class OAuth {
//
public static final String CAMEL_OAUTH_COOKIE = "camel.oauth.session";
+ // Camel OAuth Exchange Properties
+ //
+ /**
+ * The {@link UserProfile} that {@link OAuthBearerTokenProcessor} authenticated for the current request.
+ */
+ public static final String CAMEL_OAUTH_USER_PROFILE = "CamelOAuthUserProfile";
+
protected OAuthConfig config;
protected OAuthSessionStore sessionStore;
diff --git a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthBearerTokenProcessor.java b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthBearerTokenProcessor.java
index 337a29968946..cf9ec132a470 100644
--- a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthBearerTokenProcessor.java
+++ b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthBearerTokenProcessor.java
@@ -20,6 +20,14 @@ import org.apache.camel.Exchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import static org.apache.camel.oauth.OAuth.CAMEL_OAUTH_USER_PROFILE;
+
+/**
+ * Authenticates the bearer token of the request, as sent in its {@code Authorization} header (RFC 6750 section 2.1).
+ * <p>
+ * Bearer authentication is per request: the authenticated {@link UserProfile} is set as the
+ * {@link OAuth#CAMEL_OAUTH_USER_PROFILE} exchange property, and no {@link OAuthSession} is created or updated.
+ */
public class OAuthBearerTokenProcessor extends AbstractOAuthProcessor {
private final Logger log = LoggerFactory.getLogger(getClass());
@@ -33,14 +41,20 @@ public class OAuthBearerTokenProcessor extends AbstractOAuthProcessor {
// Validate Authorization header
//
- var authHeader = msg.getHeader("Authorization", String.class);
- if (authHeader == null) {
+ var authHeaders = HttpRequestHeaders.values(msg, "Authorization");
+ if (authHeaders.isEmpty()) {
log.error("No Authorization header in request");
rejectUnauthorized(exchange, "Authorization header");
return;
}
- var toks = authHeader.split(" ");
+ if (authHeaders.size() > 1) {
+ log.error("Invalid Authorization header: expected exactly one, got {}", authHeaders.size());
+ rejectUnauthorized(exchange, "Invalid Authorization header");
+ return;
+ }
+
+ var toks = authHeaders.get(0).split(" ");
if (toks.length != 2 || !"Bearer".equals(toks[0])) {
log.error("Invalid Authorization header");
rejectUnauthorized(exchange, "Invalid Authorization header");
@@ -59,10 +73,9 @@ public class OAuthBearerTokenProcessor extends AbstractOAuthProcessor {
var access_token = toks[1];
var userProfile = oauth.authenticate(new TokenCredentials(access_token));
- // Get or create the OAuthSession
+ // The token authenticates this request only, so the profile goes on the exchange and not into an OAuthSession
//
- var session = oauth.getOrCreateSession(exchange);
- session.putUserProfile(userProfile);
+ exchange.setProperty(CAMEL_OAUTH_USER_PROFILE, userProfile);
log.info("Authenticated {}", userProfile.subject());
userProfile.logDetails();
diff --git a/components/camel-oauth/src/main/java/org/apache/camel/oauth/ServletHttpRequestHeaders.java b/components/camel-oauth/src/main/java/org/apache/camel/oauth/ServletHttpRequestHeaders.java
new file mode 100644
index 000000000000..d9c0947413ca
--- /dev/null
+++ b/components/camel-oauth/src/main/java/org/apache/camel/oauth/ServletHttpRequestHeaders.java
@@ -0,0 +1,81 @@
+/*
+ * 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.oauth;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.Message;
+import org.apache.camel.http.common.HttpMessage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Reads request headers from a message of the servlet based HTTP consumers. Only loaded for a message of such a type,
+ * see {@link HttpRequestHeaders}.
+ */
+final class ServletHttpRequestHeaders {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ServletHttpRequestHeaders.class);
+
+ private ServletHttpRequestHeaders() {
+ }
+
+ /**
+ * Reads the headers of the request of a camel-http-common {@link HttpMessage} (camel-servlet, camel-jetty).
+ */
+ static Optional<List<String>> values(Message message, String name) {
+ return values(((HttpMessage) message).getRequest(), name);
+ }
+
+ /**
+ * Reads the headers of the request of a platform-http message on Spring Boot. The request comes from the
+ * {@code getRequest()} method of the message, which camel-oauth does not depend on, or else from the
+ * {@link Exchange#HTTP_SERVLET_REQUEST} header that the message also sets, but that a route may remove. Without a
+ * request no value is read at all, rather than the message headers, which also hold the form parameters.
+ */
+ static Optional<List<String>> valuesOfSpringBootMessage(Message message, String name) {
+ var request = springBootRequest(message);
+ if (request == null) {
+ LOG.debug("No HTTP request on the platform-http message, so no {} header is read", name);
+ return Optional.of(List.of());
+ }
+ return Optional.of(values(request, name).orElse(List.of()));
+ }
+
+ private static HttpServletRequest springBootRequest(Message message) {
+ try {
+ if (message.getClass().getMethod("getRequest").invoke(message) instanceof HttpServletRequest request) {
+ return request;
+ }
+ } catch (ReflectiveOperationException | SecurityException e) {
+ LOG.debug("Cannot call getRequest() on {}, reading the {} header instead", message.getClass().getName(),
+ Exchange.HTTP_SERVLET_REQUEST, e);
+ }
+ return message.getHeader(Exchange.HTTP_SERVLET_REQUEST) instanceof HttpServletRequest request ? request : null;
+ }
+
+ private static Optional<List<String>> values(HttpServletRequest request, String name) {
+ // a container that does not allow access to the headers returns null
+ var values = request != null ? request.getHeaders(name) : null;
+ return values != null ? Optional.of(Collections.list(values)) : Optional.empty();
+ }
+}
diff --git a/components/camel-oauth/src/main/java/org/apache/camel/oauth/VertxHttpRequestHeaders.java b/components/camel-oauth/src/main/java/org/apache/camel/oauth/VertxHttpRequestHeaders.java
new file mode 100644
index 000000000000..48c23920bd43
--- /dev/null
+++ b/components/camel-oauth/src/main/java/org/apache/camel/oauth/VertxHttpRequestHeaders.java
@@ -0,0 +1,38 @@
+/*
+ * 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.oauth;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.camel.Message;
+import org.apache.camel.component.platform.http.vertx.HttpMessage;
+
+/**
+ * Reads request headers from a platform-http message on Vert.x. Only loaded for a message of that type, see
+ * {@link HttpRequestHeaders}.
+ */
+final class VertxHttpRequestHeaders {
+
+ private VertxHttpRequestHeaders() {
+ }
+
+ static Optional<List<String>> values(Message message, String name) {
+ var request = ((HttpMessage) message).getRequest();
+ return request != null ? Optional.of(request.headers().getAll(name)) : Optional.empty();
+ }
+}
diff --git a/components/camel-oauth/src/test/java/org/apache/camel/component/platform/http/springboot/PlatformHttpMessage.java b/components/camel-oauth/src/test/java/org/apache/camel/component/platform/http/springboot/PlatformHttpMessage.java
new file mode 100644
index 000000000000..5f9aa36db903
--- /dev/null
+++ b/components/camel-oauth/src/test/java/org/apache/camel/component/platform/http/springboot/PlatformHttpMessage.java
@@ -0,0 +1,43 @@
+/*
+ * 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.platform.http.springboot;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.support.DefaultMessage;
+
+/**
+ * Test stand-in for the message of platform-http on Spring Boot (camel-platform-http-starter, in camel-spring-boot),
+ * which camel-oauth does not depend on. It has the same name and, like the real one, gives the servlet request from
+ * {@link #getRequest()} and holds it in the {@link Exchange#HTTP_SERVLET_REQUEST} header.
+ */
+public class PlatformHttpMessage extends DefaultMessage {
+
+ private final HttpServletRequest request;
+
+ public PlatformHttpMessage(CamelContext camelContext, HttpServletRequest request) {
+ super(camelContext);
+ this.request = request;
+ setHeader(Exchange.HTTP_SERVLET_REQUEST, request);
+ }
+
+ public HttpServletRequest getRequest() {
+ return request;
+ }
+}
diff --git a/components/camel-oauth/src/test/java/org/apache/camel/oauth/AbstractOAuthRequestHeadersTest.java b/components/camel-oauth/src/test/java/org/apache/camel/oauth/AbstractOAuthRequestHeadersTest.java
new file mode 100644
index 000000000000..2fa2eeb6643f
--- /dev/null
+++ b/components/camel-oauth/src/test/java/org/apache/camel/oauth/AbstractOAuthRequestHeadersTest.java
@@ -0,0 +1,249 @@
+/*
+ * 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.oauth;
+
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.test.AvailablePortFinder;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+/**
+ * The OAuth processors behind a real HTTP consumer: the bearer token and the session cookie must come from the
+ * {@code Authorization} and {@code Cookie} request headers, although the consumer also maps the query and form
+ * parameters of the request into the message headers. The identity provider is {@link StubOAuth}.
+ */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+abstract class AbstractOAuthRequestHeadersTest {
+
+ private final HttpClient client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NEVER).build();
+
+ private int port;
+ private CamelContext context;
+ private OAuth oauth;
+
+ /**
+ * Starts the HTTP server, before the routes are added to the context.
+ */
+ abstract void startServer(CamelContext context, int port) throws Exception;
+
+ abstract void stopServer() throws Exception;
+
+ /**
+ * The consumer endpoint uri for the given path.
+ */
+ abstract String endpointUri(String path);
+
+ /**
+ * Whether the processors can read the HTTP request of this consumer. When they cannot, they read the message
+ * headers, without the values that also appear in the query string.
+ */
+ boolean readsTheRequestHeaders() {
+ return true;
+ }
+
+ @BeforeAll
+ void start() throws Exception {
+ port = AvailablePortFinder.getNextAvailable();
+ context = new DefaultCamelContext();
+ context.getPropertiesComponent().addInitialProperty(OAuth.CAMEL_OAUTH_REDIRECT_URI, "https://app.example/auth");
+ oauth = new StubOAuth();
+ context.getRegistry().bind(OAuth.class.getName(), oauth);
+ startServer(context, port);
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from(endpointUri("/api"))
+ .process(new OAuthBearerTokenProcessor())
+ .process(e -> e.getMessage().setBody("api:" + bearerSubject(e)));
+ from(endpointUri("/auth"))
+ .process(new OAuthCodeFlowCallback());
+ from(endpointUri("/app"))
+ .process(new OAuthCodeFlowProcessor())
+ .process(e -> e.getMessage().setBody("app:" + sessionSubject(e)));
+ }
+ });
+ context.start();
+ }
+
+ @AfterAll
+ void stop() throws Exception {
+ if (context != null) {
+ context.stop();
+ }
+ stopServer();
+ }
+
+ @Test
+ void bearerTokenIsReadFromTheAuthorizationHeader() throws Exception {
+ var response = get("/api", null, "Bearer " + StubOAuth.ALICE_TOKEN);
+
+ assertEquals(200, response.statusCode());
+ assertEquals("api:alice", response.body());
+ assertNull(setCookie(response), "a bearer request does not start a session");
+ }
+
+ @Test
+ void bearerTokenIsNotReadFromAQueryParameter() throws Exception {
+ var response = get("/api?Authorization=Bearer%20" + StubOAuth.ALICE_TOKEN, null, null);
+
+ assertEquals(401, response.statusCode());
+ assertNull(setCookie(response));
+ }
+
+ @Test
+ void bearerTokenIsNotReadFromAFormParameter() throws Exception {
+ assumeTrue(readsTheRequestHeaders(),
+ "the request of this consumer cannot be read from the route, so its form parameters are message headers");
+
+ var request = request("/api")
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .POST(HttpRequest.BodyPublishers.ofString("Authorization=Bearer%20" + StubOAuth.ALICE_TOKEN))
+ .build();
+ var response = client.send(request, HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(401, response.statusCode());
+ assertNull(setCookie(response));
+ }
+
+ @Test
+ void authorizationHeaderIsReadWhateverTheQueryStringHolds() throws Exception {
+ assumeTrue(readsTheRequestHeaders(),
+ "the request of this consumer cannot be read from the route, so a value in the query string is left out");
+
+ var response = get("/api?note=Bearer%20" + StubOAuth.ALICE_TOKEN, null, "Bearer " + StubOAuth.ALICE_TOKEN);
+
+ assertEquals(200, response.statusCode());
+ assertEquals("api:alice", response.body());
+ }
+
+ @Test
+ void aQueryParameterDoesNotReplaceTheAuthorizationHeader() throws Exception {
+ var response = get("/api?Authorization=Bearer%20" + StubOAuth.BOB_TOKEN, null, "Bearer " + StubOAuth.ALICE_TOKEN);
+
+ assertEquals(200, response.statusCode());
+ assertEquals("api:alice", response.body());
+ }
+
+ @Test
+ void severalAuthorizationHeadersAreRejected() throws Exception {
+ var request = request("/api")
+ .header("Authorization", "Bearer " + StubOAuth.ALICE_TOKEN)
+ .header("Authorization", "Bearer " + StubOAuth.BOB_TOKEN)
+ .GET()
+ .build();
+ var response = client.send(request, HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(401, response.statusCode());
+ }
+
+ @Test
+ void sessionCookieIsReadFromTheCookieHeader() throws Exception {
+ var cookie = loginAsBob();
+
+ var response = get("/app", cookie, null);
+
+ assertEquals(200, response.statusCode());
+ assertEquals("app:bob", response.body());
+ }
+
+ @Test
+ void sessionCookieIsNotReadFromAQueryParameter() throws Exception {
+ var cookie = loginAsBob();
+
+ var response = get("/app?Cookie=" + URLEncoder.encode(cookie, StandardCharsets.UTF_8), null, null);
+
+ assertEquals(302, response.statusCode(), "a request without a session cookie is sent to the login");
+ }
+
+ @Test
+ void bearerRequestLeavesTheSessionUnchanged() throws Exception {
+ var cookie = loginAsBob();
+
+ var response = get("/api", cookie, "Bearer " + StubOAuth.ALICE_TOKEN);
+
+ assertEquals(200, response.statusCode());
+ assertEquals("api:alice", response.body());
+ assertNull(setCookie(response));
+ assertEquals("app:bob", get("/app", cookie, null).body());
+ }
+
+ /**
+ * Runs the authorization code flow and returns the session cookie the browser holds afterwards.
+ */
+ private String loginAsBob() throws Exception {
+ var start = get("/app", null, null);
+ assertEquals(302, start.statusCode());
+ var sessionCookie = setCookie(start);
+ assertNotNull(sessionCookie);
+ var cookie = sessionCookie.substring(0, sessionCookie.indexOf(';'));
+ var location = start.headers().firstValue("Location").orElseThrow();
+ var state = location.substring(location.indexOf("state=") + "state=".length());
+
+ var callback = get("/auth?code=" + StubOAuth.BOB_CODE + "&state=" + URLEncoder.encode(state, StandardCharsets.UTF_8),
+ cookie, null);
+ assertEquals(302, callback.statusCode());
+ return cookie;
+ }
+
+ HttpResponse<String> get(String pathAndQuery, String cookie, String authorization) throws Exception {
+ var builder = request(pathAndQuery).GET();
+ if (cookie != null) {
+ builder.header("Cookie", cookie);
+ }
+ if (authorization != null) {
+ builder.header("Authorization", authorization);
+ }
+ return client.send(builder.build(), HttpResponse.BodyHandlers.ofString());
+ }
+
+ private HttpRequest.Builder request(String pathAndQuery) {
+ return HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + port + pathAndQuery));
+ }
+
+ private static String setCookie(HttpResponse<?> response) {
+ return response.headers().firstValue("Set-Cookie").orElse(null);
+ }
+
+ private static String bearerSubject(Exchange exchange) {
+ var profile = exchange.getProperty(OAuth.CAMEL_OAUTH_USER_PROFILE, UserProfile.class);
+ return profile != null ? profile.subject() : "none";
+ }
+
+ private String sessionSubject(Exchange exchange) {
+ return oauth.getSession(exchange)
+ .flatMap(OAuthSession::getUserProfile)
+ .map(UserProfile::subject)
+ .orElse("none");
+ }
+}
diff --git a/components/camel-oauth/src/test/java/org/apache/camel/oauth/HttpRequestHeadersTest.java b/components/camel-oauth/src/test/java/org/apache/camel/oauth/HttpRequestHeadersTest.java
new file mode 100644
index 000000000000..07fb18aa50b6
--- /dev/null
+++ b/components/camel-oauth/src/test/java/org/apache/camel/oauth/HttpRequestHeadersTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.oauth;
+
+import java.lang.reflect.Proxy;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.Message;
+import org.apache.camel.component.platform.http.springboot.PlatformHttpMessage;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * A message that gives no access to the HTTP request: the header values are read from the message, without the values
+ * that also appear in the query string of the request. And the message of platform-http on Spring Boot, which holds the
+ * servlet request in a header.
+ */
+class HttpRequestHeadersTest {
+
+ @Test
+ void messageHeaderIsReadWhenThereIsNoQueryString() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ Message message = new DefaultExchange(context).getMessage();
+ message.setHeader("Authorization", "Bearer token");
+
+ assertEquals(List.of("Bearer token"), HttpRequestHeaders.values(message, "Authorization"));
+ }
+ }
+
+ @Test
+ void messageHeaderIsReadWhenTheQueryStringDoesNotHoldIt() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ Message message = new DefaultExchange(context).getMessage();
+ message.setHeader("Authorization", "Bearer token");
+ message.setHeader(Exchange.HTTP_RAW_QUERY, "page=2&sort=name");
+
+ assertEquals(List.of("Bearer token"), HttpRequestHeaders.values(message, "Authorization"));
+ }
+ }
+
+ @Test
+ void valueThatTheQueryStringHoldsIsLeftOut() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ Message message = new DefaultExchange(context).getMessage();
+ message.setHeader("Authorization", "Bearer token");
+ message.setHeader(Exchange.HTTP_RAW_QUERY, "Authorization=Bearer%20token");
+
+ assertEquals(List.of(), HttpRequestHeaders.values(message, "Authorization"));
+ }
+ }
+
+ @Test
+ void valueThatTheUndecodedQueryStringHoldsIsLeftOut() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ Message message = new DefaultExchange(context).getMessage();
+ message.setHeader("Cookie", "camel.oauth.session=abc");
+ message.setHeader(Exchange.HTTP_QUERY, "Cookie=camel.oauth.session=abc");
+
+ assertEquals(List.of(), HttpRequestHeaders.values(message, "Cookie"));
+ }
+ }
+
+ @Test
+ void onlyTheValueThatTheQueryStringHoldsIsLeftOut() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ Message message = new DefaultExchange(context).getMessage();
+ message.setHeader("Authorization", List.of("Bearer header-token", "Bearer query-token"));
+ message.setHeader(Exchange.HTTP_RAW_QUERY, "Authorization=Bearer%20query-token");
+
+ assertEquals(List.of("Bearer header-token"), HttpRequestHeaders.values(message, "Authorization"));
+ }
+ }
+
+ @Test
+ void noValueIsReadWhenTheQueryStringCannotBeDecoded() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ Message message = new DefaultExchange(context).getMessage();
+ message.setHeader("Authorization", "Bearer token");
+ message.setHeader(Exchange.HTTP_RAW_QUERY, "discount=100%");
+
+ assertEquals(List.of(), HttpRequestHeaders.values(message, "Authorization"));
+ }
+ }
+
+ @Test
+ void springBootMessageIsReadFromTheServletRequest() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ Message message = new PlatformHttpMessage(
+ context, servletRequest(Map.of("authorization", List.of("Bearer request-token"))));
+ // what the binding maps from a form field of the same name
+ message.setHeader("Authorization", "Bearer form-token");
+
+ assertEquals(List.of("Bearer request-token"), HttpRequestHeaders.values(message, "Authorization"));
+ }
+ }
+
+ @Test
+ void springBootMessageHeaderIsNotReadWhenTheRequestHasNone() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ Message message = new PlatformHttpMessage(context, servletRequest(Map.of()));
+ message.setHeader("Authorization", "Bearer form-token");
+
+ assertEquals(List.of(), HttpRequestHeaders.values(message, "Authorization"));
+ }
+ }
+
+ @Test
+ void springBootMessageIsReadWhenTheRouteRemovedTheServletRequestHeader() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ Message message = new PlatformHttpMessage(
+ context, servletRequest(Map.of("authorization", List.of("Bearer request-token"))));
+ message.setHeader("Authorization", "Bearer form-token");
+ // as the hardening checklist of the security model advises for untrusted callers
+ message.removeHeaders("Camel*");
+
+ assertEquals(List.of("Bearer request-token"), HttpRequestHeaders.values(message, "Authorization"));
+ }
+ }
+
+ @Test
+ void springBootMessageWithoutARequestReadsNothing() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ Message message = new PlatformHttpMessage(context, null);
+ message.setHeader("Authorization", "Bearer form-token");
+
+ assertEquals(List.of(), HttpRequestHeaders.values(message, "Authorization"));
+ }
+ }
+
+ /**
+ * A servlet request with the given headers, keyed by lower case name, that only answers {@code getHeaders}.
+ */
+ private static HttpServletRequest servletRequest(Map<String, List<String>> headers) {
+ return (HttpServletRequest) Proxy.newProxyInstance(HttpRequestHeadersTest.class.getClassLoader(),
+ new Class<?>[] { HttpServletRequest.class },
+ (proxy, method, args) -> switch (method.getName()) {
+ case "getHeaders" -> Collections.enumeration(
+ headers.getOrDefault(((String) args[0]).toLowerCase(Locale.ROOT), List.of()));
+ case "toString" -> "HttpServletRequest" + headers;
+ case "hashCode" -> System.identityHashCode(proxy);
+ case "equals" -> proxy == args[0];
+ default -> throw new UnsupportedOperationException(method.getName());
+ });
+ }
+}
diff --git a/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthBearerTokenProcessorTest.java b/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthBearerTokenProcessorTest.java
new file mode 100644
index 000000000000..3ab41a6a3a83
--- /dev/null
+++ b/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthBearerTokenProcessorTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.oauth;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Bearer authentication on a message that carries no HTTP request, so the processor reads the message headers.
+ */
+class OAuthBearerTokenProcessorTest {
+
+ @Test
+ void authenticatedProfileIsSetOnTheExchange() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ context.getRegistry().bind(OAuth.class.getName(), new StubOAuth());
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getMessage().setHeader("Authorization", "Bearer " + StubOAuth.ALICE_TOKEN);
+
+ new OAuthBearerTokenProcessor().process(exchange);
+
+ UserProfile profile = exchange.getProperty(OAuth.CAMEL_OAUTH_USER_PROFILE, UserProfile.class);
+ assertNotNull(profile);
+ assertEquals("alice", profile.subject());
+ assertFalse(exchange.isRouteStop());
+ }
+ }
+
+ @Test
+ void noSessionIsCreated() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ OAuth oauth = new StubOAuth();
+ context.getRegistry().bind(OAuth.class.getName(), oauth);
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getMessage().setHeader("Authorization", "Bearer " + StubOAuth.ALICE_TOKEN);
+
+ new OAuthBearerTokenProcessor().process(exchange);
+
+ assertTrue(oauth.getSession(exchange).isEmpty());
+ assertNull(exchange.getMessage().getHeader("Set-Cookie"));
+ assertNull(exchange.getMessage().getHeader(OAuth.CAMEL_OAUTH_SESSION_ID));
+ }
+ }
+
+ @Test
+ void existingSessionIsLeftUnchanged() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ OAuth oauth = new StubOAuth();
+ context.getRegistry().bind(OAuth.class.getName(), oauth);
+ OAuthSession session = oauth.createSession(new DefaultExchange(context));
+ session.putUserProfile(oauth.authenticate(new TokenCredentials(StubOAuth.BOB_TOKEN)));
+
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getMessage().setHeader("Cookie", OAuth.CAMEL_OAUTH_COOKIE + "=" + session.getSessionId());
+ exchange.getMessage().setHeader("Authorization", "Bearer " + StubOAuth.ALICE_TOKEN);
+
+ new OAuthBearerTokenProcessor().process(exchange);
+
+ assertEquals("alice", exchange.getProperty(OAuth.CAMEL_OAUTH_USER_PROFILE, UserProfile.class).subject());
+ assertEquals("bob", session.getUserProfile().orElseThrow().subject());
+ }
+ }
+}
diff --git a/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthRequestHeadersNettyHttpTest.java b/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthRequestHeadersNettyHttpTest.java
new file mode 100644
index 000000000000..fc85816ee6b2
--- /dev/null
+++ b/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthRequestHeadersNettyHttpTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.oauth;
+
+import org.apache.camel.CamelContext;
+
+/**
+ * The OAuth processors behind camel-netty-http.
+ */
+class OAuthRequestHeadersNettyHttpTest extends AbstractOAuthRequestHeadersTest {
+
+ private int port;
+
+ @Override
+ void startServer(CamelContext context, int port) {
+ // the consumers start the server
+ this.port = port;
+ }
+
+ @Override
+ void stopServer() {
+ // stopped with the context
+ }
+
+ @Override
+ String endpointUri(String path) {
+ return "netty-http:http://127.0.0.1:" + port + path;
+ }
+}
diff --git a/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthRequestHeadersServletTest.java b/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthRequestHeadersServletTest.java
new file mode 100644
index 000000000000..aa801d2c57a3
--- /dev/null
+++ b/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthRequestHeadersServletTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.oauth;
+
+import io.undertow.Handlers;
+import io.undertow.Undertow;
+import io.undertow.servlet.Servlets;
+import io.undertow.servlet.api.DeploymentManager;
+import org.apache.camel.CamelContext;
+import org.apache.camel.component.servlet.CamelHttpTransportServlet;
+
+/**
+ * The OAuth processors behind camel-servlet.
+ */
+class OAuthRequestHeadersServletTest extends AbstractOAuthRequestHeadersTest {
+
+ // a servlet name of its own, so that other servlet tests of the module do not share the servlet registry entry
+ private static final String SERVLET_NAME = "OAuthRequestHeadersServlet";
+
+ private DeploymentManager manager;
+ private Undertow server;
+
+ @Override
+ void startServer(CamelContext context, int port) throws Exception {
+ var deploymentInfo = Servlets.deployment()
+ .setContextPath("/")
+ .setDeploymentName(SERVLET_NAME)
+ .setClassLoader(OAuthRequestHeadersServletTest.class.getClassLoader())
+ .addServlet(Servlets.servlet(SERVLET_NAME, CamelHttpTransportServlet.class).addMapping("/*"));
+ manager = Servlets.newContainer().addDeployment(deploymentInfo);
+ manager.deploy();
+ var path = Handlers.path(Handlers.redirect("/")).addPrefixPath("/", manager.start());
+ server = Undertow.builder().addHttpListener(port, "127.0.0.1").setHandler(path).build();
+ server.start();
+ }
+
+ @Override
+ void stopServer() throws Exception {
+ if (manager != null) {
+ manager.stop();
+ manager.undeploy();
+ }
+ if (server != null) {
+ server.stop();
+ }
+ }
+
+ @Override
+ String endpointUri(String path) {
+ return "servlet:" + path + "?servletName=" + SERVLET_NAME;
+ }
+}
diff --git a/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthRequestHeadersUndertowTest.java b/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthRequestHeadersUndertowTest.java
new file mode 100644
index 000000000000..043bf2da45f2
--- /dev/null
+++ b/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthRequestHeadersUndertowTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.oauth;
+
+import org.apache.camel.CamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * The OAuth processors behind camel-undertow, whose messages do not give access to the HTTP request: the processors
+ * read the message headers, without the values that also appear in the query string.
+ */
+class OAuthRequestHeadersUndertowTest extends AbstractOAuthRequestHeadersTest {
+
+ private int port;
+
+ @Override
+ void startServer(CamelContext context, int port) {
+ // the consumers start the server
+ this.port = port;
+ }
+
+ @Override
+ void stopServer() {
+ // stopped with the context
+ }
+
+ @Override
+ String endpointUri(String path) {
+ return "undertow:http://127.0.0.1:" + port + path;
+ }
+
+ @Override
+ boolean readsTheRequestHeaders() {
+ return false;
+ }
+
+ @Test
+ void authorizationHeaderThatTheQueryStringAlsoHoldsIsNotRead() throws Exception {
+ var response = get("/api?note=Bearer%20" + StubOAuth.ALICE_TOKEN, null, "Bearer " + StubOAuth.ALICE_TOKEN);
+
+ assertEquals(401, response.statusCode());
+ }
+}
diff --git a/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthRequestHeadersVertxTest.java b/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthRequestHeadersVertxTest.java
new file mode 100644
index 000000000000..993c1ca04d24
--- /dev/null
+++ b/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthRequestHeadersVertxTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.oauth;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.component.platform.http.vertx.VertxPlatformHttpServer;
+import org.apache.camel.component.platform.http.vertx.VertxPlatformHttpServerConfiguration;
+
+/**
+ * The OAuth processors behind platform-http on Vert.x.
+ */
+class OAuthRequestHeadersVertxTest extends AbstractOAuthRequestHeadersTest {
+
+ @Override
+ void startServer(CamelContext context, int port) throws Exception {
+ var conf = new VertxPlatformHttpServerConfiguration();
+ conf.setBindHost("127.0.0.1");
+ conf.setBindPort(port);
+ context.addService(new VertxPlatformHttpServer(conf));
+ }
+
+ @Override
+ void stopServer() {
+ // stopped with the context
+ }
+
+ @Override
+ String endpointUri(String path) {
+ return "platform-http:" + path;
+ }
+}
diff --git a/components/camel-oauth/src/test/java/org/apache/camel/oauth/StubOAuth.java b/components/camel-oauth/src/test/java/org/apache/camel/oauth/StubOAuth.java
new file mode 100644
index 000000000000..e078d22777b6
--- /dev/null
+++ b/components/camel-oauth/src/test/java/org/apache/camel/oauth/StubOAuth.java
@@ -0,0 +1,74 @@
+/*
+ * 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.oauth;
+
+import com.google.gson.JsonObject;
+import org.apache.camel.CamelContext;
+
+/**
+ * An identity provider stand-in: it knows the access tokens of alice and bob, and one authorization code that logs bob
+ * in. Everything else is rejected.
+ */
+class StubOAuth extends OAuth {
+
+ static final String ALICE_TOKEN = "alice-token";
+ static final String BOB_TOKEN = "bob-token";
+ static final String BOB_CODE = "bob-code";
+
+ @Override
+ public void discoverOAuthConfig(CamelContext ctx) {
+ // nothing to discover
+ }
+
+ @Override
+ public UserProfile authenticate(Credentials creds) {
+ String token;
+ if (creds instanceof TokenCredentials tokenCreds) {
+ token = tokenCreds.getToken();
+ } else if (creds instanceof UserCredentials userCreds) {
+ token = userCreds.getUserProfile().accessToken().orElseThrow();
+ } else if (creds instanceof AuthCodeCredentials codeCreds && BOB_CODE.equals(codeCreds.getCode())) {
+ token = BOB_TOKEN;
+ } else {
+ throw new OAuthException("Unsupported credentials");
+ }
+ String user;
+ if (ALICE_TOKEN.equals(token)) {
+ user = "alice";
+ } else if (BOB_TOKEN.equals(token)) {
+ user = "bob";
+ } else {
+ throw new OAuthException("Unknown token");
+ }
+ var principal = new JsonObject();
+ principal.addProperty("access_token", token);
+ principal.addProperty("username", user);
+ var attributes = new JsonObject();
+ attributes.addProperty("exp", System.currentTimeMillis() / 1000L + 3600);
+ return new UserProfile(principal, attributes);
+ }
+
+ @Override
+ public String buildLogoutRequestUrl(OAuthLogoutParams params) {
+ return "https://idp.example/logout";
+ }
+
+ @Override
+ public String buildCodeFlowAuthRequestUrl(OAuthCodeFlowParams params) {
+ return "https://idp.example/auth?state=" + params.getState();
+ }
+}