Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions components/camel-oauth/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@
<version>${rest-assured-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 <token>`, 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.