Skip to content

Commit

Permalink
API - Fix cors bug in http interceptor
Browse files Browse the repository at this point in the history
- Resolves bug #160
- CORS related config can be set from environment variables now for flexibility
- Added logic to handle preflight requests in HTTP interceptor
- Removed OPTIONS http method from the DB table
  • Loading branch information
neel1996 committed Jul 13, 2022
1 parent 0866b74 commit cd9c36e
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import lombok.extern.log4j.Log4j2;
import org.apache.logging.log4j.Level;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
Expand All @@ -15,6 +17,7 @@
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
Expand All @@ -27,6 +30,18 @@ public class DefaultHttpInterceptor implements HandlerInterceptor {
private final GenericMockRequestController genericMockRequestController;
private final WriterCollection writerCollection;

@Value("#{'${app.security.cors-config.allowed-origins}'.split(',')}")
private List<String> corsAllowedOrigins;

@Value("#{'${app.security.cors-config.allowed-methods}'.split(',')}")
private List<String> corsAllowedMethods;

@Value("#{'${app.security.cors-config.allowed-headers}'.split(',')}")
private List<String> corsAllowedHeaders;

@Value("#{'${app.security.cors-config.exposed-headers}'.split(',')}")
private List<String> corsExposedHeaders;

@Autowired
DefaultHttpInterceptor(GenericMockRequestController genericMockRequestController, WriterCollection writerCollection) {
this.genericMockRequestController = genericMockRequestController;
Expand All @@ -39,9 +54,30 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons

log.log(Level.INFO, "Intercepting request for " + path);

if (request.getMethod().equals(HttpMethod.OPTIONS.toString())) {
log.log(Level.WARN, "Method is {}. Considering as preflight request", request.getMethod());
handleAsPreflightRequest(request, response);
return false;
}

return checkAndHandleAsMockRequest(request, response, path);
}

private String getOrigin(HttpServletRequest request) {
String requestOrigin = request.getHeader("Origin");
for (String origin : corsAllowedOrigins) {
if (origin.equals(requestOrigin)) {
return origin;
}
}
return "";
}

private void handleAsPreflightRequest(HttpServletRequest request, HttpServletResponse response) {
setPreflightHeaders(response, getOrigin(request));
response.setStatus(HttpStatus.OK.value());
}

private boolean checkAndHandleAsMockRequest(HttpServletRequest request, HttpServletResponse response, String path) {
log.log(Level.INFO, "Routing intercepted request to mock controller");
Optional<DomainModelForMock> domainModelForMock = genericMockRequestController.serveRequest(
Expand All @@ -57,25 +93,33 @@ private boolean checkAndHandleAsMockRequest(HttpServletRequest request, HttpServ

DomainModelForMock matchingMock = domainModelForMock.get();
setStatusAndContentType(response, matchingMock);
setResponseHeaders(response, matchingMock);
setResponseHeaders(request, response, matchingMock);
writeResponse(response, matchingMock);

return false;
}

private void setResponseHeaders(HttpServletResponse response, DomainModelForMock matchingMock) {
private void setResponseHeaders(HttpServletRequest request, HttpServletResponse response, DomainModelForMock matchingMock) {
log.log(Level.INFO, "Writing response headers");
if (matchingMock.getResponseHeaders() != null && !matchingMock.getResponseHeaders().isEmpty()) {
for (Map.Entry<String, Object> item : matchingMock.getResponseHeaders().entrySet()) {
String key = item.getKey();
response.setHeader(key, item.getValue().toString());
}
}
setPreflightHeaders(response, getOrigin(request));
}

private void setPreflightHeaders(HttpServletResponse response, String origin) {
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Headers", "*");
response.setHeader("Access-Control-Allow-Methods", String.join("", corsAllowedMethods));
response.setHeader("Access-Control-Expose-Headers", String.join("", corsExposedHeaders));
}

private void writeResponse(HttpServletResponse response, DomainModelForMock matchingMock) {
try {
log.log(Level.INFO, "Writing response the matching mock");
log.log(Level.INFO, "Writing response for the matching mock");
writerCollection.getWriterFor(matchingMock.getTypeOfResponse()).write(matchingMock, response);
} catch (IOException e) {
log.log(Level.ERROR, "Response writer exited with a failure : {}", e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ public class DefaultAuthConfiguration extends WebSecurityConfigurerAdapter {
@Value("#{'${app.security.cors-config.allowed-origins}'.split(',')}")
private List<String> corsAllowedOrigins;

@Value("#{'${app.security.cors-config.allowed-methods}'.split(',')}")
private List<String> corsAllowedMethods;

@Value("#{'${app.security.cors-config.allowed-headers}'.split(',')}")
private List<String> corsAllowedHeaders;

@Value("#{'${app.security.cors-config.exposed-headers}'.split(',')}")
private List<String> corsExposedHeaders;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
Expand Down Expand Up @@ -62,7 +71,7 @@ protected void configure(HttpSecurity http) throws Exception {
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

setupCSRF(http, apiPath + wildCardPath);
setupCorsConfig(http);
setupCorsConfig(http, apiPath + wildCardPath);

http.headers().frameOptions().sameOrigin();
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
Expand All @@ -75,29 +84,15 @@ private void setupCSRF(HttpSecurity http, String apiPath) throws Exception {
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
}

private void setupCorsConfig(HttpSecurity http) throws Exception {
http.cors(cors -> {
private void setupCorsConfig(HttpSecurity http, String apiPath) throws Exception {
http.antMatcher(apiPath).cors(cors -> {
CorsConfigurationSource cs = resources -> {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(corsAllowedOrigins);
corsConfiguration.setAllowedMethods(List.of("POST", "GET", "PUT", "DELETE", "OPTIONS"));
corsConfiguration.setAllowedHeaders(List.of(
"Authorization",
"Content-Type",
"X-Requested-With",
"Accept",
"X-XSRF-TOKEN"));
corsConfiguration.setAllowedMethods(corsAllowedMethods);
corsConfiguration.setAllowedHeaders(corsAllowedHeaders);
corsConfiguration.setExposedHeaders(corsExposedHeaders);
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setExposedHeaders(List.of(
"Cache-Control",
"Content-Language",
"Content-Length",
"Content-Type",
"Content-Disposition",
"Expires",
"Last-Modified",
"Pragma"
));
return corsConfiguration;
};

Expand Down
5 changes: 4 additions & 1 deletion mimock-backend/src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ spring:
jpa:
hibernate:
ddl-auto: none
show-sql: true
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
Expand Down Expand Up @@ -74,6 +74,9 @@ app:
jwt-secret-key: B4982E53863B4DA7FDA4E1236C4BAF8A # Only for development. Do not use this in production.
cors-config:
allowed-origins: http://localhost:3000,http://localhost:3001
allowed-methods: POST,GET,PUT,DELETE,OPTIONS
allowed-headers: Authorization,Content-Type,X-Requested-With,Accept,X-XSRF-TOKEN
exposed-headers: Cache-Control,Content-Language,Content-Length,Content-Type,Content-Disposition,Expires,Last-Modified,Pragma

springdoc:
swagger-ui:
Expand Down
6 changes: 5 additions & 1 deletion mimock-backend/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ app:
jwt-expiry-in-seconds: ${MIMOCK_JWT_EXPIRY_IN_SECONDS}
jwt-secret-key: ${MIMOCK_JWT_SECRET} # Only for testing. Do not use this in production.
cors-config:
allowed-origins: http://localhost:3000,http://localhost:3001
allowed-origins: ${MIMOCK_CORS_ORIGINS}
allowed-methods: ${MIMOCK_CORS_METHODS}
allowed-headers: ${MIMOCK_CORS_ALLOWED_HEADERS}
exposed-headers: ${MIMOCK_CORS_EXPOSED_HEADERS}


springdoc:
swagger-ui:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd">
<changeSet author="neel" id="0033_delete_options_from_http_methods">
<sql>
DELETE
FROM http_methods
WHERE method = 'OPTIONS';
</sql>
<rollback>
<sql>
INSERT INTO http_methods(method, created_at) VALUES ('OPTIONS', current_timestamp);
</sql>
</rollback>
</changeSet>
</databaseChangeLog>
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@
<include file="changelogs/0030_alter_mocks_add_columns_for_audit.xml" relativeToChangelogFile="true"/>
<include file="changelogs/0031_alter_mocks_add_column_query_param_value.xml" relativeToChangelogFile="true"/>
<include file="changelogs/0032_update_unique_selectors_for_mocks.xml" relativeToChangelogFile="true"/>
<include file="changelogs/0033_delete_options_from_http_methods.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class HttpMethodsRepositoryTest {
HttpMethodsRepository httpMethodsRepository;

@ParameterizedTest
@ValueSource(strings = {"GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"})
@ValueSource(strings = {"GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "TRACE", "PATCH"})
void shouldReturnHttpMethodForValidMethod(String method) {
// Act
HttpMethod httpMethod = httpMethodsRepository.findByMethod(method);
Expand All @@ -27,7 +27,7 @@ void shouldReturnHttpMethodForValidMethod(String method) {
}

@ParameterizedTest
@ValueSource(strings = {"TEST", "RANDOM", "EXEC", "123X"})
@ValueSource(strings = {"TEST", "RANDOM", "EXEC", "123X", "OPTIONS"})
void shouldReturnNullForInvalidMethod(String method) {
// Act
HttpMethod httpMethod = httpMethodsRepository.findByMethod(method);
Expand Down
4 changes: 4 additions & 0 deletions mimock-backend/src/test/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,9 @@ app:
jwt-secret-key: C4BE6B45CBBD4CBADFE5E22F4BCDBAF8 # Only for testing. Do not use this in production.
cors-config:
allowed-origins: http://localhost:3000,http://localhost:3001
allowed-methods: POST,GET,PUT,DELETE,OPTIONS
allowed-headers: Authorization,Content-Type,X-Requested-With,Accept,X-XSRF-TOKEN
exposed-headers: Cache-Control,Content-Language,Content-Length,Content-Type,Content-Disposition,Expires,Last-Modified,Pragma


flush-bin-cron-expression: "*/4 * * * * *"

0 comments on commit cd9c36e

Please sign in to comment.