Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
1329e74
AYS-509 | `AysRateLimitFilter` Has Been Separated from `AysBearerToke…
agitrubard Nov 12, 2024
d54b48b
AYS-509 | `AysRateLimitFilter` Has Been Added to Filter Chain
agitrubard Nov 12, 2024
50839ae
AYS-509 | `AysBearerTokenAuthenticationFilterEndToEndTest` Class Has …
agitrubard Nov 12, 2024
f4cf2a7
Merge remote-tracking branch 'origin/main' into fetaure/AYS-509/logging
agitrubard Nov 25, 2024
3be337b
AYS-509 | `toUnformattedJson()` Method Has Been Added to `AysJsonUtil…
agitrubard Nov 27, 2024
63ca567
AYS-509 | `isAuthenticated()` Method Has Been Added to `AysIdentity` …
agitrubard Nov 27, 2024
7da1c93
AYS-509 | `AysDynamoDbEntity` Class Has Been Deleted Because It's Unused
agitrubard Nov 27, 2024
469febe
AYS-509 | `AysDynamoDbData` Class Has Been Deleted Because It's Unused
agitrubard Nov 27, 2024
eb20dd5
AYS-509 | `AysAuditLogEntity` Class Has Been Created
agitrubard Nov 27, 2024
69f714d
AYS-509 | `AysAuditLog` Class Has Been Created
agitrubard Nov 27, 2024
c4686e8
AYS-509 | `AysAuditLogToEntityMapper` Class Has Been Created
agitrubard Nov 27, 2024
16adbfe
AYS-509 | Audit Log Repository Interface Has Been Created
agitrubard Nov 27, 2024
39d1360
AYS-509 | Audit Log Repository Has Been Implemented for Default Profile
agitrubard Nov 27, 2024
ac806f5
AYS-509 | Audit Log Repository Has Been Implemented for Kinesis Profile
agitrubard Nov 27, 2024
11f47e4
AYS-509 | Access Modifier of `AysAuditLogRepositoryKinesisImpl` Class…
agitrubard Nov 27, 2024
950cdfd
AYS-509 | `AysAuditLogRepositoryImpl` Class Has Been Covered with Uni…
agitrubard Nov 27, 2024
af9c6d9
AYS-509 | `default` Profile Has Been Activated for All Tests
agitrubard Nov 27, 2024
de2f228
AYS-509 | `AysAuditLogEntityBuilder` Class Has Been Created
agitrubard Nov 27, 2024
0d46e64
AYS-509 | `AysAuditLogRepositoryKinesisImpl` Class Has Been Covered w…
agitrubard Nov 27, 2024
bee4547
AYS-509 | Empty Lines Have Been Fixed in `AysAuditLogRepositoryKinesi…
agitrubard Nov 27, 2024
6c6589c
AYS-509 | `@Setter` Annotation Have Been Added to `AysAuditLog` Class
agitrubard Nov 27, 2024
aef68f6
AYS-509 | Mock IDs Have Been Changed
agitrubard Nov 27, 2024
41fa1aa
AYS-509 | Audit Log Save Port Has Been Created
agitrubard Nov 27, 2024
0c94e6e
AYS-509 | Audit Log Save Adapter Has Been Created and `AysAuditLogSav…
agitrubard Nov 27, 2024
0fa8391
AYS-509 | `AysHttpHeader` Has Been Created for Mapping HTTP Headers
agitrubard Nov 27, 2024
fe5b610
AYS-509 | `AysHttpServletRequest` Has Been Created for Mapping `HttpS…
agitrubard Nov 27, 2024
8dfe89f
AYS-509 | `java:S1939` Sonar Issue Has Been Suppressed in `AysHttpSer…
agitrubard Nov 27, 2024
fa0b4bd
AYS-509 | `java:S1939` Sonar Issue Has Been Suppressed in `AysHttpSer…
agitrubard Nov 27, 2024
637e2ff
AYS-509 | "`java:S1939` Sonar Issue Has Been Suppressed in `AysHttpSe…
agitrubard Nov 27, 2024
e8e13cf
AYS-509 | "`java:S1939` Sonar Issue Has Been Suppressed in `AysHttpSe…
agitrubard Nov 27, 2024
fbace5e
AYS-509 | `AysHttpServletRequest`, `AysHttpHeader` and `AysHttpServle…
agitrubard Nov 27, 2024
ff04e36
AYS-509 | `AysHttpServletRequest`, `AysHttpHeader` and `AysHttpServle…
agitrubard Nov 27, 2024
a985a8f
AYS-509 | Unused Methods Have Been Deleted from `AysToken` Class
agitrubard Nov 27, 2024
fa15526
AYS-509 | `AysAuditLogFilter` Has Been Created and Implemented
agitrubard Nov 27, 2024
6ccea7e
AYS-509 | `AysAuditLogBuilder` Class Has Been Created
agitrubard Nov 27, 2024
199f84d
AYS-509 | `AysAuditLogFilter` and `AysRateLimitFilter` Have Been Conf…
agitrubard Nov 27, 2024
d966c17
AYS-509 | `AysAuditLogAdapter` Has Been Covered with Unit Tests
agitrubard Nov 27, 2024
a814976
AYS-509 | `AysBearerTokenAuthenticationFilter` Has Been Covered with …
agitrubard Nov 27, 2024
7018340
AYS-509 | `AysAuditLogFilter` Has Been Covered with End to End Tests
agitrubard Nov 27, 2024
c3579bf
AYS-509 | `java:S5977` Sonar Issue Has Been Fixed in `givenValidIdAnd…
agitrubard Nov 27, 2024
b17c2db
AYS-509 | `java:S116` Sonar Issue Has Been Fixed in `EmergencyEvacuat…
agitrubard Nov 27, 2024
eb3c913
AYS-509 | `java:S3415` Sonar Issue Has Been Fixed in Tests
agitrubard Nov 27, 2024
2bd7945
AYS-509 | `java:S1854` Sonar Issue Has Been Suppressed in `AysErrorRe…
agitrubard Nov 27, 2024
5350625
AYS-509 | `java:S2221` Sonar Issue Has Been Fixed in `AysPhoneNumberU…
agitrubard Nov 27, 2024
024c4d0
AYS-509 | `java:S1941` Sonar Issue Has Been Suppressed Because `role`…
agitrubard Nov 27, 2024
d7e8824
AYS-509 | `toString()` Method of `AysPhoneNumberFilterRequest` Has Be…
agitrubard Nov 27, 2024
69aa7dd
AYS-509 | `toString()` Method of `AysPhoneNumberRequest` Has Been Fix…
agitrubard Nov 27, 2024
89a7a64
AYS-509 | `toUnformattedJson()` Method Has Been Changed as `toEscaped…
agitrubard Nov 27, 2024
4fa2201
AYS-509 | `GSON` Has Been Removed from `AysJsonUtil`
agitrubard Nov 27, 2024
28fca22
Merge remote-tracking branch 'origin/main' into fetaure/AYS-509/logging
agitrubard Nov 29, 2024
70a8a62
AYS-509 | `HttpServletRequestUtil` Class Has Been Deleted Because It'…
agitrubard Dec 8, 2024
28687b4
AYS-509 | `InstitutionResponse` Class Has Been Deleted Because It's U…
agitrubard Dec 8, 2024
b79de0c
AYS-509 | `AysKinesisConfiguration` Class Has Been Refactored
agitrubard Dec 8, 2024
e8ea70a
AYS-509 | `EnumValidation` Class Has Been Deleted Because It's Unused
agitrubard Dec 8, 2024
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
14 changes: 11 additions & 3 deletions src/main/java/org/ays/auth/config/SecurityConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import lombok.RequiredArgsConstructor;
import org.ays.auth.filter.AysBearerTokenAuthenticationFilter;
import org.ays.auth.filter.AysRateLimitFilter;
import org.ays.auth.security.AysAuthenticationEntryPoint;
import org.ays.common.filter.AysAuditLogFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
Expand Down Expand Up @@ -58,15 +60,19 @@ SessionAuthenticationStrategy sessionAuthenticationStrategy() {
* Returns the {@link SecurityFilterChain} instance that defines the security configuration for HTTP requests.
*
* @param httpSecurity the {@link HttpSecurity} instance to configure
* @param auditLogFilter the {@link AysAuditLogFilter} instance to log audit information
* @param rateLimitFilter the {@link AysRateLimitFilter} instance to rate limit requests
* @param bearerTokenAuthenticationFilter the {@link AysBearerTokenAuthenticationFilter} instance to authenticate bearer tokens
* @param customAuthenticationEntryPoint the {@link AysAuthenticationEntryPoint} instance to handle authentication errors
* @return the {@link SecurityFilterChain} instance
* @throws Exception if there is an error setting up the filter chain
*/
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity,
AysBearerTokenAuthenticationFilter bearerTokenAuthenticationFilter,
AysAuthenticationEntryPoint customAuthenticationEntryPoint)
SecurityFilterChain filterChain(final HttpSecurity httpSecurity,
final AysAuditLogFilter auditLogFilter,
final AysRateLimitFilter rateLimitFilter,
final AysBearerTokenAuthenticationFilter bearerTokenAuthenticationFilter,
final AysAuthenticationEntryPoint customAuthenticationEntryPoint)
throws Exception {

httpSecurity
Expand All @@ -83,6 +89,8 @@ SecurityFilterChain filterChain(HttpSecurity httpSecurity,
.anyRequest().authenticated()
)
.sessionManagement(customizer -> customizer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(auditLogFilter, BearerTokenAuthenticationFilter.class)
.addFilterBefore(rateLimitFilter, BearerTokenAuthenticationFilter.class)
.addFilterBefore(bearerTokenAuthenticationFilter, BearerTokenAuthenticationFilter.class);

return httpSecurity.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,40 @@
package org.ays.auth.filter;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ays.auth.model.AysToken;
import org.ays.auth.service.AysInvalidTokenService;
import org.ays.auth.service.AysTokenService;
import org.ays.common.util.HttpServletRequestUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.ays.common.model.request.AysHttpHeader;
import org.ays.common.model.request.AysHttpServletRequest;
import org.ays.common.model.response.AysHttpServletResponse;
import org.springframework.core.annotation.Order;
import org.springframework.lang.NonNull;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
* AysBearerTokenAuthenticationFilter is a filter that intercepts HTTP requests and processes the Bearer tokens included in the Authorization headers.
* If the token is valid, the user is authenticated and added to the SecurityContext for the duration of the request.
* If the token is invalid, a 401 Unauthorized response is returned.
* <p>The filter uses an instance of AysTokenService to verify and validate the token and retrieve the user authentication.
* Filter responsible for handling Bearer token-based authentication in incoming HTTP requests.
* <p>
* This filter intercepts requests and checks for the presence of a Bearer token in the `Authorization` header.
* If a valid token is found:
* <ul>
* <li>The token is verified and validated using {@link AysTokenService}.</li>
* <li>The token's payload is retrieved, and its validity is checked using {@link AysInvalidTokenService}.</li>
* <li>An {@link org.springframework.security.core.Authentication} object is created and added to the {@link SecurityContextHolder} for the current request.</li>
* </ul>
* If the token is invalid, the filter does not authenticate the request, and a 401 Unauthorized response may be returned.
* <p>
* This filter ensures that only authenticated users can access secured resources in the application.
* It runs with an order of {@code 3}, which allows it to be executed after other filters with lower order values.
*/
@Slf4j
@Order(3)
@Component
@RequiredArgsConstructor
public class AysBearerTokenAuthenticationFilter extends OncePerRequestFilter {
Expand All @@ -44,141 +43,31 @@ public class AysBearerTokenAuthenticationFilter extends OncePerRequestFilter {
private final AysInvalidTokenService invalidTokenService;


@Value("${ays.rate-limit.authorized.enabled}")
private boolean isAuthorizedRateLimitEnabled;

private static final int MAXIMUM_AUTHORIZED_REQUESTS_COUNTS = 20;
private static final int MAXIMUM_AUTHORIZED_REQUESTS_DURATION_MINUTES = 1;
private static final Duration MAXIMUM_AUTHORIZED_REQUESTS_DURATION = Duration.ofMinutes(MAXIMUM_AUTHORIZED_REQUESTS_DURATION_MINUTES);
private final LoadingCache<String, Bucket> authorizedBuckets = CacheBuilder.newBuilder()
.expireAfterWrite(MAXIMUM_AUTHORIZED_REQUESTS_DURATION_MINUTES, TimeUnit.MINUTES)
.build(new CacheLoader<>() {
@Override
public @NotNull Bucket load(@NotNull String key) {
return newBucket(
MAXIMUM_AUTHORIZED_REQUESTS_COUNTS,
MAXIMUM_AUTHORIZED_REQUESTS_DURATION
);
}
});


@Value("${ays.rate-limit.unauthorized.enabled}")
private boolean isUnauthorizedRateLimitEnabled;

private static final int MAXIMUM_UNAUTHORIZED_REQUESTS_COUNTS = 5;
private static final int MAXIMUM_UNAUTHORIZED_REQUESTS_DURATION_MINUTES = 10;
private static final Duration MAXIMUM_UNAUTHORIZED_REQUESTS_DURATION = Duration.ofMinutes(MAXIMUM_UNAUTHORIZED_REQUESTS_DURATION_MINUTES);
private final LoadingCache<String, Bucket> unauthorizedBuckets = CacheBuilder.newBuilder()
.expireAfterWrite(MAXIMUM_UNAUTHORIZED_REQUESTS_DURATION_MINUTES, TimeUnit.MINUTES)
.build(new CacheLoader<>() {
@Override
public @NotNull Bucket load(@NotNull String key) {
return newBucket(
MAXIMUM_UNAUTHORIZED_REQUESTS_COUNTS,
MAXIMUM_UNAUTHORIZED_REQUESTS_DURATION
);
}
});


private static final List<String> ALLOWED_PATHS = List
.of("/public/actuator");


@Override
protected void doFilterInternal(@NotNull HttpServletRequest httpServletRequest,
@NonNull HttpServletResponse httpServletResponse,
@NonNull FilterChain filterChain) throws ServletException, IOException {

final String authorizationHeader = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION);
final AysHttpServletRequest aysHttpServletRequest = (AysHttpServletRequest) httpServletRequest;
final AysHttpHeader aysHttpHeader = aysHttpServletRequest.getHeader();

boolean rateLimitExceeded = this.isRateLimitExceeded(
authorizationHeader,
httpServletRequest,
httpServletResponse
);
if (aysHttpHeader.hasBearerToken()) {

if (rateLimitExceeded) {
return;
}

if (AysToken.isBearerToken(authorizationHeader)) {

final String jwt = AysToken.getJwt(authorizationHeader);
final String token = aysHttpHeader.getBearerToken();

tokenService.verifyAndValidate(jwt);
tokenService.verifyAndValidate(token);

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

final var authentication = tokenService.getAuthentication(jwt);
final var authentication = tokenService.getAuthentication(token);
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);

boolean isRateLimitEnabled = isAuthorizedRateLimitEnabled || isUnauthorizedRateLimitEnabled;

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

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

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

private boolean isRateLimitExceededOnBuckets(final LoadingCache<String, Bucket> buckets,
final HttpServletRequest httpServletRequest,
final HttpServletResponse httpServletResponse) {

final String ipAddress = HttpServletRequestUtil.getClientIpAddress(httpServletRequest);

try {

final Bucket bucket = buckets.get(ipAddress);
if (bucket.tryConsume(1)) {
return false;
}

} catch (ExecutionException 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;
}

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

private static Bucket newBucket(int maximumRequestsCounts, Duration maximumDuration) {
final Bandwidth bandwidth = Bandwidth
.builder()
.capacity(maximumRequestsCounts)
.refillIntervally(maximumRequestsCounts, maximumDuration)
.build();
return Bucket.builder()
.addLimit(bandwidth)
.build();
final AysHttpServletResponse aysHttpServletResponse = (AysHttpServletResponse) httpServletResponse;
filterChain.doFilter(aysHttpServletRequest, aysHttpServletResponse);
}

}
Loading
Loading