diff --git a/src/main/java/org/ays/auth/filter/AysBearerTokenAuthenticationFilter.java b/src/main/java/org/ays/auth/filter/AysBearerTokenAuthenticationFilter.java index 08e363602..b76cfc854 100644 --- a/src/main/java/org/ays/auth/filter/AysBearerTokenAuthenticationFilter.java +++ b/src/main/java/org/ays/auth/filter/AysBearerTokenAuthenticationFilter.java @@ -91,10 +91,20 @@ 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); @@ -102,40 +112,41 @@ protected void doFilterInternal(@NotNull HttpServletRequest httpServletRequest, 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 buckets, + final HttpServletRequest httpServletRequest, + final HttpServletResponse httpServletResponse) { - private boolean isRateLimitExceeded(final String ipAddress, - final LoadingCache buckets, - final HttpServletResponse httpServletResponse) { + final String ipAddress = HttpServletRequestUtil.getClientIpAddress(httpServletRequest); try { @@ -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; } diff --git a/src/main/java/org/ays/auth/util/exception/AysUserAlreadyActiveException.java b/src/main/java/org/ays/auth/util/exception/AysUserAlreadyActiveException.java deleted file mode 100644 index 6380ecf04..000000000 --- a/src/main/java/org/ays/auth/util/exception/AysUserAlreadyActiveException.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.ays.auth.util.exception; - -import org.ays.common.util.exception.AysAlreadyException; - -import java.io.Serial; - -/** - * Exception thrown when a user is active and attempting to perform an action that requires an active user. - */ -public final class AysUserAlreadyActiveException extends AysAlreadyException { - - /** - * Unique serial version ID. - */ - @Serial - private static final long serialVersionUID = -9064818847163094755L; - - /** - * Constructs a new {@link AysUserAlreadyDeletedException} with the specified id. - * - * @param id the id of the active user - */ - public AysUserAlreadyActiveException(String id) { - super("user is already active! id:" + id); - } - -} diff --git a/src/main/java/org/ays/auth/util/exception/AysUserAlreadyPassiveException.java b/src/main/java/org/ays/auth/util/exception/AysUserAlreadyPassiveException.java deleted file mode 100644 index ee77d968b..000000000 --- a/src/main/java/org/ays/auth/util/exception/AysUserAlreadyPassiveException.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.ays.auth.util.exception; - -import org.ays.common.util.exception.AysAlreadyException; - -import java.io.Serial; - -/** - * Exception thrown when a user is passive and attempting to perform an action that requires a passive user. - */ -public final class AysUserAlreadyPassiveException extends AysAlreadyException { - - /** - * Unique serial version ID. - */ - @Serial - private static final long serialVersionUID = -3686691276790127586L; - - /** - * Constructs a new {@link AysUserAlreadyPassiveException} with the specified id. - * - * @param id the id of the passive user - */ - public AysUserAlreadyPassiveException(String id) { - super("user is already passive! id:" + id); - } - -} diff --git a/src/main/java/org/ays/auth/util/exception/AysUserLoginAttemptNotExistException.java b/src/main/java/org/ays/auth/util/exception/AysUserLoginAttemptNotExistException.java deleted file mode 100644 index 179f72034..000000000 --- a/src/main/java/org/ays/auth/util/exception/AysUserLoginAttemptNotExistException.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.ays.auth.util.exception; - -import org.ays.common.util.exception.AysNotExistException; - -import java.io.Serial; - -/** - * Exception to be thrown when a user with a given User ID does not exist. - */ -public final class AysUserLoginAttemptNotExistException extends AysNotExistException { - - /** - * Unique serial version ID. - */ - @Serial - private static final long serialVersionUID = 4835784446413089860L; - - /** - * Constructs a new AysUserNotExistByIdException with the specified user ID. - * - * @param userId the User ID of the user that does not exist - */ - public AysUserLoginAttemptNotExistException(String userId) { - super("user login attempt not exist! userId:" + userId); - } - -} diff --git a/src/test/java/org/ays/auth/filter/AysBearerTokenAuthenticationFilterEndToEndTest.java b/src/test/java/org/ays/auth/filter/AysBearerTokenAuthenticationFilterEndToEndTest.java new file mode 100644 index 000000000..cefec0ebf --- /dev/null +++ b/src/test/java/org/ays/auth/filter/AysBearerTokenAuthenticationFilterEndToEndTest.java @@ -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 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 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()); + } + + } + +}