diff --git a/components/camel-oauth/pom.xml b/components/camel-oauth/pom.xml
index 3b6da44c69020..4916362ff38b9 100644
--- a/components/camel-oauth/pom.xml
+++ b/components/camel-oauth/pom.xml
@@ -114,6 +114,11 @@
${rest-assured-version}
test
+
+ org.assertj
+ assertj-core
+ test
+
diff --git a/components/camel-oauth/src/main/java/org/apache/camel/oauth/AbstractOAuthProcessor.java b/components/camel-oauth/src/main/java/org/apache/camel/oauth/AbstractOAuthProcessor.java
index e7847205a9ead..8a480c458b005 100644
--- a/components/camel-oauth/src/main/java/org/apache/camel/oauth/AbstractOAuthProcessor.java
+++ b/components/camel-oauth/src/main/java/org/apache/camel/oauth/AbstractOAuthProcessor.java
@@ -64,6 +64,33 @@ protected void logRequestHeaders(String msgPrefix, Message msg) {
});
}
+ /**
+ * Rejects the current request and stops the route, so that no subsequent step runs for a request this processor did
+ * not authenticate. Absence of credentials must be rejected at least as strongly as invalid credentials.
+ *
+ * @param exchange the exchange to reject and stop
+ * @param statusCode the HTTP status code to reply with
+ * @param body the response body
+ */
+ protected void reject(Exchange exchange, int statusCode, String body) {
+ var msg = exchange.getMessage();
+ msg.setHeader(Exchange.HTTP_RESPONSE_CODE, statusCode);
+ msg.setBody(body);
+ exchange.setRouteStop(true);
+ }
+
+ /**
+ * Rejects the current request as unauthenticated with a {@code 401} and a {@code WWW-Authenticate: Bearer}
+ * challenge, as required by RFC 6750, and stops the route.
+ *
+ * @param exchange the exchange to reject and stop
+ * @param body the response body
+ */
+ protected void rejectUnauthorized(Exchange exchange, String body) {
+ exchange.getMessage().setHeader("WWW-Authenticate", "Bearer");
+ reject(exchange, 401, body);
+ }
+
protected void sendRedirect(Message msg, String redirectUrl) {
log.debug("Redirect to: {}", redirectUrl);
msg.setHeader(Exchange.HTTP_RESPONSE_CODE, 302);
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 0819e7b37f9d9..337a299689463 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
@@ -36,16 +36,14 @@ public void process(Exchange exchange) {
var authHeader = msg.getHeader("Authorization", String.class);
if (authHeader == null) {
log.error("No Authorization header in request");
- msg.setHeader("CamelHttpResponseCode", 400);
- msg.setBody("Authorization header");
+ rejectUnauthorized(exchange, "Authorization header");
return;
}
var toks = authHeader.split(" ");
if (toks.length != 2 || !"Bearer".equals(toks[0])) {
- log.error("Invalid Authorization header: {}", authHeader);
- msg.setHeader("CamelHttpResponseCode", 400);
- msg.setBody("Invalid Authorization header");
+ log.error("Invalid Authorization header");
+ rejectUnauthorized(exchange, "Invalid Authorization header");
return;
}
diff --git a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowCallback.java b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowCallback.java
index 64b19141feaef..677305dd5cf2c 100644
--- a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowCallback.java
+++ b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowCallback.java
@@ -39,8 +39,7 @@ public void process(Exchange exchange) {
var authCode = msg.getHeader("code", String.class);
if (authCode == null) {
log.error("Authorization code is missing in the request");
- msg.setHeader("CamelHttpResponseCode", 400);
- msg.setBody("Authorization code missing");
+ reject(exchange, 400, "Authorization code missing");
return;
}
diff --git a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowProcessor.java b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowProcessor.java
index 52b1c9078adbe..2de2d8969b28a 100644
--- a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowProcessor.java
+++ b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowProcessor.java
@@ -70,6 +70,9 @@ public void process(Exchange exchange) {
var authRequestUrl = oauth.buildCodeFlowAuthRequestUrl(params);
sendRedirect(msg, authRequestUrl);
+
+ // The caller is not authenticated: the redirect is the whole response, so the protected route must not run
+ exchange.setRouteStop(true);
}
private String getPostLoginUrl(Message msg) {
diff --git a/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthProcessorFailClosedTest.java b/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthProcessorFailClosedTest.java
new file mode 100644
index 0000000000000..72a8d13ac5cfa
--- /dev/null
+++ b/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthProcessorFailClosedTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.assertj.core.api.Assertions.assertThat;
+
+/**
+ * A request the processors do not authenticate must stop the route, so that no subsequent step runs for it. These are
+ * the paths that return before any identity provider is contacted, so they can be exercised without one.
+ */
+class OAuthProcessorFailClosedTest {
+
+ @Test
+ void missingAuthorizationHeaderStopsTheRoute() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ Exchange exchange = new DefaultExchange(context);
+
+ new OAuthBearerTokenProcessor().process(exchange);
+
+ assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(401);
+ assertThat(exchange.getMessage().getHeader("WWW-Authenticate")).isEqualTo("Bearer");
+ assertThat(exchange.isRouteStop()).isTrue();
+ }
+ }
+
+ @Test
+ void nonBearerAuthorizationHeaderStopsTheRoute() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getMessage().setHeader("Authorization", "Basic c2NvdHQ6c2VjcmV0");
+
+ new OAuthBearerTokenProcessor().process(exchange);
+
+ assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(401);
+ assertThat(exchange.getMessage().getHeader("WWW-Authenticate")).isEqualTo("Bearer");
+ assertThat(exchange.isRouteStop()).isTrue();
+ }
+ }
+
+ @Test
+ void missingAuthorizationCodeStopsTheRoute() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ Exchange exchange = new DefaultExchange(context);
+
+ new OAuthCodeFlowCallback().process(exchange);
+
+ assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(400);
+ assertThat(exchange.isRouteStop()).isTrue();
+ }
+ }
+}
diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 9dd0869b124de..290c64ea0078a 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -95,3 +95,25 @@ denies `java.net.**`, and enforces JEP-290 graph-shape limits). Routes that dese
outside that allow-list must pass an explicit filter pattern to the two-argument
`ObjectDecoder(ClassResolver, String)` / `DatagramPacketObjectDecoder(ClassResolver, String)`
constructor (or configure `jdk.serialFilter`) to permit them.
+
+=== camel-oauth
+
+The OAuth processors now stop the route on the paths where they do not authenticate the caller, so
+that no subsequent step of the route runs for such a request. Previously they set a response code
+and returned, which left the rest of the route to execute and overwrite the response the processor
+had just prepared.
+
+What changed:
+
+* `OAuthBearerTokenProcessor` — a request with no `Authorization` header, or with one that does not
+parse as `Bearer `, is now answered with `401` and a `WWW-Authenticate: Bearer` challenge
+(RFC 6750) instead of `400`, and the route is stopped. A present-but-invalid token continues to fail
+by propagating the exception from `OAuth.authenticate()`, as before.
+* `OAuthCodeFlowProcessor` — when the caller has no authenticated session and is redirected to the
+identity provider, the route is now stopped; the `302` is the whole response.
+* `OAuthCodeFlowCallback` — a callback request without the `code` parameter still answers `400`, and
+now also stops the route.
+
+Routes that relied on steps after these processors running for unauthenticated requests must be
+restructured. The authenticated paths are unchanged: a successfully authenticated request continues
+through the rest of the route exactly as before, and `OAuthLogoutProcessor` is unchanged.