Skip to content

Commit

Permalink
Tests related to brute force attack prevention. (#2245)
Browse files Browse the repository at this point in the history
Tests related to brute force attack prevention.

Signed-off-by: Lukasz Soszynski <[email protected]>
  • Loading branch information
lukasz-soszynski-eliatra authored Nov 21, 2022
1 parent dc7d21c commit 25ea092
Show file tree
Hide file tree
Showing 15 changed files with 716 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
*/
package org.opensearch.security;

import java.util.concurrent.TimeUnit;

import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.opensearch.test.framework.AuthFailureListeners;
import org.opensearch.test.framework.RateLimiting;
import org.opensearch.test.framework.TestSecurityConfig.User;
import org.opensearch.test.framework.cluster.ClusterManager;
import org.opensearch.test.framework.cluster.LocalCluster;
import org.opensearch.test.framework.cluster.TestRestClient;
import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse;
import org.opensearch.test.framework.cluster.TestRestClientConfiguration;
import org.opensearch.test.framework.log.LogsRule;

import static org.apache.hc.core5.http.HttpStatus.SC_OK;
import static org.apache.hc.core5.http.HttpStatus.SC_UNAUTHORIZED;
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE;
import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS;
import static org.opensearch.test.framework.cluster.TestRestClientConfiguration.userWithSourceIp;

@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class)
@ThreadLeakScope(ThreadLeakScope.Scope.NONE)
public class IpBruteForceAttacksPreventionTests {
private static final User USER_1 = new User("simple-user-1").roles(ALL_ACCESS);
private static final User USER_2 = new User("simple-user-2").roles(ALL_ACCESS);

public static final int ALLOWED_TRIES = 3;
public static final int TIME_WINDOW_SECONDS = 3;

public static final String CLIENT_IP_2 = "127.0.0.2";
public static final String CLIENT_IP_3 = "127.0.0.3";
public static final String CLIENT_IP_4 = "127.0.0.4";
public static final String CLIENT_IP_5 = "127.0.0.5";
public static final String CLIENT_IP_6 = "127.0.0.6";
public static final String CLIENT_IP_7 = "127.0.0.7";
public static final String CLIENT_IP_8 = "127.0.0.8";
public static final String CLIENT_IP_9 = "127.0.0.9";

private static final AuthFailureListeners listener = new AuthFailureListeners()
.addRateLimit(new RateLimiting("internal_authentication_backend_limiting").type("ip")
.allowedTries(ALLOWED_TRIES).timeWindowSeconds(TIME_WINDOW_SECONDS).blockExpirySeconds(2).maxBlockedClients(500)
.maxTrackedClients(500));

@ClassRule
public static final LocalCluster cluster = new LocalCluster.Builder()
.clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false).authFailureListeners(listener)
.authc(AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE).users(USER_1, USER_2).build();

@Rule
public LogsRule logsRule = new LogsRule("org.opensearch.security.auth.BackendRegistry");

@Test
public void shouldAuthenticateUserWhenBlockadeIsNotActive() {
try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_2))) {

HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_OK);
}
}

@Test
public void shouldBlockIpAddress() {
authenticateUserWithIncorrectPassword(CLIENT_IP_3, USER_2, ALLOWED_TRIES);
try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_2, CLIENT_IP_3))) {

HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_UNAUTHORIZED);
logsRule.assertThatContain("Rejecting REST request because of blocked address: /" + CLIENT_IP_3);
}
}

@Test
public void shouldBlockUsersWhoUseTheSameIpAddress() {
authenticateUserWithIncorrectPassword(CLIENT_IP_4, USER_1, ALLOWED_TRIES);
try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_2, CLIENT_IP_4))) {

HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_UNAUTHORIZED);
logsRule.assertThatContain("Rejecting REST request because of blocked address: /" + CLIENT_IP_4);
}
}

@Test
public void testUserShouldBeAbleToAuthenticateFromAnotherNotBlockedIpAddress() {
authenticateUserWithIncorrectPassword(CLIENT_IP_5, USER_1, ALLOWED_TRIES);
try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_6))) {
HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_OK);
}
}

@Test
public void shouldNotBlockIpWhenFailureAuthenticationCountIsLessThanAllowedTries() {
authenticateUserWithIncorrectPassword(CLIENT_IP_7, USER_1, ALLOWED_TRIES - 1);
try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_7))) {

HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_OK);
}
}

@Test
public void shouldBlockIpWhenFailureAuthenticationCountIsGraterThanAllowedTries() {
authenticateUserWithIncorrectPassword(CLIENT_IP_8, USER_1, ALLOWED_TRIES * 2);
try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_8))) {

HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_UNAUTHORIZED);
logsRule.assertThatContain("Rejecting REST request because of blocked address: /" + CLIENT_IP_8);
}
}

