Skip to content

Commit

Permalink
fix(FSADT1-1241): adding csrf support (#864)
Browse files Browse the repository at this point in the history
Co-authored-by: Derek Roberts <[email protected]>
Co-authored-by: Maria Martinez <[email protected]>
Co-authored-by: Maria Martinez <[email protected]>
  • Loading branch information
4 people authored Mar 14, 2024
1 parent f47ed8f commit 142b2c6
Show file tree
Hide file tree
Showing 23 changed files with 427 additions and 177 deletions.
16 changes: 4 additions & 12 deletions .github/workflows/merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,8 @@ jobs:
-p CHES_CLIENT_SECRET=${{ secrets.CHES_CLIENT_SECRET }}
-p ADDRESS_COMPLETE_KEY=${{ secrets.ADDRESS_COMPLETE_KEY }}
-p DB_PASSWORD=${{ secrets.DB_PASSWORD }}
-p COGNITO_REGION=${{ secrets.COGNITO_REGION }}
-p COGNITO_CLIENT_ID=${{ secrets.COGNITO_CLIENT_ID }}
-p COGNITO_USER_POOL=${{ secrets.COGNITO_USER_POOL }}
-p COGNITO_DOMAIN=${{ secrets.COGNITO_DOMAIN }}
-p COGNITO_ENVIRONMENT=TEST
-p COGNITO_REDIRECT_URI=https://${{ env.URL }}/dashboard
-p CHES_MAIL_COPY=${{ secrets.CHES_MAIL_COPY }}

- name: Conventional Changelog Update
Expand Down Expand Up @@ -168,7 +164,6 @@ jobs:
-p CHES_API_URL='https://ches.api.gov.bc.ca/api/v1/email'
-p BCREGISTRY_URI='https://bcregistry-prod.apigee.net'
-p COGNITO_REGION=ca-central-1
-p COGNITO_COOKIE_DOMAIN=gov.bc.ca
-p FRONTEND_URL=${{ env.URL }}

- name: Dev data replacement
Expand Down Expand Up @@ -213,7 +208,8 @@ jobs:
-p COGNITO_USER_POOL=${{ secrets.COGNITO_USER_POOL }}
-p COGNITO_DOMAIN=${{ secrets.COGNITO_DOMAIN }}
-p COGNITO_ENVIRONMENT=TEST
-p COGNITO_LOGOUT_URI='https://logontest7.gov.bc.ca/clp-cgi/logoff.cgi?retnow=1&returl=https://test.loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/logout?redirect_uri=${{ secrets.COGNITO_LOGOUT_URI }}'
-p LANDING_URL='${{ secrets.COGNITO_LOGOUT_URI }}'
-p FRONTEND_URL=${{ env.URL }}

- name: Deploy Processor
uses: bcgov-nr/[email protected]
Expand Down Expand Up @@ -279,12 +275,8 @@ jobs:
-p CHES_CLIENT_SECRET=${{ secrets.CHES_CLIENT_SECRET }}
-p ADDRESS_COMPLETE_KEY=${{ secrets.ADDRESS_COMPLETE_KEY }}
-p DB_PASSWORD=${{ secrets.DB_PASSWORD }}
-p COGNITO_REGION=${{ secrets.COGNITO_REGION }}
-p COGNITO_CLIENT_ID=${{ secrets.COGNITO_CLIENT_ID }}
-p COGNITO_USER_POOL=${{ secrets.COGNITO_USER_POOL }}
-p COGNITO_DOMAIN=${{ secrets.COGNITO_DOMAIN }}
-p COGNITO_ENVIRONMENT=PROD
-p COGNITO_REDIRECT_URI=https://${{ env.URL }}/dashboard
-p CHES_MAIL_COPY=${{ secrets.CHES_MAIL_COPY }}

prod-deploy:
Expand Down Expand Up @@ -349,7 +341,6 @@ jobs:
-p CHES_API_URL='https://ches.api.gov.bc.ca/api/v1/email'
-p BCREGISTRY_URI='https://bcregistry-prod.apigee.net'
-p COGNITO_REGION=ca-central-1
-p COGNITO_COOKIE_DOMAIN=gov.bc.ca
-p FRONTEND_URL=${{ env.URL }}

- name: Deploy Legacy
Expand Down Expand Up @@ -384,7 +375,8 @@ jobs:
-p COGNITO_USER_POOL=${{ secrets.COGNITO_USER_POOL }}
-p COGNITO_DOMAIN=${{ secrets.COGNITO_DOMAIN }}
-p COGNITO_ENVIRONMENT=PROD
-p COGNITO_LOGOUT_URI='https://logon7.gov.bc.ca/clp-cgi/logoff.cgi?retnow=1&returl=https://loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/logout?redirect_uri=${{ secrets.COGNITO_LOGOUT_URI }}'
-p LANDING_URL='${{ secrets.COGNITO_LOGOUT_URI }}'
-p FRONTEND_URL=${{ env.URL }}

- name: Deploy Processor
uses: bcgov-nr/[email protected]
Expand Down
8 changes: 2 additions & 6 deletions .github/workflows/pr-open.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,8 @@ jobs:
-p CHES_CLIENT_SECRET=${{ secrets.CHES_CLIENT_SECRET }}
-p ADDRESS_COMPLETE_KEY=${{ secrets.ADDRESS_COMPLETE_KEY }}
-p DB_PASSWORD=$(echo ${{github.ref}}${{github.event.number}}|md5sum|cut -d' ' -f1)
-p COGNITO_REGION=${{ secrets.COGNITO_REGION }}
-p COGNITO_CLIENT_ID=${{ secrets.COGNITO_CLIENT_ID }}
-p COGNITO_USER_POOL=${{ secrets.COGNITO_USER_POOL }}
-p COGNITO_DOMAIN=${{ secrets.COGNITO_DOMAIN }}
-p COGNITO_ENVIRONMENT=DEV
-p COGNITO_REDIRECT_URI=https://${{ github.event.repository.name }}-${{ github.event.number }}-frontend.apps.silver.devops.gov.bc.ca/dashboard
-p CHES_MAIL_COPY=${{ secrets.CHES_MAIL_COPY }}

- name: Deploy Database Backup
Expand Down Expand Up @@ -156,7 +152,6 @@ jobs:
-p CHES_API_URL='https://ches.api.gov.bc.ca/api/v1/email'
-p BCREGISTRY_URI='https://bcregistry-prod.apigee.net'
-p COGNITO_REGION=ca-central-1
-p COGNITO_COOKIE_DOMAIN=gov.bc.ca
-p FRONTEND_URL=${{ needs.vars.outputs.url }}

- name: Dev data replacement
Expand Down Expand Up @@ -201,7 +196,8 @@ jobs:
-p COGNITO_USER_POOL=${{ secrets.COGNITO_USER_POOL }}
-p COGNITO_DOMAIN=${{ secrets.COGNITO_DOMAIN }}
-p COGNITO_ENVIRONMENT=DEV
-p COGNITO_LOGOUT_URI='https://logontest7.gov.bc.ca/clp-cgi/logoff.cgi?retnow=1&returl=https://dev.loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/logout?redirect_uri=https://${{ github.event.repository.name }}-${{ github.event.number }}-frontend.apps.silver.devops.gov.bc.ca'
-p LANDING_URL=${{ needs.vars.outputs.url }}
-p FRONTEND_URL=${{ needs.vars.outputs.url }}

- name: Deploy Processor
uses: bcgov-nr/[email protected]
Expand Down
9 changes: 1 addition & 8 deletions backend/openshift.deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,6 @@ parameters:
- name: COGNITO_REGION
description: Cognito region to be used
required: true
- name: COGNITO_COOKIE_DOMAIN
description: Cognito cookie domain to be used
required: true
default: gov.bc.ca
- name: FRONTEND_URL
description: Frontend URL
required: true
Expand Down Expand Up @@ -175,10 +171,7 @@ objects:
name: ${NAME}-${ZONE}
key: cognito-environment
- name: COGNITO_REGION
valueFrom:
secretKeyRef:
name: ${NAME}-${ZONE}
key: cognito-region
value: ${COGNITO_REGION}
- name: PROCESSOR_SERVICE_ACCOUNT_NAME
valueFrom:
secretKeyRef:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package ca.bc.gov.app.configuration;

import ca.bc.gov.app.ApplicationConstant;
import ca.bc.gov.app.security.ForestHeadersCustomizer;
import ca.bc.gov.app.security.CorsCustomizer;
import ca.bc.gov.app.security.ApiAuthorizationCustomizer;
import ca.bc.gov.app.security.CorsCustomizer;
import ca.bc.gov.app.security.CsrfCustomizer;
import ca.bc.gov.app.security.HeadersCustomizer;
import ca.bc.gov.app.security.Oauth2Customizer;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity.CsrfSpec;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
Expand All @@ -29,36 +29,37 @@
@EnableWebFluxSecurity
public class SecurityConfiguration {

/**
* This method configures the SecurityWebFilterChain, which is the main security filter for the
* application. It customizes the ServerHttpSecurity object by setting the authorization rules,
* OAuth2 resource server settings, CORS settings, CSRF settings, and HTTP Basic settings. It then
* builds the ServerHttpSecurity object into a SecurityWebFilterChain and returns it.
*
* @param http The ServerHttpSecurity object to be customized.
* @param corsSpecCustomizer The customizer for the CORS settings.
* @param apiAuthorizationCustomizer The customizer for the authorization rules.
* @param oauth2SpecCustomizer The customizer for the OAuth2 resource server settings.
* @param headersCustomizer The customizer for the headers settings.
* @return The configured SecurityWebFilterChain.
*/
@Bean
SecurityWebFilterChain springSecurityFilterChain(
ServerHttpSecurity http,
ForestHeadersCustomizer headersCustomizer,
CorsCustomizer corsSpecCustomizer,
ApiAuthorizationCustomizer apiAuthorizationCustomizer,
Oauth2Customizer oauth2SpecCustomizer
) {
http
.headers(headersCustomizer)
.authorizeExchange(apiAuthorizationCustomizer)
.oauth2ResourceServer(oauth2SpecCustomizer)
.cors(corsSpecCustomizer)
.csrf(CsrfSpec::disable)
.httpBasic(Customizer.withDefaults());
return http.build();
}
/**
* This method is a Spring Bean that configures the Spring Security filter chain.
* The filter chain is a mechanism that Spring Security uses to apply security features to HTTP requests.
*
* @param http The ServerHttpSecurity instance that is used to build the security filter chain.
* @param headersCustomizer A customizer for the HTTP headers security settings.
* @param corsSpecCustomizer A customizer for the Cross-Origin Resource Sharing (CORS) security settings.
* @param apiAuthorizationCustomizer A customizer for the API authorization security settings.
* @param oauth2SpecCustomizer A customizer for the OAuth2 resource server security settings.
* @param csrfSpecCustomizer A customizer for the Cross-Site Request Forgery (CSRF) security settings.
*
* @return The configured SecurityWebFilterChain.
*/
@Bean
SecurityWebFilterChain springSecurityFilterChain(
ServerHttpSecurity http,
HeadersCustomizer headersCustomizer,
CorsCustomizer corsSpecCustomizer,
ApiAuthorizationCustomizer apiAuthorizationCustomizer,
Oauth2Customizer oauth2SpecCustomizer,
CsrfCustomizer csrfSpecCustomizer
) {
http
.headers(headersCustomizer)
.authorizeExchange(apiAuthorizationCustomizer)
.oauth2ResourceServer(oauth2SpecCustomizer)
.cors(corsSpecCustomizer)
.csrf(csrfSpecCustomizer)
.httpBasic(Customizer.withDefaults());
return http.build();
}

/**
* This method creates a ReactiveJwtDecoder bean. The ReactiveJwtDecoder is used to decode JWTs in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.security.web.reactive.result.view.CsrfRequestDataValueProcessor;
import org.springframework.security.web.server.csrf.CsrfToken;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.RequestPredicates;
Expand All @@ -24,8 +27,27 @@
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
* The GlobalErrorController class extends the AbstractErrorWebExceptionHandler class.
* It is annotated with @RestControllerAdvice, @ControllerAdvice, @Slf4j, @Component, and @Order.
*
* @RestControllerAdvice is a convenience annotation that is itself annotated with @ControllerAdvice and @ResponseBody.
* This annotation is used to assist with exception handling in a @Controller class.
*
* @ControllerAdvice is an annotation provided by Spring allowing you to handle exceptions across the whole application, not just an individual controller.
* You can think of it as an interceptor of exceptions thrown by methods annotated with @RequestMapping and similar.
*
* @Slf4j is a simple facade for logging systems allowing the end-user to plug in the desired logging system at deployment time.
*
* @Component is an annotation that allows Spring to automatically detect our custom beans.
*
* @Order is used to sort the components that Spring should load in the ApplicationContext.
*
* The GlobalErrorController class is responsible for handling and routing all the exceptions that occur within the application.
*/
@RestControllerAdvice
@ControllerAdvice
@Slf4j
Expand All @@ -42,47 +64,113 @@ public GlobalErrorController(
this.setMessageWriters(configurer.getWriters());
}

@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(
RequestPredicates.all(), request -> renderErrorResponse(request, errorAttributes));
}
/**
* This method is responsible for routing all requests to the error response rendering method.
* It overrides the getRoutingFunction method from the AbstractErrorWebExceptionHandler class.
* The method takes ErrorAttributes as a parameter, which are used to get the error associated with a request.
* It returns a RouterFunction that routes all requests to the renderErrorResponse method.
*
* @param errorAttributes The ErrorAttributes associated with the request.
* @return A RouterFunction that routes all requests to the renderErrorResponse method.
*/
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(
RequestPredicates.all(), request -> renderErrorResponse(request, errorAttributes));
}

