From c89b919f3f023dfbf8cc6013c3e805f977626314 Mon Sep 17 00:00:00 2001 From: Florian Date: Fri, 25 Oct 2024 18:06:31 +0200 Subject: [PATCH] =?UTF-8?q?Use=20sdk=20for=20callback=20verification=20?= =?UTF-8?q?=F0=9F=A4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 1 + gradle/libs.versions.toml | 1 + .../a2j/config/SecurityConfig.java | 14 ++--- .../receiver/CallbackVerificationFilter.java | 50 +++++++++++++++ .../CallbackVerificationFilter.java | 40 ------------ .../CallbackVerificationService.java | 28 --------- .../receiver/verification/HmacVerifier.java | 32 ---------- .../verification/TimestampVerifier.java | 18 ------ src/main/resources/application-local.yaml | 8 +++ src/main/resources/application.yaml | 13 ++-- .../CallbackVerificationServiceTest.java | 62 ------------------- .../verification/TimestampVerifierTest.java | 41 ------------ 12 files changed, 73 insertions(+), 235 deletions(-) create mode 100644 src/main/java/de/bund/digitalservice/a2j/service/receiver/CallbackVerificationFilter.java delete mode 100644 src/main/java/de/bund/digitalservice/a2j/service/receiver/verification/CallbackVerificationFilter.java delete mode 100644 src/main/java/de/bund/digitalservice/a2j/service/receiver/verification/CallbackVerificationService.java delete mode 100644 src/main/java/de/bund/digitalservice/a2j/service/receiver/verification/HmacVerifier.java delete mode 100644 src/main/java/de/bund/digitalservice/a2j/service/receiver/verification/TimestampVerifier.java create mode 100644 src/main/resources/application-local.yaml delete mode 100644 src/test/java/de/bund/digitalservice/a2j/service/receiver/verification/CallbackVerificationServiceTest.java delete mode 100644 src/test/java/de/bund/digitalservice/a2j/service/receiver/verification/TimestampVerifierTest.java diff --git a/build.gradle.kts b/build.gradle.kts index a8bc200..62c38aa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { implementation(libs.spring.boot.starter.actuator) implementation(libs.spring.boot.starter.security) implementation(libs.spring.boot.starter.web) + implementation("org.springframework.cloud:spring-cloud-starter-kubernetes-client-config:3.1.3") implementation(libs.fitko.fitconnect.sdk) compileOnly(libs.lombok) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f562610..bef278a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ spring-boot-starter-actuator = { module = "org.springframework.boot:spring-boot- spring-boot-starter-security = { module = "org.springframework.boot:spring-boot-starter-security" } spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web" } spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test" } +spring-cloud-starter-kubernetes-client-config = { module = "org.springframework.cloud:spring-cloud-starter-kubernetes-client-config"} spring-security-test = { module = "org.springframework.security:spring-security-test" } fitko-fitconnect-sdk = "dev.fitko.fitconnect.sdk:client:2.3.0" diff --git a/src/main/java/de/bund/digitalservice/a2j/config/SecurityConfig.java b/src/main/java/de/bund/digitalservice/a2j/config/SecurityConfig.java index f9ec97c..9a20263 100644 --- a/src/main/java/de/bund/digitalservice/a2j/config/SecurityConfig.java +++ b/src/main/java/de/bund/digitalservice/a2j/config/SecurityConfig.java @@ -1,7 +1,6 @@ package de.bund.digitalservice.a2j.config; -import de.bund.digitalservice.a2j.service.receiver.verification.CallbackVerificationFilter; -import de.bund.digitalservice.a2j.service.receiver.verification.CallbackVerificationService; +import de.bund.digitalservice.a2j.service.receiver.CallbackVerificationFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -13,10 +12,11 @@ @Configuration @EnableWebSecurity public class SecurityConfig { - private final CallbackVerificationService callbackVerificationService; - public SecurityConfig(CallbackVerificationService callbackVerificationService) { - this.callbackVerificationService = callbackVerificationService; + private final CallbackVerificationFilter callbackVerificationFilter; + + public SecurityConfig(CallbackVerificationFilter callbackVerificationFilter) { + this.callbackVerificationFilter = callbackVerificationFilter; } @Bean @@ -36,9 +36,7 @@ public SecurityFilterChain springSecurityWebFilterChain(HttpSecurity http) throw .permitAll() .anyRequest() .denyAll()) - .addFilterAfter( - new CallbackVerificationFilter(callbackVerificationService), - BasicAuthenticationFilter.class) + .addFilterAfter(callbackVerificationFilter, BasicAuthenticationFilter.class) .build(); } diff --git a/src/main/java/de/bund/digitalservice/a2j/service/receiver/CallbackVerificationFilter.java b/src/main/java/de/bund/digitalservice/a2j/service/receiver/CallbackVerificationFilter.java new file mode 100644 index 0000000..3310837 --- /dev/null +++ b/src/main/java/de/bund/digitalservice/a2j/service/receiver/CallbackVerificationFilter.java @@ -0,0 +1,50 @@ +package de.bund.digitalservice.a2j.service.receiver; + +import dev.fitko.fitconnect.client.SenderClient; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@Component +public class CallbackVerificationFilter extends OncePerRequestFilter { + private final SenderClient senderClient; + private final String callbackSecret; + + public CallbackVerificationFilter( + SenderClient senderClient, @Value("${callbackSecret}") String callbackSecret) { + this.senderClient = senderClient; + this.callbackSecret = callbackSecret; + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + return !request.getServletPath().startsWith("/callbacks/fit-connect"); + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain chain) + throws ServletException, IOException { + + if (!senderClient + .validateCallback( + request.getHeader("callback-athentication"), + Long.parseLong(request.getHeader("callback-timestamp")), + request.getReader().lines().reduce("", String::concat), + callbackSecret) + .isValid()) { + + System.out.println("Received fit-connect callback, returned 401"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + + chain.doFilter(request, response); + } +} diff --git a/src/main/java/de/bund/digitalservice/a2j/service/receiver/verification/CallbackVerificationFilter.java b/src/main/java/de/bund/digitalservice/a2j/service/receiver/verification/CallbackVerificationFilter.java deleted file mode 100644 index 127b872..0000000 --- a/src/main/java/de/bund/digitalservice/a2j/service/receiver/verification/CallbackVerificationFilter.java +++ /dev/null @@ -1,40 +0,0 @@ -package de.bund.digitalservice.a2j.service.receiver.verification; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import org.springframework.web.filter.OncePerRequestFilter; - -public class CallbackVerificationFilter extends OncePerRequestFilter { - - private final CallbackVerificationService service; - - public CallbackVerificationFilter(CallbackVerificationService service) { - this.service = service; - } - - @Override - protected boolean shouldNotFilter(HttpServletRequest request) { - return !request.getServletPath().startsWith("/callbacks/fit-connect"); - } - - @Override - protected void doFilterInternal( - HttpServletRequest request, HttpServletResponse response, FilterChain chain) - throws ServletException, IOException { - - if (!service.isValidCallback( - request.getHeader("callback-authentication"), - request.getHeader("callback-timestamp"), - request.getReader().lines().reduce("", String::concat))) { - - System.out.println("Received callback, returned 401"); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return; - } - - chain.doFilter(request, response); - } -} diff --git a/src/main/java/de/bund/digitalservice/a2j/service/receiver/verification/CallbackVerificationService.java b/src/main/java/de/bund/digitalservice/a2j/service/receiver/verification/CallbackVerificationService.java deleted file mode 100644 index 1e7d86a..0000000 --- a/src/main/java/de/bund/digitalservice/a2j/service/receiver/verification/CallbackVerificationService.java +++ /dev/null @@ -1,28 +0,0 @@ -package de.bund.digitalservice.a2j.service.receiver.verification; - -import org.springframework.stereotype.Service; - -@Service -public class CallbackVerificationService { - private final TimestampVerifier timestampVerifier; - private final HmacVerifier hmacVerifier; - - CallbackVerificationService(TimestampVerifier timestampVerifier, HmacVerifier hmacVerifier) { - this.timestampVerifier = timestampVerifier; - this.hmacVerifier = hmacVerifier; - } - - public boolean isValidCallback(String callbackAuth, String callbackTimestamp, String body) { - if (!timestampVerifier.verify(callbackTimestamp)) { - System.out.println("Error: callback timestamp invalid"); - return false; - } - - if (!hmacVerifier.verify(callbackAuth, callbackTimestamp, body)) { - System.out.println("Error: callback hmac invalid"); - return false; - } - - return true; - } -} diff --git a/src/main/java/de/bund/digitalservice/a2j/service/receiver/verification/HmacVerifier.java b/src/main/java/de/bund/digitalservice/a2j/service/receiver/verification/HmacVerifier.java deleted file mode 100644 index bbb8609..0000000 --- a/src/main/java/de/bund/digitalservice/a2j/service/receiver/verification/HmacVerifier.java +++ /dev/null @@ -1,32 +0,0 @@ -package de.bund.digitalservice.a2j.service.receiver.verification; - -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import org.bouncycastle.util.encoders.Hex; -import org.springframework.stereotype.Component; - -@Component -public class HmacVerifier { - public boolean verify(String authentication, String timestamp, String body) { - try { - return !authentication.equals(recomputeHMAC(timestamp, body)); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - System.out.println("Error: cannot compute hmac"); - return false; - } - } - - private String recomputeHMAC(String timestamp, String body) - throws NoSuchAlgorithmException, InvalidKeyException { - Mac sha512HMAC = Mac.getInstance("HmacSHA512"); - SecretKeySpec secretKey = - new SecretKeySpec("123".getBytes(StandardCharsets.UTF_8), "HmacSHA512"); - - sha512HMAC.init(secretKey); - return Hex.toHexString( - sha512HMAC.doFinal((timestamp + "." + body).getBytes(StandardCharsets.UTF_8))); - } -} diff --git a/src/main/java/de/bund/digitalservice/a2j/service/receiver/verification/TimestampVerifier.java b/src/main/java/de/bund/digitalservice/a2j/service/receiver/verification/TimestampVerifier.java deleted file mode 100644 index efe87ea..0000000 --- a/src/main/java/de/bund/digitalservice/a2j/service/receiver/verification/TimestampVerifier.java +++ /dev/null @@ -1,18 +0,0 @@ -package de.bund.digitalservice.a2j.service.receiver.verification; - -import java.time.Instant; -import org.springframework.stereotype.Component; - -@Component -public class TimestampVerifier { - public boolean verify(String callbackTimestamp) { - try { - long timestamp = Long.parseLong(callbackTimestamp); - long currentTime = Instant.now().getEpochSecond(); - - return timestamp <= currentTime && (currentTime - timestamp) < 300; - } catch (NumberFormatException e) { - return false; - } - } -} diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml new file mode 100644 index 0000000..7ccbdbd --- /dev/null +++ b/src/main/resources/application-local.yaml @@ -0,0 +1,8 @@ +submission: + destination: d0fe1fad-d6f1-4c67-b4e7-9d0ca6663bf3 + serviceType: + urn: urn:de:fim:leika:leistung:99400048079000 + name: Simple Dummy Service + jsonUri: https://schema.fitko.de/fim/s17000717_1.0.schema.json + +callbackSecret: s3cr3t diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index db4f3ea..e5eb631 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,9 +1,10 @@ -submission: - destination: d0fe1fad-d6f1-4c67-b4e7-9d0ca6663bf3 - serviceType: - urn: urn:de:fim:leika:leistung:99400048079000 - name: Simple Dummy Service - jsonUri: https://schema.fitko.de/fim/s17000717_1.0.schema.json +spring: + cloud: + kubernetes: + secrets: + enabled: true + fail-fast: true + paths: /etc/secrets management: endpoint: diff --git a/src/test/java/de/bund/digitalservice/a2j/service/receiver/verification/CallbackVerificationServiceTest.java b/src/test/java/de/bund/digitalservice/a2j/service/receiver/verification/CallbackVerificationServiceTest.java deleted file mode 100644 index 2987e43..0000000 --- a/src/test/java/de/bund/digitalservice/a2j/service/receiver/verification/CallbackVerificationServiceTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package de.bund.digitalservice.a2j.service.receiver.verification; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.when; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -public class CallbackVerificationServiceTest { - - @Mock private TimestampVerifier timestampVerifier; - @Mock private HmacVerifier hmacVerifier; - - private CallbackVerificationService service; - - @BeforeEach - void setup() { - service = new CallbackVerificationService(timestampVerifier, hmacVerifier); - } - - @Test - void testValidCallback_withValidCallback() { - String timestamp = "validTimestamp"; - String authentication = "validAuthentication"; - String body = "body"; - - when(timestampVerifier.verify(timestamp)).thenReturn(true); - when(hmacVerifier.verify(authentication, timestamp, body)).thenReturn(true); - - assertTrue(service.isValidCallback(authentication, timestamp, body)); - } - - @Test - void testValidCallback_withInvalidTimestamp() { - String timestamp = "invalidTimestamp"; - String authentication = "validAuthentication"; - String body = "body"; - - when(timestampVerifier.verify(timestamp)).thenReturn(false); - lenient().when(hmacVerifier.verify(authentication, timestamp, body)).thenReturn(true); - - assertFalse(service.isValidCallback(authentication, timestamp, body)); - } - - @Test - void testValidCallback_withInvalidHmac() { - String timestamp = "validTimestamp"; - String authentication = "invalidAuthentication"; - String body = "body"; - - when(timestampVerifier.verify(timestamp)).thenReturn(true); - when(hmacVerifier.verify(authentication, timestamp, body)).thenReturn(false); - - assertFalse(service.isValidCallback(authentication, timestamp, body)); - } -} diff --git a/src/test/java/de/bund/digitalservice/a2j/service/receiver/verification/TimestampVerifierTest.java b/src/test/java/de/bund/digitalservice/a2j/service/receiver/verification/TimestampVerifierTest.java deleted file mode 100644 index e82cb27..0000000 --- a/src/test/java/de/bund/digitalservice/a2j/service/receiver/verification/TimestampVerifierTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package de.bund.digitalservice.a2j.service.receiver.verification; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.time.Instant; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -public class TimestampVerifierTest { - private TimestampVerifier verifier; - - @BeforeEach - void setup() { - this.verifier = new TimestampVerifier(); - } - - @Test - void testVerify_withValidTimestamp() { - long currentTime = Instant.now().getEpochSecond(); - String validTimestamp = String.valueOf(currentTime); - - assertTrue(verifier.verify(validTimestamp), "Callback should be valid with current timestamp"); - } - - @ParameterizedTest - @ValueSource(strings = {"invalid", "", "-12334554121", "99999999999999999"}) - void testVerify_withMalformedTimestamp(String timestamp) { - assertFalse(verifier.verify(timestamp)); - } - - @ParameterizedTest - @ValueSource(longs = {301, 400, 6000}) - void testVerify_withOldTimestamp(long offset) { - long currentTime = Instant.now().getEpochSecond(); - String timestamp = String.valueOf(currentTime - offset); - assertFalse(verifier.verify(timestamp)); - } -}