@Test
public void shouldReleaseIpAddressLock() throws InterruptedException {
authenticateUserWithIncorrectPassword(CLIENT_IP_9, USER_1, ALLOWED_TRIES * 2);
TimeUnit.SECONDS.sleep(TIME_WINDOW_SECONDS);
try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_9))) {

HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_OK);
logsRule.assertThatContain("Rejecting REST request because of blocked address: /" + CLIENT_IP_9);
}
}

private static void authenticateUserWithIncorrectPassword(String sourceIpAddress, User user, int numberOfRequests) {
var clientConfiguration = new TestRestClientConfiguration().username(user.getName())
.password("incorrect password").sourceInetAddress(sourceIpAddress);
try(TestRestClient client = cluster.createGenericClientRestClient(clientConfiguration)) {
for(int i = 0; i < numberOfRequests; ++i) {
HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_UNAUTHORIZED);
}
}
}
}
26 changes: 20 additions & 6 deletions src/integrationTest/java/org/opensearch/security/TlsTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,29 @@
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.NoHttpResponseException;
import org.apache.hc.core5.http.message.BasicHeader;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.opensearch.security.auditlog.impl.AuditCategory;
import org.opensearch.test.framework.AuditCompliance;
import org.opensearch.test.framework.AuditConfiguration;
import org.opensearch.test.framework.AuditFilters;
import org.opensearch.test.framework.TestSecurityConfig.User;
import org.opensearch.test.framework.audit.AuditLogsRule;
import org.opensearch.test.framework.cluster.ClusterManager;
import org.opensearch.test.framework.cluster.LocalCluster;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.opensearch.security.auditlog.AuditLog.Origin.REST;
import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_CIPHERS;
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL;
import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS;
import static org.opensearch.test.framework.audit.AuditMessagePredicate.auditPredicate;
import static org.opensearch.test.framework.cluster.TestRestClientConfiguration.getBasicAuthHeader;
import static org.opensearch.test.framework.matcher.ExceptionMatcherAssert.assertThatThrownBy;

@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class)
Expand All @@ -49,10 +57,17 @@ public class TlsTests {
public static final String AUTH_INFO_ENDPOINT = "/_opendistro/_security/authinfo?pretty";

@ClassRule
public static LocalCluster cluster = new LocalCluster.Builder()
public static final LocalCluster cluster = new LocalCluster.Builder()
.clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS).anonymousAuth(false)
.nodeSettings(Map.of(SECURITY_SSL_HTTP_ENABLED_CIPHERS, List.of(SUPPORTED_CIPHER_SUIT)))
.authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_ADMIN).build();
.authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_ADMIN)
.audit(new AuditConfiguration(true)
.compliance(new AuditCompliance().enabled(true))
.filters(new AuditFilters().enabledRest(true).enabledTransport(true))
).build();

@Rule
public AuditLogsRule auditLogsRule = new AuditLogsRule();

@Test
public void shouldCreateAuditOnIncomingNonTlsConnection() throws IOException {
Expand All @@ -61,15 +76,14 @@ public void shouldCreateAuditOnIncomingNonTlsConnection() throws IOException {

assertThatThrownBy(() -> httpClient.execute(request), instanceOf(NoHttpResponseException.class));
}
//TODO check if audit is created, audit_category = SSL_EXCEPTION
auditLogsRule.assertAtLeast(1, auditPredicate(AuditCategory.SSL_EXCEPTION).withLayer(REST));
}

@Test
public void shouldSupportClientCipherSuite_positive() throws IOException {
try(CloseableHttpClient client = cluster.getClosableHttpClient(new String[] { SUPPORTED_CIPHER_SUIT })) {
HttpGet httpGet = new HttpGet("https://localhost:" + cluster.getHttpPort() + AUTH_INFO_ENDPOINT);
BasicHeader header = cluster.getBasicAuthHeader(USER_ADMIN.getName(), USER_ADMIN.getPassword());
httpGet.addHeader(header);
httpGet.addHeader(getBasicAuthHeader(USER_ADMIN.getName(), USER_ADMIN.getPassword()));

try(CloseableHttpResponse response = client.execute(httpGet)) {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
*/
package org.opensearch.security;

import java.util.concurrent.TimeUnit;

import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.opensearch.test.framework.AuthFailureListeners;
import org.opensearch.test.framework.RateLimiting;
import org.opensearch.test.framework.TestSecurityConfig.User;
import org.opensearch.test.framework.cluster.ClusterManager;
import org.opensearch.test.framework.cluster.LocalCluster;
import org.opensearch.test.framework.cluster.TestRestClient;
import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse;
import org.opensearch.test.framework.log.LogsRule;

import static org.apache.hc.core5.http.HttpStatus.SC_OK;
import static org.apache.hc.core5.http.HttpStatus.SC_UNAUTHORIZED;
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL;
import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS;

@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class)
@ThreadLeakScope(ThreadLeakScope.Scope.NONE)
public class UserBruteForceAttacksPreventionTests {

private static final User USER_1 = new User("simple-user-1").roles(ALL_ACCESS);
private static final User USER_2 = new User("simple-user-2").roles(ALL_ACCESS);
private static final User USER_3 = new User("simple-user-3").roles(ALL_ACCESS);
private static final User USER_4 = new User("simple-user-4").roles(ALL_ACCESS);
private static final User USER_5 = new User("simple-user-5").roles(ALL_ACCESS);

public static final int ALLOWED_TRIES = 3;
public static final int TIME_WINDOW_SECONDS = 3;
private static final AuthFailureListeners listener = new AuthFailureListeners()
.addRateLimit(new RateLimiting("internal_authentication_backend_limiting").type("username").authenticationBackend("intern")
.allowedTries(ALLOWED_TRIES).timeWindowSeconds(TIME_WINDOW_SECONDS).blockExpirySeconds(2).maxBlockedClients(500)
.maxTrackedClients(500));

@ClassRule
public static final LocalCluster cluster = new LocalCluster.Builder()
.clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false).authFailureListeners(listener)
.authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_1, USER_2, USER_3, USER_4, USER_5).build();

@Rule
public LogsRule logsRule = new LogsRule("org.opensearch.security.auth.BackendRegistry");

@Test
public void shouldAuthenticateUserWhenBlockadeIsNotActive() {
try(TestRestClient client = cluster.getRestClient(USER_1)) {

HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_OK);
}
}

