Skip to content

Commit

Permalink
GAP-2594: add interceptor + annotation on the endpoint consumed by th…
Browse files Browse the repository at this point in the history
…e lambda-upload and add decryption
  • Loading branch information
a-lor-cab committed Apr 17, 2024
1 parent 58aaba2 commit e1f2116
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package gov.cabinetoffice.gap.applybackend.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LambdasHeaderValidator {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package gov.cabinetoffice.gap.applybackend.config;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Configuration("lambdaProperties")
@ConfigurationProperties(prefix = "lambda")
public class LambdaSecretConfigProperties {

private String secret;

private String privateKey;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package gov.cabinetoffice.gap.applybackend.config;

import gov.cabinetoffice.gap.applybackend.security.interceptors.AuthorizationHeaderInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@RequiredArgsConstructor
@Configuration
public class LambdasInterceptor implements WebMvcConfigurer {

private static final String UUID_REGEX_STRING = "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}";
private final LambdaSecretConfigProperties lambdaSecretConfigProperties;

@Bean()
AuthorizationHeaderInterceptor lambdasInterceptor() {
return new AuthorizationHeaderInterceptor(lambdaSecretConfigProperties.getSecret(),
lambdaSecretConfigProperties.getPrivateKey());
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(lambdasInterceptor())
.addPathPatterns( "/submissions/{submissionId:" + UUID_REGEX_STRING
+ "}/question/{questionId:" + UUID_REGEX_STRING + "}/attachment/scanresult")
.order(Ordered.HIGHEST_PRECEDENCE);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package gov.cabinetoffice.gap.applybackend.security.interceptors;


import gov.cabinetoffice.gap.applybackend.annotations.LambdasHeaderValidator;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpHeaders;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

//this is needed to "authenticate" all the call from the Lambdas
@Log4j2
public class AuthorizationHeaderInterceptor implements HandlerInterceptor {

private final String expectedAuthorizationValue;

private final String privateKey;

public AuthorizationHeaderInterceptor(String expectedAuthorizationValue, String privateKey) {
this.expectedAuthorizationValue = expectedAuthorizationValue;
this.privateKey = privateKey;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
log.info("Intercepting request: " + request.getRequestURI());
if (handler instanceof HandlerMethod handlerMethod) {
final Method method = handlerMethod.getMethod();

final LambdasHeaderValidator annotation = method.getAnnotation(LambdasHeaderValidator.class);

if (annotation != null) {
log.info("Request is coming from lambda, validating authorization header");

final String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
final boolean isAuthorizationHeaderCorrect = compareAuthorizationSecret(authorizationHeader,
expectedAuthorizationValue, privateKey);
if (authorizationHeader == null || !isAuthorizationHeaderCorrect) {

log.info("Authorization Header Value: " + authorizationHeader
+ " does not match the expected value");

response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return false;
}

log.info("Authorization Header Value matches the expected value");
}
}

return true;
}

private boolean compareAuthorizationSecret(String authorizationHeader, String expectedAuthorizationValue,
String privateKey) {

if (authorizationHeader == null || privateKey == null) {
return false;
}

return decrypt(authorizationHeader, privateKey).equals(expectedAuthorizationValue);

}

private String decrypt(String encryptedText, String privateKeyString) {

try {

final byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyString);
final byte[] encryptedMessageBytes = Base64.getDecoder().decode(encryptedText);

final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
final KeyFactory keyFactory = KeyFactory.getInstance("RSA");
final PrivateKey rsaPrivateKey = keyFactory.generatePrivate(keySpec);

final Cipher decryptCipher = Cipher.getInstance("RSA");
decryptCipher.init(Cipher.DECRYPT_MODE, rsaPrivateKey);

final byte[] decryptedMessageBytes = decryptCipher.doFinal(encryptedMessageBytes);

return new String(decryptedMessageBytes, StandardCharsets.UTF_8);
}
catch (IllegalArgumentException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException
| BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException e) {
log.info("Error decrypting authorization header from lambdas: " + e.getMessage());
return "";
}
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gov.cabinetoffice.gap.applybackend.web;

import com.fasterxml.jackson.core.JsonProcessingException;
import gov.cabinetoffice.gap.applybackend.annotations.LambdasHeaderValidator;
import gov.cabinetoffice.gap.applybackend.constants.APIConstants;
import gov.cabinetoffice.gap.applybackend.dto.api.CreateQuestionResponseDto;
import gov.cabinetoffice.gap.applybackend.dto.api.CreateSubmissionResponseDto;
Expand Down Expand Up @@ -56,7 +57,6 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
Expand Down Expand Up @@ -273,14 +273,13 @@ public ResponseEntity<CreateSubmissionResponseDto> createApplication(@PathVariab

//this endpoint is consumed by the lambda-upload
@PutMapping("/{submissionId}/question/{questionId}/attachment/scanresult")
@LambdasHeaderValidator
public ResponseEntity<String> updateAttachment(
@PathVariable final UUID submissionId,
@PathVariable final String questionId,
@RequestBody final UpdateAttachmentDto updateDetails,
@RequestHeader(HttpHeaders.AUTHORIZATION) final String authHeader) {
log.info("Lambda-upload is updating attachment for submissionId: {}, questionId: {} based on the scan result", submissionId, questionId);
@RequestBody final UpdateAttachmentDto updateDetails) {

secretAuthService.authenticateSecret(authHeader);
log.info("Lambda-upload is updating attachment for submissionId: {}, questionId: {} based on the scan result", submissionId, questionId);

final Submission submission = submissionService.getSubmissionById(submissionId);
log.info("Submission found for submissionId: {}", submissionId);
Expand Down
4 changes: 3 additions & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ frontEndUri=http://localhost:3001/apply/applicant
environmentName=an-environment-name

lambda.secret=lambdaSecretKey
lambda.privateKey=lambdaPrivateKey

feature.onelogin.enabled=false

Expand All @@ -49,4 +50,5 @@ contentful.environmentId=an-environment
spotlight-queue.queueUrl=a-sqs-queue-url

cloud.aws.sqs.event-service-queue=gap-apply-events-service-queue
cloud.aws.sqs.event-service-queue-enabled=true
cloud.aws.sqs.event-service-queue-enabled=true
spring.profiles.active=LOCAL
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package gov.cabinetoffice.gap.applybackend.security.interceptors;

import gov.cabinetoffice.gap.applybackend.annotations.LambdasHeaderValidator;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.web.method.HandlerMethod;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@SpringJUnitConfig

class AuthorizationHeaderInterceptorTest {

private static final String EXPECTED_AUTHORIZATION_VALUE = "expectedToken";

private static final String ENCRYPTED_EXPECTED_AUTHORIZATION_VALUE = "gPiAtfeQbMcv9e0boGa2tlNIthMM8uXq1JnzQy/KVPtpcmqVpOkA28dKR7UN/hJTO8ACW3TVVMTzHxvmJT+YPUmC7ggRHO9VSpItdPBdwgaHzDDu571KYrTLKEeUYhzt00PfB+O7kwA8cw1sr2rog8dI0wXVU0tBqkoFjBUl+NdSCe/AxkjL1ziq6MSpI3yflJC3crkYoZf0zrWQTKRZWIr2MXBgCRHfXGy0LKdwrkvKUUvcWObSybA6yPFzGW4IDNbivOUngGr9goRtnkSY3/ezvsQzl/nPGS2VF2FrTtDD/U7kKOE83s5ojKR8VEu4hCieYu0T8eT3ur51seHTOA==";

private static final String PRIVATE_KEY = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDUKDTCzH6/coqqf2y95NWJ/FfrSbT273rCrTT8in+CDeDaHmm+kHSwOigL5o5Vci4kY71/pqLASp9PwmSbbWuzTnK0hr30aZsYELVr1Nv4lE6IrQUby2pDaiXdd/PrKBeXGbzd5JNmamYqLn8B9jl3Sftde66pGhimaSr2MTLGfE32ZoK8VA3mAHGURt9zWZ3B97d+ivkT/UTu64VzczsyrIf9Rtll0xrIki00NToXt5gX32YPzGHX1ZYtT2aGVpfZSpgnwOuJZORIepE8aB5jsqvy24gGBi60nYFSXW1mxVRJ7VHZoOLPkiX28BLexsl4CIYTvAUwJhEGy3QxOTWRAgMBAAECggEAD6unmALkUs5KVUUjulQCfNZ1hQTU989hDSufiA7P08KkKuBHtcJ0SAcHqbdeKIwOQfRXDSx5BYSNh2Xnqf5XT+95tAFm2vyI7upfCCe5Fr+XnGtRPwgToYaf5N+lFefIdEn7pBUr2Qr/YFq+WV3/SSMfDLzjvgxWKUaH3JbuNfBafyuLDObXFYKTxS+JJIE/DHFKK9qG+D1KSaJsm7XxD3u9njziXiNP62DnnhpW7NE7z8Bj8Pn5qJEnW8DSjGlWfeNJ0dvXvepBgilk0dZKEuDXVHpw3yxl/aP+SWA3IxMsYRi6vGC6dCASO1oiQoh69FhWAxYeyZAFJXM+5luH1QKBgQD+eq26WN7HkD9slrvA9lP7NaE47BsFsxJbWGjJOkimA+arVVvKlflPZ8RR4nQIFex8ygm2LoTZVxLb6y21NVowNthPltitWPEuvGxmwM2dP9wHU+HGo+TSFVTKspxj9vdBItO2Jv9xatevUjbmmkJwUero3pAn7yss6OkzBx1rnQKBgQDVbMelE/Bj6yFtFFIGuljpap6R3gMEq7+cqIvYT5z1EOdb23DPYD30U7YZPx19hIB+Q8NF6CHiuLIgNgm4Vm0DlCBaxuS/SvXclII/hMKOTKFI8TkQ4C3cOukMpbrZRSnslfQAx9Nw2I6aega8MmQx+Ib3oX97K4utbDKzGTBxhQKBgDS0KajPYQxJtqkBqs7y9T+wwrwsQghzJtkePU13sbYYVryjSAhz+RdV8VXYJZnLGJhbq5k8ly0AGJrNYUEHVK9pnHEXV7bHFeRNB9JcGfl4UVR/LeJa7TgJTO9SAIu/iUPHN7ug5gSSUSsxRseJqTrj7FfgSFDM+s8pSarUzWYNAoGAZv0UJhPGF+FaAvIgVwDLcO+zTz5sBHAAic9HlH1uh0+95TSybk/J9cIDiJFNYMl02/lFHNHUsDxiMGsDK8IA3w42wrdhoPHCTMwZQh+FZveRiMpmuD8FwlVnKmQ7EyduAK2nzkyfOsA1qFOkNmK2uOpt7scu5jfwMiKZIJXSK6UCgYB446Uz1BR/0Gb7zQ6hbV/tEO1pOtgV/uXMOFJ4UOulyQzQXyIhMuzT/66oxKc7Ik1ZQ468bVv4CCL4yoaKOj04j1z23v3aR9tg2gv45ryRiqo2vJbwMrZpP/BYeIZehxMrXauqMKwl/t64JR8XTwArbWVDSIqwYX76kGG46YHW/w==";

@Mock
private HttpServletRequest request;

@Mock
private HttpServletResponse response;

@Mock
private HandlerMethod handlerMethod;

@InjectMocks
private AuthorizationHeaderInterceptor authorizationHeaderInterceptor;

@Test
void preHandleValidAuthorization() throws Exception {
final byte[] expectedAuthorizationValueBytes = EXPECTED_AUTHORIZATION_VALUE.getBytes(StandardCharsets.UTF_8);

authorizationHeaderInterceptor = new AuthorizationHeaderInterceptor(EXPECTED_AUTHORIZATION_VALUE, PRIVATE_KEY);
when(handlerMethod.getMethod()).thenReturn(getClass().getMethod("annotatedTestMethod"));
when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(ENCRYPTED_EXPECTED_AUTHORIZATION_VALUE);

boolean result = authorizationHeaderInterceptor.preHandle(request, response, handlerMethod);

assertTrue(result);
}

@Test
void preHandleNullAuthorizationHeader() throws Exception {
when(handlerMethod.getMethod()).thenReturn(getClass().getMethod("annotatedTestMethod"));
when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(null);

boolean result = authorizationHeaderInterceptor.preHandle(request, response, handlerMethod);

verify(response).setStatus(HttpServletResponse.SC_FORBIDDEN);
assertFalse(result);
}

@Test
void preHandleInvalidAuthorizationHeader() throws Exception {
String invalidAuthorizationValue = "invalidToken";
when(handlerMethod.getMethod()).thenReturn(getClass().getMethod("annotatedTestMethod"));
when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(invalidAuthorizationValue);

boolean result = authorizationHeaderInterceptor.preHandle(request, response, handlerMethod);

verify(response).setStatus(HttpServletResponse.SC_FORBIDDEN);
assertFalse(result);
}

@Test
void preHandleValidAuthorizationForNonAnnotatedMethods() throws Exception {
authorizationHeaderInterceptor = new AuthorizationHeaderInterceptor(EXPECTED_AUTHORIZATION_VALUE, PRIVATE_KEY);
when(handlerMethod.getMethod()).thenReturn(getClass().getMethod("nonAnnotatedTestMethod"));
when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(EXPECTED_AUTHORIZATION_VALUE);

boolean result = authorizationHeaderInterceptor.preHandle(request, response, handlerMethod);

assertTrue(result);
}

// Test method to provide a valid HandlerMethod for testing
@LambdasHeaderValidator
public void annotatedTestMethod() {
// This method is just a placeholder for testing HandlerMethod
}

// Test method to provide a NON valid HandlerMethod for testing
public void nonAnnotatedTestMethod() {
// This method is just a placeholder for testing HandlerMethod
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -1173,7 +1173,7 @@ void updateAttachment_UpdatesExpectedAttachment(UpdateAttachmentDto update, Gran

final ArgumentCaptor<GrantAttachment> attachmentCaptor = ArgumentCaptor.forClass(GrantAttachment.class);

final ResponseEntity<String> methodResponse = controllerUnderTest.updateAttachment(SUBMISSION_ID, QUESTION_ID_1, update, "topSecretKey");
final ResponseEntity<String> methodResponse = controllerUnderTest.updateAttachment(SUBMISSION_ID, QUESTION_ID_1, update);

assertThat(methodResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(methodResponse.getBody()).isEqualTo("Attachment Updated");
Expand All @@ -1184,13 +1184,7 @@ void updateAttachment_UpdatesExpectedAttachment(UpdateAttachmentDto update, Gran
assertThat(attachmentCaptor.getValue().getLocation()).isEqualTo(update.getUri());
}

@ParameterizedTest
@MethodSource("provideGrantAttachmentUpdates")
void updateAttachment_unauthenticatedError(UpdateAttachmentDto update) {
doThrow(new UnauthorizedException("Unauthorized oh nooo")).when(secretAuthService).authenticateSecret(anyString());

assertThrows(UnauthorizedException.class, () -> controllerUnderTest.updateAttachment(SUBMISSION_ID, QUESTION_ID_1, update, "topSecretKey"));
}
}
}
}

0 comments on commit e1f2116

Please sign in to comment.