From f8e25532ababb0f4dd756b0623e466538a131121 Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Fri, 21 Aug 2026 20:14:02 +0200 Subject: [PATCH] CAMEL-24411: camel-oauth - stop the route when the processors do not authenticate the request OAuthBearerTokenProcessor and OAuthCodeFlowProcessor both returned normally from process() on the paths where they do not authenticate the caller, so the remaining steps of the route still ran and overwrote the response the processor had just prepared. The component's own test routes have the shape .process(new OAuthBearerTokenProcessor()).setBody(...) where that following step executes. The same shape is present in OAuthCodeFlowCallback, which answered 400 for a callback without the code parameter and then let the route continue. There was no setRouteStop, CamelAuthorizationException or RoutePolicy anywhere in camel-oauth, so nothing halted the exchange on any of these paths. This adds reject() and rejectUnauthorized() helpers to AbstractOAuthProcessor and uses them at the three denial points: * OAuthBearerTokenProcessor - a missing Authorization header, or one that does not parse as "Bearer ", now answers 401 with a WWW-Authenticate: Bearer challenge (RFC 6750) instead of 400, and stops the route. A present but invalid token keeps failing by propagating the exception from OAuth.authenticate(), as before. * OAuthCodeFlowProcessor - stops the route after redirecting an unauthenticated caller to the identity provider; the 302 is the whole response. * OAuthCodeFlowCallback - keeps answering 400 for a missing authorization code and now stops the route too. sendRedirect() itself is deliberately left alone, and OAuthLogoutProcessor is unchanged: the shipped logout route relies on the step after the redirect running, so stopping the route inside sendRedirect would break an intended flow. Only the denial paths stop; authenticated requests continue through the rest of the route exactly as before. Adds OAuthProcessorFailClosedTest covering the three paths that return before an identity provider is contacted, a test-scoped assertj-core the module was missing, and a 4.23 upgrade-guide entry for the status-code and route-stop change. Co-Authored-By: Claude Opus 5 (1M context) --- components/camel-oauth/pom.xml | 5 ++ .../camel/oauth/AbstractOAuthProcessor.java | 27 +++++++ .../oauth/OAuthBearerTokenProcessor.java | 8 +-- .../camel/oauth/OAuthCodeFlowCallback.java | 3 +- .../camel/oauth/OAuthCodeFlowProcessor.java | 3 + .../oauth/OAuthProcessorFailClosedTest.java | 70 +++++++++++++++++++ .../pages/camel-4x-upgrade-guide-4_23.adoc | 22 ++++++ 7 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthProcessorFailClosedTest.java 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.