private Mono<ServerResponse> renderErrorResponse(
ServerRequest request, ErrorAttributes errorAttributes) {
/**
* This method is responsible for handling and rendering error responses.
* It takes a ServerRequest and ErrorAttributes as parameters.
* The method first retrieves the error associated with the request and logs it.
* If the error is an instance of ValidationException, it logs the validation errors and returns a
* response with the status code, reason, and errors from the exception.
* If the error is not a ValidationException, it checks if it's a ResponseStatusException.
* If it is, it retrieves the reason and status code from the exception.
* If the error message is blank, it sets it to an empty string.
* Finally, it logs the error status and message, and returns a response with the status code
* and error message.
*
* @param request The ServerRequest that caused the error.
* @param errorAttributes The ErrorAttributes associated with the request.
* @return A Mono<ServerResponse> that represents the error response.
*/
private Mono<ServerResponse> renderErrorResponse(
ServerRequest request, ErrorAttributes errorAttributes) {

Throwable exception = errorAttributes.getError(request).fillInStackTrace();
// Get the error associated with the request and fill in its stack trace
Throwable exception = errorAttributes.getError(request).fillInStackTrace();

log.error(
"An error was generated during request for {} {}",
request.method(),
request.requestPath(),
exception);
// Log the error
log.error(
"An error was generated during request for {} {}",
request.method(),
request.requestPath(),
exception);

if (exception instanceof ValidationException validationException) {
log.error("Failed Validations: {}",
Arrays.toString(validationException.getErrors().toArray()));
return ServerResponse.status(validationException.getStatusCode())
.header("Reason", validationException.getReason())
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(validationException.getErrors()));
}
// If the error is a ValidationException, log the validation errors and return a response with the status code, reason, and errors from the exception
if (exception instanceof ValidationException validationException) {
log.error("Failed Validations: {}",
Arrays.toString(validationException.getErrors().toArray()));
return ServerResponse.status(validationException.getStatusCode())
.header("Reason", validationException.getReason())
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(validationException.getErrors()));
}

