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
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,26 @@ public class DefaultAuthenticationHttpClientConfigurer implements HttpClientConf
private final String username;
private final char[] password;
private final String domain;
private final String scheme;
private final String host;
private final Integer port;
private final String bearerToken;
private final HttpCredentialsHelper credentialsHelper;

public DefaultAuthenticationHttpClientConfigurer(String user, String pwd, String domain, String host, String bearerToken,
HttpCredentialsHelper credentialsHelper) {
this(user, pwd, domain, null, host, null, bearerToken, credentialsHelper);
}

DefaultAuthenticationHttpClientConfigurer(String user, String pwd, String domain, String scheme, String host,
Integer port, String bearerToken,
HttpCredentialsHelper credentialsHelper) {
this.username = user;
this.password = pwd == null ? new char[0] : pwd.toCharArray();
this.domain = domain;
this.scheme = scheme;
this.host = host;
this.port = port;
this.bearerToken = bearerToken;
this.credentialsHelper = credentialsHelper;
}
Expand Down Expand Up @@ -80,7 +90,7 @@ public void configureHttpClient(HttpClientBuilder clientBuilder) {
defaultcreds = new UsernamePasswordCredentials(username, password);
}
clientBuilder.setDefaultCredentialsProvider(credentialsHelper
.getCredentialsProvider(host, null, defaultcreds));
.getCredentialsProvider(scheme, host, port, defaultcreds));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
public class HttpComponent extends HttpCommonComponent implements RestProducerFactory, SSLContextParametersAware {

private static final Logger LOG = LoggerFactory.getLogger(HttpComponent.class);
private static final String TARGET_URI_PARAMETER = HttpComponent.class.getName() + ".targetUri";

@Metadata(label = "advanced",
description = "To use the custom HttpClientConfigurer to perform configuration of the HttpClient that will be used.")
Expand Down Expand Up @@ -247,6 +248,12 @@ public HttpComponent() {
* @throws Exception is thrown if error creating configurer
*/
protected HttpClientConfigurer createHttpClientConfigurer(Map<String, Object> parameters, boolean secure) throws Exception {
URI targetUri = (URI) parameters.remove(TARGET_URI_PARAMETER);
return createHttpClientConfigurer(parameters, secure, targetUri);
}

private HttpClientConfigurer createHttpClientConfigurer(Map<String, Object> parameters, boolean secure, URI targetUri)
throws Exception {
// prefer to use endpoint configured over component configured
HttpClientConfigurer configurer
= resolveAndRemoveReferenceParameter(parameters, "httpClientConfigurer", HttpClientConfigurer.class);
Expand All @@ -255,15 +262,15 @@ protected HttpClientConfigurer createHttpClientConfigurer(Map<String, Object> pa
configurer = getHttpClientConfigurer();
}
HttpCredentialsHelper credentialsProvider = new HttpCredentialsHelper();
configurer = configureBasicAuthentication(parameters, configurer, credentialsProvider);
configurer = configureBasicAuthentication(parameters, configurer, credentialsProvider, targetUri);
configurer = configureHttpProxy(parameters, configurer, secure, credentialsProvider);
configurer = configureOAuth2Authentication(parameters, configurer);
configurer = configureOAuth2Authentication(parameters, configurer, targetUri);

return configurer;
}

private HttpClientConfigurer configureOAuth2Authentication(
Map<String, Object> parameters, HttpClientConfigurer configurer) {
Map<String, Object> parameters, HttpClientConfigurer configurer, URI targetUri) {

String clientId = getParameter(parameters, "oauth2ClientId", String.class);
String clientSecret = getParameter(parameters, "oauth2ClientSecret", String.class);
Expand Down Expand Up @@ -302,14 +309,15 @@ private HttpClientConfigurer configureOAuth2Authentication(
cacheTokens,
cachedTokensDefaultExpirySeconds,
cachedTokensExpirationMarginSeconds,
useBodyAuthentication));
useBodyAuthentication,
targetUri));
}
return configurer;
}

