Skip to content

Commit

Permalink
Tests related to brute force attack prevention.
Browse files Browse the repository at this point in the history
Signed-off-by: Lukasz Soszynski <[email protected]>
  • Loading branch information
lukasz-soszynski-eliatra committed Nov 10, 2022
1 parent 40e2e9c commit 7970da3
Show file tree
Hide file tree
Showing 12 changed files with 662 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* 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.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 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();

@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);
}
}

@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);
}
}

@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);
}
}

@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);
}
}

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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
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.Test;
import org.junit.runner.RunWith;
Expand All @@ -36,6 +35,7 @@
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.cluster.TestRestClientConfiguration.getBasicAuthHeader;
import static org.opensearch.test.framework.matcher.ExceptionMatcherAssert.assertThatThrownBy;

@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class)
Expand All @@ -49,7 +49,7 @@ 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();
Expand All @@ -68,8 +68,7 @@ public void shouldCreateAuditOnIncomingNonTlsConnection() throws IOException {
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,116 @@
/*
* 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.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 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();

@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);
}
}

@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);
}
}

@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);
}
}

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
@@ -0,0 +1,39 @@
/*
* 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.test.framework;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

import org.opensearch.common.xcontent.ToXContentObject;
import org.opensearch.common.xcontent.XContentBuilder;

public class AuthFailureListeners implements ToXContentObject {

private Map<String, RateLimiting> limits = new LinkedHashMap<>();

public AuthFailureListeners addRateLimit(RateLimiting rateLimiting) {
Objects.requireNonNull(rateLimiting, "Rate limiting is required");
limits.put(rateLimiting.getName(), rateLimiting);
return this;
}

@Override
public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException {
xContentBuilder.startObject();
for(Map.Entry<String, RateLimiting> entry : limits.entrySet()) {
xContentBuilder.field(entry.getKey(), entry.getValue());
}
xContentBuilder.endObject();
return xContentBuilder;
}
}
Loading

0 comments on commit 7970da3

Please sign in to comment.