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.