private HttpClientConfigurer configureBasicAuthentication(
Map<String, Object> parameters, HttpClientConfigurer configurer,
HttpCredentialsHelper credentialsProvider) {
HttpCredentialsHelper credentialsProvider, URI targetUri) {
String authUsername = getParameter(parameters, "authUsername", String.class);
String authPassword = getParameter(parameters, "authPassword", String.class);

Expand All @@ -319,22 +327,56 @@ private HttpClientConfigurer configureBasicAuthentication(

return CompositeHttpConfigurer.combineConfigurers(configurer,
new DefaultAuthenticationHttpClientConfigurer(
authUsername, authPassword, authDomain, authHost, null, credentialsProvider));
authUsername, authPassword, authDomain, authScopeScheme(authHost, targetUri),
authScopeHost(authHost, targetUri), authScopePort(authHost, targetUri), null,
credentialsProvider));
} else if (this.httpConfiguration != null) {
if ("basic".equalsIgnoreCase(this.httpConfiguration.getAuthMethod())
|| "bearer".equalsIgnoreCase(this.httpConfiguration.getAuthMethod())) {
return CompositeHttpConfigurer.combineConfigurers(configurer,
new DefaultAuthenticationHttpClientConfigurer(
this.httpConfiguration.getAuthUsername(),
this.httpConfiguration.getAuthPassword(), this.httpConfiguration.getAuthDomain(),
this.httpConfiguration.getAuthHost(), this.httpConfiguration.getAuthBearerToken(),
authScopeScheme(this.httpConfiguration.getAuthHost(), targetUri),
authScopeHost(this.httpConfiguration.getAuthHost(), targetUri),
authScopePort(this.httpConfiguration.getAuthHost(), targetUri),
this.httpConfiguration.getAuthBearerToken(),
credentialsProvider));
}
}

return configurer;
}

/**
* The host the credentials are scoped to.
* <p>
* {@code authHost} is optional and is unset in the common basic-auth configuration, which made the scope
* {@code new AuthScope(null, -1)} - matching any host, any port, any scheme. HttpClient then offers the credentials
* to whichever host issues a 401 challenge, so with {@code followRedirects=true} a redirect chosen by the remote
* server could collect them. Fall back to the authority the endpoint actually addresses.
*/
private static String authScopeHost(String authHost, URI targetUri) {
if (authHost != null) {
return authHost;
}
return targetUri != null ? targetUri.getHost() : null;
}

private static String authScopeScheme(String authHost, URI targetUri) {
return authHost == null && targetUri != null ? targetUri.getScheme() : null;
}

private static Integer authScopePort(String authHost, URI targetUri) {
if (authHost != null || targetUri == null) {
return null;
}
if (targetUri.getPort() >= 0) {
return targetUri.getPort();
}
return "https".equalsIgnoreCase(targetUri.getScheme()) ? 443 : 80;
}