String errorMessage = exception.getMessage();
HttpStatusCode errorStatus = HttpStatus.INTERNAL_SERVER_ERROR;
// Get the error message
String errorMessage = exception.getMessage();
// Set the default error status to INTERNAL_SERVER_ERROR
HttpStatusCode errorStatus = HttpStatus.INTERNAL_SERVER_ERROR;

if (exception instanceof ResponseStatusException responseStatusException) {
errorMessage = responseStatusException.getReason();
errorStatus = responseStatusException.getStatusCode();
}
// If the error is a ResponseStatusException, get the reason and status code from the exception
if (exception instanceof ResponseStatusException responseStatusException) {
errorMessage = responseStatusException.getReason();
errorStatus = responseStatusException.getStatusCode();
}

errorMessage =
BooleanUtils.toString(StringUtils.isBlank(errorMessage), StringUtils.EMPTY, errorMessage);
// If the error message is blank, set it to an empty string
errorMessage =
BooleanUtils.toString(StringUtils.isBlank(errorMessage), StringUtils.EMPTY, errorMessage);

log.error("{} - {}", errorStatus, errorMessage);
// Log the error status and message
log.error("{} - {}", errorStatus, errorMessage);

return ServerResponse.status(errorStatus)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(errorMessage));
// Return a response with the status code and error message
return ServerResponse.status(errorStatus)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(errorMessage));
}

