Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AYS-379 | Rate Limit Filter Has Been Improved and Optimized #382

Merged
merged 3 commits into from
Sep 30, 2024
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 @@ -91,51 +91,62 @@ protected void doFilterInternal(@NotNull HttpServletRequest httpServletRequest,
@NonNull HttpServletResponse httpServletResponse,
@NonNull FilterChain filterChain) throws ServletException, IOException {

final String ipAddress = HttpServletRequestUtil.getClientIpAddress(httpServletRequest);

final String authorizationHeader = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION);

boolean rateLimitExceeded = this.isRateLimitExceeded(
authorizationHeader,
httpServletRequest,
httpServletResponse
);

if (rateLimitExceeded) {
return;
}

if (AysToken.isBearerToken(authorizationHeader)) {

final String jwt = AysToken.getJwt(authorizationHeader);

tokenService.verifyAndValidate(jwt);

final String tokenId = tokenService.getPayload(jwt).getId();
invalidTokenService.checkForInvalidityOfToken(tokenId);

if (isAuthorizedRateLimitEnabled) {
boolean isRateLimitExceeded = this.isRateLimitExceeded(ipAddress, authorizedBuckets, httpServletResponse);
if (isRateLimitExceeded) {
return;
}
}

final var authentication = tokenService.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}

filterChain.doFilter(httpServletRequest, httpServletResponse);
}