private HttpClientConfigurer configureHttpProxy(
Map<String, Object> parameters, HttpClientConfigurer configurer, boolean secure,
HttpCredentialsHelper credentialsProvider) {
Expand Down Expand Up @@ -450,8 +492,14 @@ protected Endpoint createEndpoint(String uri, String remaining, Map<String, Obje
// uri part should be without protocol as that was how this component was originally created
uri = org.apache.camel.component.http.HttpUtil.removeHttpOrHttpsProtocol(uri);

// create the configurer to use for this endpoint
HttpClientConfigurer configurer = createHttpClientConfigurer(parameters, secure);
// Keep dispatching through the existing protected method so subclasses overriding it continue to be invoked.
HttpClientConfigurer configurer;
parameters.put(TARGET_URI_PARAMETER, uriHttpUriAddress);
try {
configurer = createHttpClientConfigurer(parameters, secure);
} finally {
parameters.remove(TARGET_URI_PARAMETER);
}
URI endpointUri = URISupport.createRemainingURI(uriHttpUriAddress, httpClientParameters);

endpointUri = URISupport.createRemainingURI(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,17 @@ public final class HttpCredentialsHelper {

public CredentialsProvider getCredentialsProvider(
String host, Integer port, Credentials credentials) {
return getCredentialsProvider(null, host, port, credentials);
}

CredentialsProvider getCredentialsProvider(
String scheme, String host, Integer port, Credentials credentials) {
this.credentialsProvider.setCredentials(new AuthScope(
scheme,
host,
Objects.requireNonNullElse(port, -1)), credentials);
Objects.requireNonNullElse(port, -1),
null,
null), credentials);
return credentialsProvider;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,13 @@
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OAuth2ClientConfigurer extends ServiceSupport implements HttpClientConfigurer {

private static final Logger LOG = LoggerFactory.getLogger(OAuth2ClientConfigurer.class);

private final String clientId;
private final String clientSecret;
private final String tokenEndpoint;
Expand All @@ -54,12 +58,27 @@ public class OAuth2ClientConfigurer extends ServiceSupport implements HttpClient
private final static ConcurrentMap<OAuth2URIAndCredentials, TokenCache> tokenCache = new ConcurrentHashMap<>();
private final boolean useBodyAuthentication;
private final String resourceIndicator;
private final URI targetUri;
private HttpClient httpClient;

public OAuth2ClientConfigurer(String clientId, String clientSecret, String tokenEndpoint, String resourceIndicator,
String scope, boolean cacheTokens,
long cachedTokensDefaultExpirySeconds, long cachedTokensExpirationMarginSeconds,
boolean useBodyAuthentication) {
this(clientId, clientSecret, tokenEndpoint, resourceIndicator, scope, cacheTokens,
cachedTokensDefaultExpirySeconds, cachedTokensExpirationMarginSeconds, useBodyAuthentication, null);
}

/**
* @param targetUri the URI the endpoint addresses. The bearer token is only attached to requests for the same
* authority, so that a redirect chosen by the remote server cannot collect it. Null keeps the
* previous behaviour of attaching it to whatever authority the request names.
*/
OAuth2ClientConfigurer(String clientId, String clientSecret, String tokenEndpoint, String resourceIndicator,
String scope, boolean cacheTokens,
long cachedTokensDefaultExpirySeconds, long cachedTokensExpirationMarginSeconds,
boolean useBodyAuthentication, URI targetUri) {
this.targetUri = targetUri;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.tokenEndpoint = tokenEndpoint;
Expand All @@ -78,7 +97,16 @@ public void configureHttpClient(HttpClientBuilder clientBuilder) {

clientBuilder.addRequestInterceptorFirst((HttpRequest request, EntityDetails entity, HttpContext context) -> {
URI requestUri = getUriFromRequest(request);
OAuth2URIAndCredentials uriAndCredentials = new OAuth2URIAndCredentials(requestUri, clientId, clientSecret);
if (!isTargetAuthority(requestUri)) {
// HttpClient runs protocol-level request interceptors inside ProtocolExec, which sits below
// RedirectExec, so this runs again for every redirect hop. Without this check the bearer token is
// re-attached to whichever authority the Location header named.
LOG.debug("Not attaching the OAuth2 bearer token to {}, which is not the endpoint's authority {}",
requestUri, targetUri);
return;
}
OAuth2URIAndCredentials uriAndCredentials = new OAuth2URIAndCredentials(
requestUri, clientId, clientSecret, tokenEndpoint, scope, resourceIndicator);
if (cacheTokens) {
if (tokenCache.containsKey(uriAndCredentials)
&& !tokenCache.get(uriAndCredentials).isExpiredWithMargin(cachedTokensExpirationMarginSeconds)) {
Expand All @@ -102,6 +130,32 @@ public void configureHttpClient(HttpClientBuilder clientBuilder) {
});
}

private boolean isTargetAuthority(URI requestUri) {
if (targetUri == null) {
return true;
}
if (targetUri.getScheme() == null || targetUri.getHost() == null
|| requestUri == null || requestUri.getScheme() == null || requestUri.getHost() == null) {
return false;
}
return targetUri.getScheme().equalsIgnoreCase(requestUri.getScheme())
&& targetUri.getHost().equalsIgnoreCase(requestUri.getHost())
&& effectivePort(targetUri) == effectivePort(requestUri);
}

private static int effectivePort(URI uri) {
if (uri.getPort() >= 0) {
return uri.getPort();
}
if ("http".equalsIgnoreCase(uri.getScheme())) {
return 80;
}
if ("https".equalsIgnoreCase(uri.getScheme())) {
return 443;
}
return -1;
}

private JsonObject getAccessTokenResponse(HttpClient httpClient) throws IOException {
String bodyStr = "grant_type=client_credentials";
if (scope != null) {
Expand Down Expand Up @@ -177,7 +231,16 @@ public String getToken() {
}
}

private record OAuth2URIAndCredentials(URI uri, String clientId, String clientSecret) {
/**
* Cache key for a minted token.
* <p>
* Every field that shapes the token request has to be part of it. The map is static, so it is shared by every
* configurer instance and every CamelContext in the JVM; a key that left out the scope, the token endpoint or the
* resource indicator would let a route configured for a narrow scope be served a broad-scope token that another
* route cached first, which defeats the scoping the operator asked for and makes the audit trail misleading.
*/
private record OAuth2URIAndCredentials(URI uri, String clientId, String clientSecret, String tokenEndpoint,
String scope, String resourceIndicator) {
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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.http;

import java.util.Map;

import org.apache.camel.test.junit6.CamelTestSupport;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class HttpClientConfigurerOverrideTest extends CamelTestSupport {

@Test
public void existingTwoArgumentOverrideIsStillInvoked() {
TrackingHttpComponent component = new TrackingHttpComponent();
context.addComponent("http-tracking", component);

assertThat(context.getEndpoint("http-tracking://localhost:8080")).isNotNull();
assertThat(component.invoked).isTrue();
}

private static final class TrackingHttpComponent extends HttpComponent {

private boolean invoked;

@Override
protected HttpClientConfigurer createHttpClientConfigurer(Map<String, Object> parameters, boolean secure)
throws Exception {
invoked = true;
return super.createHttpClientConfigurer(parameters, secure);
}
}
}
Loading