/**
* This method is used to handle CSRF (Cross-Site Request Forgery) tokens in the application.
* It is annotated with @ModelAttribute, which means its executed before any request mapping methods.
* The method returns a Mono<Void> as the cookie will be set asynchronously behind the scene and this
* is here only to subscribe the requester to receive the cookie.
*
* Another important thing here is that this method will always set the csrf token on the stream.
*
* @param exchange The ServerWebExchange interface provides access to information about the request and response.
* @return Mono<Void> If the CSRF token is not present in the ServerWebExchange, it returns an empty Mono.
* If the CSRF token is present, it adds the token to the ServerWebExchange attributes and then returns a Mono<Void> indicating completion.
*/
@ModelAttribute
Mono<Void> csrfCookie(ServerWebExchange exchange) {
// Retrieve the CSRF token from the ServerWebExchange
Mono<CsrfToken> csrfToken = exchange.getAttribute(CsrfToken.class.getName());

// If the CSRF token is not present, return an empty Mono
if (csrfToken == null) {
return Mono.empty();
}

// If the CSRF token is present, add it to the ServerWebExchange attributes
// The token is added under the key CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME
// After adding the token, a Mono<Void> indicating completion is returned
return csrfToken
.doOnSuccess(token -> exchange
.getAttributes()
.put(CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME, token))
.then();
}
}
Loading

0 comments on commit 142b2c6

Please sign in to comment.