Skip to content

Commit

Permalink
fix: preemptively send basic auth (#127)
Browse files Browse the repository at this point in the history
* fix: preemptively send basic auth

When Elasticsearch is configured to allow anonymous auth, it does not include
a challenge header in response to requests, so we cannot rely on the apache
http client's auth handling (which is overkill anyway, since an individual
Elasticsearch client is not connected to multiple realms).

Instead, we build the http basic auth header ourselves, and register an
interceptor to ensure it is added to all requests, similar to how we handle
api key auth headers.

* Update CHANGELOG.md

Co-authored-by: Mashhur <99575341+mashhurs@users.noreply.github.com>

---------

Co-authored-by: Mashhur <99575341+mashhurs@users.noreply.github.com>
  • Loading branch information
yaauie and mashhurs authored Feb 8, 2024
1 parent cfe8dfa commit 933511b
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 17 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 0.1.6
- Fixes issue where configured `username`/`password` credentials was not sent to Elasticsearch instances that had anonymous access enabled [#127](https://github.com/elastic/logstash-filter-elastic_integration/pull/127)

## 0.1.5
- Adds relevant information to Elasticsearch client's User-Agent header [#117](https://github.com/elastic/logstash-filter-elastic_integration/pull/117)

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.5
0.1.6
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
import co.elastic.logstash.api.Password;
import co.elastic.logstash.filters.elasticintegration.util.Exceptions;
import co.elastic.logstash.filters.elasticintegration.util.KeyStoreUtil;
import org.apache.http.*;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.Header;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.TrustAllStrategy;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HttpContext;
Expand Down Expand Up @@ -251,9 +251,9 @@ public void process(HttpRequest request, HttpContext context) throws HttpExcepti
}
}

static class ApiKeyHttpRequestInterceptor extends HeaderInterceptor {
ApiKeyHttpRequestInterceptor(Header h) {
super(h);
static class AuthorizationHeaderHttpRequestInterceptor extends HeaderInterceptor {
AuthorizationHeaderHttpRequestInterceptor(final String headerValue) {
super(new BasicHeader("Authorization", headerValue));
}
}

Expand Down Expand Up @@ -391,13 +391,12 @@ public RequestAuthConfig setBasicAuth(final String username, final Password pass
Objects.requireNonNull(username, "username");
Objects.requireNonNull(password, "password");

final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password.getPassword()));
assert username.indexOf(58) == -1 : "username cannot contain colon (`:`)";

return this.setHttpClientConfigurator((httpAsyncClientBuilder -> {
httpAsyncClientBuilder.disableAuthCaching();
httpAsyncClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
}));
final String encodedCredentials = Base64.getEncoder().encodeToString(String.format("%s:%s", username, password.getPassword()).getBytes(StandardCharsets.UTF_8));

final HttpRequestInterceptor interceptor = new AuthorizationHeaderHttpRequestInterceptor(String.format("Basic %s", encodedCredentials));
return this.setHttpClientConfigurator(HttpClientConfigurator.forAddInterceptorFirst(interceptor));
}

public RequestAuthConfig setApiKey(final Password apiKey) {
Expand All @@ -411,8 +410,7 @@ public RequestAuthConfig setApiKey(final Password apiKey) {
encodedApiKey = apiKey.getPassword();
}

final Header authorizationHeader = new BasicHeader("Authorization", String.format("ApiKey %s", encodedApiKey));
final HttpRequestInterceptor interceptor = new ApiKeyHttpRequestInterceptor(authorizationHeader);
final HttpRequestInterceptor interceptor = new AuthorizationHeaderHttpRequestInterceptor(String.format("ApiKey %s", encodedApiKey));
return this.setHttpClientConfigurator(HttpClientConfigurator.forAddInterceptorFirst(interceptor));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,32 @@ static class UserAgent {
}
}

static class RestClientWithBasicAuth {
@RegisterExtension
static WireMockExtension wireMock = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort()).build();

@Test void testPreemtiveBasicAuth() throws Exception {
final URL wiremockElasticsearch = new URL("http", "127.0.0.1", wireMock.getRuntimeInfo().getHttpPort(),"/");

final String username = "a_user";
final String password = "$3cUr3";
final String encodedBasic = "Basic YV91c2VyOiQzY1VyMw==";

wireMock.stubFor(get("/")
.withHeader("Authorization", equalTo(encodedBasic))
.willReturn(okJson(getMockResponseBody("get-root.json"))));

try (RestClient restClient = ElasticsearchRestClientBuilder
.forURLs(Collections.singleton(wiremockElasticsearch))
.configureRequestAuth(c -> c.setBasicAuth(username, new Password(password)))
.build()) {
final Response response = restClient.performRequest(new Request("GET", "/"));
assertThat(response.getStatusLine().getStatusCode(), is(Matchers.equalTo(200)));
}
}
}

static class RestClientWithApiKey {

@RegisterExtension
Expand Down

0 comments on commit 933511b

Please sign in to comment.