private boolean isRateLimitExceeded(final String authorizationHeader,
final HttpServletRequest httpServletRequest,
final HttpServletResponse httpServletResponse) {

final String endpoint = httpServletRequest.getRequestURI();
boolean isAllowedPath = ALLOWED_PATHS.stream()
.anyMatch(endpoint::startsWith);

if (this.isNotAllowedPath(httpServletRequest) && isUnauthorizedRateLimitEnabled) {
boolean isRateLimitExceeded = this.isRateLimitExceeded(ipAddress, unauthorizedBuckets, httpServletResponse);
if (isRateLimitExceeded) {
return;
}
boolean isRateLimitEnabled = isAuthorizedRateLimitEnabled || isUnauthorizedRateLimitEnabled;

if (isAllowedPath || !isRateLimitEnabled) {
return false;
}

filterChain.doFilter(httpServletRequest, httpServletResponse);
if (AysToken.isBearerToken(authorizationHeader)) {
return this.isRateLimitExceededOnBuckets(authorizedBuckets, httpServletRequest, httpServletResponse);
}

return this.isRateLimitExceededOnBuckets(unauthorizedBuckets, httpServletRequest, httpServletResponse);
}

private boolean isNotAllowedPath(final HttpServletRequest httpServletRequest) {
final String requestURI = httpServletRequest.getRequestURI();
return ALLOWED_PATHS.stream()
.noneMatch(requestURI::startsWith);
}
private boolean isRateLimitExceededOnBuckets(final LoadingCache<String, Bucket> buckets,
final HttpServletRequest httpServletRequest,
final HttpServletResponse httpServletResponse) {

private boolean isRateLimitExceeded(final String ipAddress,
final LoadingCache<String, Bucket> buckets,
final HttpServletResponse httpServletResponse) {
final String ipAddress = HttpServletRequestUtil.getClientIpAddress(httpServletRequest);

try {

Expand All @@ -145,12 +156,16 @@ private boolean isRateLimitExceeded(final String ipAddress,
}

} catch (ExecutionException exception) {
log.error("Error while checking rate limit for IP: {}", ipAddress, exception);
final String method = httpServletRequest.getMethod();
final String endpoint = httpServletRequest.getRequestURI();
log.error("Error while checking rate limit by {} to {} - {}", ipAddress, method, endpoint);
httpServletResponse.setStatus(429);
return true;
}

log.warn("Rate limit exceeded for IP: {}", ipAddress);
final String method = httpServletRequest.getMethod();
final String endpoint = httpServletRequest.getRequestURI();
log.warn("Rate limit exceeded by {} to {} - {}", ipAddress, method, endpoint);
httpServletResponse.setStatus(429);
return true;
}
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package org.ays.auth.filter;

import org.ays.AysEndToEndTest;
import org.ays.auth.model.enums.AysSourcePage;
import org.ays.auth.model.request.AysLoginRequest;
import org.ays.auth.model.request.AysLoginRequestBuilder;
import org.ays.auth.model.response.AysTokenResponse;
import org.ays.auth.model.response.AysTokenResponseBuilder;
import org.ays.auth.model.response.AysUserResponse;
import org.ays.common.model.response.AysResponse;
import org.ays.common.model.response.AysResponseBuilder;
import org.ays.util.AysMockMvcRequestBuilders;
import org.ays.util.AysMockResultMatchersBuilders;
import org.ays.util.AysValidTestData;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

class AysBearerTokenAuthenticationFilterEndToEndTest extends AysEndToEndTest {

@Autowired
private MockMvc mockMvc;

@DynamicPropertySource
static void overrideProperties(DynamicPropertyRegistry registry) {
registry.add("ays.rate-limit.authorized.enabled", () -> "true");
registry.add("ays.rate-limit.unauthorized.enabled", () -> "true");
}


@Test
void givenValidRequest_whenRateLimitNotExceededInHealthCheckEndpoint_thenReturnSuccess() throws Exception {

// Then
String endpoint = "/public/actuator/info";
MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders
.get(endpoint);

for (int requestCount = 1; requestCount <= 10; requestCount++) {

mockMvc.perform(mockHttpServletRequestBuilder)
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status()
.isOk());
}

}

@Test
void givenValidRequest_whenRateLimitExceededInPublicEndpoint_thenReturnTooManyRequestsError() throws Exception {

// Given
AysLoginRequest loginRequest = new AysLoginRequestBuilder()
.withEmailAddress(AysValidTestData.SuperAdmin.EMAIL_ADDRESS)
.withPassword(AysValidTestData.PASSWORD)
.withSourcePage(AysSourcePage.INSTITUTION)
.build();

// Then
String endpoint = "/api/v1/authentication/token";
MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders
.post(endpoint, loginRequest);

for (int requestCount = 1; requestCount <= 10; requestCount++) {

if (requestCount > 5) {

mockMvc.perform(mockHttpServletRequestBuilder)
.andExpect(AysMockResultMatchersBuilders.status()
.isTooManyRequests());

continue;
}

AysResponse<AysTokenResponse> mockResponse = AysResponseBuilder
.successOf(new AysTokenResponseBuilder().build());
aysMockMvc.perform(mockHttpServletRequestBuilder, mockResponse)
.andExpect(AysMockResultMatchersBuilders.status()
.isOk())
.andExpect(AysMockResultMatchersBuilders.response()
.isNotEmpty())
.andExpect(MockMvcResultMatchers.jsonPath("$.response.accessToken")
.isNotEmpty())
.andExpect(MockMvcResultMatchers.jsonPath("$.response.accessTokenExpiresAt")
.isNotEmpty())
.andExpect(MockMvcResultMatchers.jsonPath("$.response.refreshToken")
.isNotEmpty());
}

}

@Test
void givenValidRequest_whenRateLimitExceededInPrivateEndpoint_thenReturnTooManyRequestsError() throws Exception {

// Given
String userId = AysValidTestData.SuperAdmin.ID;

// Then
String endpoint = "/api/v1/user/".concat(userId);
MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders
.get(endpoint, superAdminToken.getAccessToken());

for (int requestCount = 1; requestCount <= 25; requestCount++) {

if (requestCount > 20) {

mockMvc.perform(mockHttpServletRequestBuilder)
.andExpect(AysMockResultMatchersBuilders.status()
.isTooManyRequests());

continue;
}

AysResponse<AysUserResponse> mockResponse = AysResponse.successOf(new AysUserResponse());

aysMockMvc.perform(mockHttpServletRequestBuilder, mockResponse)
.andExpect(AysMockResultMatchersBuilders.status()
.isOk())
.andExpect(AysMockResultMatchersBuilders.response()
.isNotEmpty())
.andExpect(AysMockResultMatchersBuilders.response("id")
.isNotEmpty())
.andExpect(AysMockResultMatchersBuilders.response("emailAddress")
.isNotEmpty())
.andExpect(AysMockResultMatchersBuilders.response("firstName")
.isNotEmpty())
.andExpect(AysMockResultMatchersBuilders.response("lastName")
.isNotEmpty())
.andExpect(AysMockResultMatchersBuilders.response("phoneNumber.countryCode")
.isNotEmpty())
.andExpect(AysMockResultMatchersBuilders.response("phoneNumber.lineNumber")
.isNotEmpty())
.andExpect(AysMockResultMatchersBuilders.response("city")
.isNotEmpty())
.andExpect(AysMockResultMatchersBuilders.response("status")
.isNotEmpty())
.andExpect(AysMockResultMatchersBuilders.response("roles[*].id")
.isNotEmpty())
.andExpect(AysMockResultMatchersBuilders.response("roles[*].name")
.isNotEmpty())
.andExpect(AysMockResultMatchersBuilders.response("createdUser")
.isNotEmpty())
.andExpect(AysMockResultMatchersBuilders.response("createdAt")
.isNotEmpty());
}

}

}
Loading