You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/10/14 19:20:50 UTC

(camel) branch camel-4.18.x updated: [backport camel-4.18.x] CAMEL-25568: camel-oauth - make OAuthBearerTokenProcessor stateless and read credentials from HTTP request headers (#27783)

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 c8f21896f9c9 [backport camel-4.18.x] CAMEL-25568: camel-oauth - make OAuthBearerTokenProcessor stateless and read credentials from HTTP request headers (#27783)
c8f21896f9c9 is described below

commit c8f21896f9c9105322613a9792315c27728d562c
Author: Andrea Cosentino <an...@gmail.com>
AuthorDate: Wed Oct 14 21:20:43 2026 +0200

    [backport camel-4.18.x] CAMEL-25568: camel-oauth - make OAuthBearerTokenProcessor stateless and read credentials from HTTP request headers (#27783)
    
    Co-Authored-By: Claude Opus 5.5 <no...@anthropic.com>
---
 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 e92f65709e4a..616bfce360f6 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 93bc587a52fc..dfa894e2a1ba 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();
+    }
+}