@Test
public void shouldBlockUserWhenNumberOfFailureLoginAttemptIsEqualToLimit() {
authenticateUserWithIncorrectPassword(USER_2, ALLOWED_TRIES);
try(TestRestClient client = cluster.getRestClient(USER_2)) {
HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_UNAUTHORIZED);
}
//Rejecting REST request because of blocked user:
logsRule.assertThatContain("Rejecting REST request because of blocked user: " + USER_2.getName());
}

@Test
public void shouldBlockUserWhenNumberOfFailureLoginAttemptIsGraterThanLimit() {
authenticateUserWithIncorrectPassword(USER_3, ALLOWED_TRIES * 2);
try(TestRestClient client = cluster.getRestClient(USER_3)) {
HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_UNAUTHORIZED);
}
logsRule.assertThatContain("Rejecting REST request because of blocked user: " + USER_3.getName());
}

@Test
public void shouldNotBlockUserWhenNumberOfLoginAttemptIsBelowLimit() {
authenticateUserWithIncorrectPassword(USER_4, ALLOWED_TRIES - 1);
try(TestRestClient client = cluster.getRestClient(USER_4)) {
HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_OK);
}
}

@Test
public void shouldReleaseLock() throws InterruptedException {
authenticateUserWithIncorrectPassword(USER_5, ALLOWED_TRIES);
try(TestRestClient client = cluster.getRestClient(USER_5)) {
HttpResponse response = client.getAuthInfo();
response.assertStatusCode(SC_UNAUTHORIZED);
TimeUnit.SECONDS.sleep(TIME_WINDOW_SECONDS);

response = client.getAuthInfo();

response.assertStatusCode(SC_OK);
}
logsRule.assertThatContain("Rejecting REST request because of blocked user: " + USER_5.getName());
}

private static void authenticateUserWithIncorrectPassword(User user, int numberOfAttempts) {
try(TestRestClient client = cluster.getRestClient(user.getName(), "incorrect password")) {
for(int i = 0; i < numberOfAttempts; ++i) {
HttpResponse response = client.getAuthInfo();
response.assertStatusCode(SC_UNAUTHORIZED);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ public void shouldAuthenticateWithJwtToken_failureLackingUserName() {
HttpResponse response = client.getAuthInfo();

response.assertStatusCode(401);
logsRule.assertThatContain("No subject found in JWT token");
logsRule.assertThatContainExactly("No subject found in JWT token");
}
}

Expand All @@ -175,7 +175,7 @@ public void shouldAuthenticateWithJwtToken_failureExpiredToken() {
HttpResponse response = client.getAuthInfo();

response.assertStatusCode(401);
logsRule.assertThatContain("Invalid or expired JWT token.");
logsRule.assertThatContainExactly("Invalid or expired JWT token.");
}
}

Expand All @@ -187,7 +187,7 @@ public void shouldAuthenticateWithJwtToken_failureIncorrectFormatOfToken() {
HttpResponse response = client.getAuthInfo();

response.assertStatusCode(401);
logsRule.assertThatContain(String.format("No JWT token found in '%s' header header", JWT_AUTH_HEADER));
logsRule.assertThatContainExactly(String.format("No JWT token found in '%s' header header", JWT_AUTH_HEADER));
}
}

Expand All @@ -200,7 +200,7 @@ public void shouldAuthenticateWithJwtToken_failureIncorrectSignature() {
HttpResponse response = client.getAuthInfo();

response.assertStatusCode(401);
logsRule.assertThatContain("Invalid or expired JWT token.");
logsRule.assertThatContainExactly("Invalid or expired JWT token.");
}
}

Expand Down
Loading

0 comments on commit 25ea092

Please sign in to comment.