-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
GAP-2594: add interceptor + annotation on the endpoint consumed by th…
…e lambda-upload and add decryption
- Loading branch information
Showing
8 changed files
with
279 additions
and
13 deletions.
There are no files selected for viewing
12 changes: 12 additions & 0 deletions
12
src/main/java/gov/cabinetoffice/gap/applybackend/annotations/LambdasHeaderValidator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
|
||
} |
22 changes: 22 additions & 0 deletions
22
src/main/java/gov/cabinetoffice/gap/applybackend/config/LambdaSecretConfigProperties.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
||
} |
32 changes: 32 additions & 0 deletions
32
src/main/java/gov/cabinetoffice/gap/applybackend/config/LambdasInterceptor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
105 changes: 105 additions & 0 deletions
105
.../cabinetoffice/gap/applybackend/security/interceptors/AuthorizationHeaderInterceptor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ""; | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
...inetoffice/gap/applybackend/security/interceptors/AuthorizationHeaderInterceptorTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters