Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • fit-connect/sdk-java
1 result
Show changes
Commits on Source (42)
Showing
with 528 additions and 110 deletions
......@@ -14,4 +14,4 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip
package dev.fitko.fitconnect.api.config;
import com.nimbusds.jose.jwk.JWK;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
import static java.util.Objects.requireNonNull;
@Getter
@NoArgsConstructor
public class SubscriberConfig {
private String clientId;
private String clientSecret;
private String privateSigningKeyPath;
private List<String> privateDecryptionKeyPaths;
private SubscriberKeys subscriberKeys;
@Getter
@AllArgsConstructor
public static class SubscriberKeys {
private JWK privateSigningKey;
private List<JWK> privateDecryptionKeys;
}
public SubscriberConfig(String clientId, String clientSecret, String privateSigningKeyPath, List<String> privateDecryptionKeyPaths) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.privateSigningKeyPath = privateSigningKeyPath;
this.privateDecryptionKeyPaths = privateDecryptionKeyPaths;
}
private SubscriberConfig(String clientId, String clientSecret, SubscriberKeys subscriberKeys) {
requireNonNull(clientId, "clientId must not be null");
requireNonNull(clientSecret, "client secret must not be null");
requireNonNull(subscriberKeys, "subscriber keys must not be null");
this.clientId = clientId;
this.clientSecret = clientSecret;
this.subscriberKeys = subscriberKeys;
}
public static SubscriberConfigBuilder builder() {
return new SubscriberConfigBuilder();
}
public static class SubscriberConfigBuilder {
private String clientId;
private String clientSecret;
private JWK privateSigningKey;
private List<JWK> privateDecryptionKeys;
private String privateSigningKeyPath;
private List<String> privateDecryptionKeyPaths;
SubscriberConfigBuilder() {
}
/**
* The subscribers clientID.
*
* @param clientId clientId as string
* @return SubscriberConfigBuilder
*/
public SubscriberConfigBuilder clientId(String clientId) {
this.clientId = clientId;
return this;
}
/**
* The subscribers client secret.
*
* @param clientSecret secret as string
* @return SubscriberConfigBuilder
*/
public SubscriberConfigBuilder clientSecret(String clientSecret) {
this.clientSecret = clientSecret;
return this;
}
/**
* JWK of the private signature key.
*
* @param privateSigningKey the private signature key as {@link JWK}
* @return SubscriberConfigBuilder
* @see JWK
*/
public SubscriberConfigBuilder privateSigningKey(JWK privateSigningKey) {
this.privateSigningKey = privateSigningKey;
return this;
}
/**
* Path to the private signature key.
*
* @param privateSigningKeyPath signature key path
* @return SubscriberConfigBuilder
*/
public SubscriberConfigBuilder privateSigningKeyPath(String privateSigningKeyPath) {
this.privateSigningKeyPath = privateSigningKeyPath;
return this;
}
/**
* List of JWKs of private decryption keys.
*
* @param privateDecryptionKeys list of private decryption keys as {@link JWK}
* @return SubscriberConfigBuilder
* @see JWK
*/
public SubscriberConfigBuilder privateDecryptionKeys(List<JWK> privateDecryptionKeys) {
this.privateDecryptionKeys = privateDecryptionKeys;
return this;
}
/**
* List of paths to the private decryption keys.
*
* @param privateDecryptionKeyPaths list of strings with paths to the private decryption keys
* @return SubscriberConfigBuilder
*/
public SubscriberConfigBuilder privateDecryptionKeyPaths(List<String> privateDecryptionKeyPaths) {
this.privateDecryptionKeyPaths = privateDecryptionKeyPaths;
return this;
}
public SubscriberConfig build() {
if (privateSigningKey != null && privateDecryptionKeys != null) {
return new SubscriberConfig(clientId, clientSecret, new SubscriberKeys(privateSigningKey, privateDecryptionKeys));
} else {
return new SubscriberConfig(clientId, clientSecret, privateSigningKeyPath, privateDecryptionKeyPaths);
}
}
}
}
......@@ -21,14 +21,14 @@ public enum SchemaConfig {
SchemaConfig(final URI schemaUri) {
this.schemaUri = schemaUri;
this.filename = "";
filename = "";
}
public URI getSchemaUriForVersion(String version ){
public URI getSchemaUriForVersion(String version) {
return URI.create(schemaUri + version + filename);
}
public URI getSchemaUri(){
public URI getSchemaUri() {
return schemaUri;
}
}
package dev.fitko.fitconnect.api.config.http;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class ProxyAuth {
private String username;
private String password;
}
......@@ -20,11 +20,20 @@ public class ProxyConfig {
@Builder.Default
private Integer port = 0;
@Builder.Default
private ProxyAuth basicAuth = new ProxyAuth();
public boolean isProxySet() {
return (port != null && port > 0) && (host != null && !host.isBlank());
}
public boolean hasBasicAuthentication() {
return basicAuth.getUsername() != null && basicAuth.getPassword() != null;
}
public Proxy getHttpProxy() {
return isProxySet() ? new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, port)) : Proxy.NO_PROXY;
}
}
......@@ -10,8 +10,11 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.UUID;
public final class Attachment {
private UUID attachmentId;
private final byte[] data;
private final String fileName;
private final String description;
......@@ -116,6 +119,15 @@ public final class Attachment {
return new Attachment(content.getBytes(StandardCharsets.UTF_8), mimeType, fileName, description);
}
/**
* Get the attachment id.
*
* @return attachment id as UUID
*/
public UUID getAttachmentId() {
return attachmentId;
}
/**
* Get the attachment content as byte[].
*
......@@ -178,8 +190,16 @@ public final class Attachment {
this.description = description;
}
public Attachment(final UUID attachmentId, final byte[] data, final String mimeType, final String fileName, final String description) {
this.attachmentId = attachmentId;
this.data = data;
this.fileName = fileName != null ? getBaseNameFromPath(fileName) : null; // prevent maliciously injected filePaths
this.mimeType = mimeType;
this.description = description;
}
private static byte[] readBytesFromInputStream(final InputStream inputStream) {
try (final BufferedInputStream bis = new BufferedInputStream(inputStream)) {
try (BufferedInputStream bis = new BufferedInputStream(inputStream)) {
return bis.readAllBytes();
} catch (final IOException e) {
throw new FitConnectSenderException("Attachment could not be read from input-stream ", e);
......
......@@ -24,7 +24,7 @@ public class Metadata {
private String schema;
@Getter
private ContentStructure contentStructure;
private ContentStructure contentStructure = new ContentStructure();
@Getter
private List<AuthenticationInformation> authenticationInformation;
......
......@@ -2,14 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.metadata.payment;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import java.net.URI;
import java.util.Date;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
......@@ -18,68 +18,9 @@ public class PaymentInformation {
private URI transactionUrl;
private String transactionId;
private String transactionReference;
private Date transactionTimestamp;
private String transactionTimestamp;
private PaymentMethod paymentMethod;
private String paymentMethodDetail;
private PaymentStatus status;
private double grossAmount;
public static Builder builder() {
return new Builder();
}
public static class Builder {
private URI transactionUrl;
private @NonNull String transactionId;
private @NonNull String transactionReference;
private @NonNull Date transactionTimestamp;
private @NonNull PaymentMethod paymentMethod;
private String paymentMethodDetail;
private @NonNull PaymentStatus status;
private double grossAmount;
public Builder withTransactionUrl(final URI transactionUrl) {
this.transactionUrl = transactionUrl;
return this;
}
public Builder withTransactionId(final String transactionId) {
this.transactionId = transactionId;
return this;
}
public Builder withTransactionReference(final String transactionReference) {
this.transactionReference = transactionReference;
return this;
}
public Builder withTransactionTimestamp(final Date transactionTimestamp) {
this.transactionTimestamp = transactionTimestamp;
return this;
}
public Builder withPaymentMethod(final PaymentMethod paymentMethod) {
this.paymentMethod = paymentMethod;
return this;
}
public Builder withPaymentMethodDetail(final String paymentMethodDetail) {
this.paymentMethodDetail = paymentMethodDetail;
return this;
}
public Builder withStatus(final PaymentStatus status) {
this.status = status;
return this;
}
public Builder withGrossAmount(final double grossAmount) {
this.grossAmount = grossAmount;
return this;
}
public PaymentInformation build() {
return new PaymentInformation(transactionUrl, transactionId, transactionReference, transactionTimestamp, paymentMethod, paymentMethodDetail, status, grossAmount);
}
}
}
......@@ -109,14 +109,8 @@ public final class ClientFactory {
LOGGER.info("Initializing subscriber client ...");
final SubscriberConfig subscriberConfig = config.getSubscriberConfig();
final String decryptionKeyPath = getPrivateDecryptionKeyPathFromSubscriber(subscriberConfig);
LOGGER.info("Reading private decryption key from {}", decryptionKeyPath);
final String decryptionKey = RessourceLoadingUtils.readKeyFromPath(decryptionKeyPath);
final RSAKey rsaDecryptionKey = parseRSAKeyFromString(decryptionKey);
LOGGER.info("Reading private signing key from {} ", config.getSubscriberConfig().getPrivateSigningKeyPath());
final String signingKey = RessourceLoadingUtils.readKeyFromPath(config.getSubscriberConfig().getPrivateSigningKeyPath());
final RSAKey rsaSigningKey = parseRSAKeyFromString(signingKey);
final RSAKey rsaDecryptionKey = getDecryptionKeyFromConfig(subscriberConfig);
final RSAKey rsaSigningKey = getSignatureKeyFromConfig(subscriberConfig);
final FitConnectService fitConnectService = createFitConnectService(config, subscriberConfig.getClientId(), subscriberConfig.getClientSecret(), rsaSigningKey);
final ValidDataGuard dataGuard = new ValidDataGuard(fitConnectService);
......@@ -221,4 +215,22 @@ public final class ClientFactory {
}
return subscriberConfig.getPrivateDecryptionKeyPaths().get(0);
}
private static RSAKey getSignatureKeyFromConfig(SubscriberConfig config) {
LOGGER.info("Initialising private signature key");
if (config.getSubscriberKeys() != null) {
return config.getSubscriberKeys().getPrivateSigningKey().toRSAKey();
}
return parseRSAKeyFromString(RessourceLoadingUtils.readKeyFromPath(config.getPrivateSigningKeyPath()));
}
private static RSAKey getDecryptionKeyFromConfig(SubscriberConfig config) {
LOGGER.info("Initialising private decryption key");
if (config.getSubscriberKeys() != null) {
return config.getSubscriberKeys().getPrivateDecryptionKeys().get(0).toRSAKey();
}
final String decryptionKeyPath = getPrivateDecryptionKeyPathFromSubscriber(config);
final String decryptionKey = RessourceLoadingUtils.readKeyFromPath(decryptionKeyPath);
return parseRSAKeyFromString(decryptionKey);
}
}
......@@ -151,7 +151,7 @@ public class SubmissionReceiver {
private Attachment toApiAttachment(final AttachmentForValidation attachment) {
final ApiAttachment metadata = attachment.getAttachmentMetadata();
return Attachment.fromByteArray(attachment.getDecryptedData(), metadata.getMimeType(), metadata.getFilename(), metadata.getDescription());
return new Attachment(attachment.getAttachmentId(), attachment.getDecryptedData(), metadata.getMimeType(), metadata.getFilename(), metadata.getDescription());
}
private List<AttachmentForValidation> loadAttachments(final Submission submission, final Metadata metadata) {
......
......@@ -32,6 +32,7 @@ import dev.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema;
import dev.fitko.fitconnect.api.domain.model.submission.Submission;
import dev.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup;
import dev.fitko.fitconnect.api.domain.model.submission.SubmissionsForPickup;
import dev.fitko.fitconnect.api.domain.subscriber.ReceivedSubmission;
import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
import dev.fitko.fitconnect.api.exceptions.client.FitConnectSubscriberException;
import dev.fitko.fitconnect.api.exceptions.internal.AuthenticationTagsEmptyException;
......@@ -756,6 +757,66 @@ class SubscriberClientTest {
verify(subscriberMock, times(1)).rejectSubmission(EventPayload.forRejectEvent(submission, List.of(new MissingAttachment(attachment.getAttachmentId()))));
}
@Test
void testReceivedSubmissionHasAttachmentIds() throws Exception {
// Given
final var dataPayload = "{ some : 'data foo' }";
final var submissionId = UUID.randomUUID();
final CryptoService cryptoService = new JWECryptoService(new HashService());
final Hash hash = new Hash();
hash.setContent(cryptoService.hashBytes(dataPayload.getBytes()));
hash.setSignatureType(SignatureType.SHA_512);
final SubmissionSchema schema = new SubmissionSchema();
schema.setSchemaUri(URI.create("https://dummy.schema.url"));
schema.setMimeType(MimeType.APPLICATION_JSON);
final Data data = new Data();
data.setSubmissionSchema(schema);
data.setHash(hash);
final ApiAttachment attachment = new ApiAttachment();
attachment.setHash(new Hash());
attachment.setAttachmentId(UUID.randomUUID());
final ContentStructure contentStructure = new ContentStructure();
contentStructure.setAttachments(List.of(attachment));
contentStructure.setData(data);
final Metadata metadata = new Metadata();
metadata.setContentStructure(contentStructure);
final byte[] metadataBytes = MAPPER.writeValueAsBytes(metadata);
final RSAKey encryptionKey = privateKey;
final RSAKey decryptionKey = encryptionKey.toPublicJWK();
final String encryptedMetadata = cryptoService.encryptBytes(decryptionKey, metadataBytes, "application/json");
final var submission = new Submission();
submission.setSubmissionId(submissionId);
final AuthenticationTags authenticationTags = new AuthenticationTags();
authenticationTags.setMetadata(encryptedMetadata.split(EventLogUtil.AUTH_TAG_SPLIT_TOKEN)[4]);
when(subscriberMock.getSubmission(any())).thenReturn(submission);
when(subscriberMock.validateSubmissionMetadata(metadata, submission, authenticationTags)).thenReturn(ValidationResult.ok());
when(subscriberMock.validateData(any(), any(), any(), any())).thenReturn(ValidationResult.ok());
when(subscriberMock.decryptString(encryptionKey, submission.getEncryptedData())).thenReturn(dataPayload.getBytes());
when(subscriberMock.decryptString(encryptionKey, submission.getEncryptedMetadata())).thenReturn(metadataBytes);
when(subscriberMock.getSubmissionAuthenticationTags(submission)).thenReturn(authenticationTags);
when(subscriberMock.getSubmissionAttachment(any(), any())).thenReturn(cryptoService.encryptObject(encryptionKey, attachment, "application/text"));
when(subscriberMock.validateAttachments(any(), any())).thenReturn(ValidationResult.ok());
// When
final ReceivedSubmission receivedSubmission = underTest.requestSubmission(submissionId);
// Then
assertThat(receivedSubmission.getAttachments().get(0).getAttachmentId(), is(attachment.getAttachmentId()));
}
@Test
void testAttachmentEncryptionIssue() throws Exception {
......
package dev.fitko.fitconnect.client.bootstrap;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import dev.fitko.fitconnect.api.config.ApplicationConfig;
import dev.fitko.fitconnect.api.config.Environment;
import dev.fitko.fitconnect.api.config.EnvironmentName;
......@@ -96,6 +99,35 @@ class ApplicationConfigLoaderTest {
assertThat(config.getSubmissionDataSchemas().size(), is(0));
}
@Test
void testLoadConfigViaConfigBuilderWithSubscriberJWKs() throws JOSEException {
// Given
final RSAKeyGenerator rsaKeyGenerator = new RSAKeyGenerator(2048);
final JWK signingKey = rsaKeyGenerator.generate().toPublicJWK();
final JWK decryptionKey = rsaKeyGenerator.generate().toPublicJWK();
var subscriberConfig = SubscriberConfig.builder()
.clientId("2")
.clientSecret("123")
.privateDecryptionKeys(List.of(decryptionKey))
.privateSigningKey(signingKey)
.build();
// When
final ApplicationConfig config = ApplicationConfig.builder()
.subscriberConfig(subscriberConfig)
.build();
// Then
assertNotNull(config);
assertThat(config.getSubscriberConfig(), equalTo(subscriberConfig));
assertThat(config.getSubscriberConfig().getSubscriberKeys().getPrivateSigningKey(), is(signingKey));
assertThat(config.getSubscriberConfig().getSubscriberKeys().getPrivateDecryptionKeys(), is(List.of(decryptionKey)));
}
@Test
void testLoadTestConfigFromString() throws IOException {
......@@ -120,7 +152,7 @@ class ApplicationConfigLoaderTest {
// Then
assertNotNull(testConfig);
assertThat(testConfig.getCurrentEnvironment(), equalTo(devEnv));
assertThat(testConfig.getSenderConfig(), equalTo(senderConfig));
assertThat(testConfig.getSenderConfig(), is(senderConfig));
}
@Test
......@@ -193,6 +225,11 @@ class ApplicationConfigLoaderTest {
assertTrue(config.getHttpConfig().getProxyConfig().isProxySet());
assertThat(config.getHttpConfig().getProxyConfig().getHost(), is("https://proxy.test.net"));
assertThat(config.getHttpConfig().getProxyConfig().getPort(), is(8080));
assertTrue(config.getHttpConfig().getProxyConfig().hasBasicAuthentication());
assertThat(config.getHttpConfig().getProxyConfig().getBasicAuth().getUsername(), is("user"));
assertThat(config.getHttpConfig().getProxyConfig().getBasicAuth().getPassword(), is("secret"));
assertThat(config.getHttpConfig().getTimeouts().getReadTimeoutInSeconds(), is(Duration.ofSeconds(60)));
assertThat(config.getHttpConfig().getTimeouts().getWriteTimeoutInSeconds(), is(Duration.ofSeconds(60)));
assertThat(config.getHttpConfig().getTimeouts().getConnectionTimeoutInSeconds(), is(DEFAULT_TIMEOUT));
......
package dev.fitko.fitconnect.client.bootstrap;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import dev.fitko.fitconnect.api.config.ApplicationConfig;
import dev.fitko.fitconnect.api.config.Environment;
import dev.fitko.fitconnect.api.config.EnvironmentName;
......@@ -36,7 +39,10 @@ class ClientFactoryTest {
final var sender = new SenderConfig("123", "abc");
final HttpConfig httpConfig = HttpConfig.builder()
.proxyConfig(new ProxyConfig("https://proxy.test.de", 8080))
.proxyConfig(ProxyConfig.builder()
.host("https://proxy.test.de")
.port(8080)
.build())
.build();
final var senderConfig = ApplicationConfig.builder()
......@@ -56,14 +62,51 @@ class ClientFactoryTest {
final var environments = Map.of(envName, new Environment("https://auth", "", List.of("https://submission.base.net"), "", true, false, false));
final var subscriber = SubscriberConfig.builder()
.clientSecret("123")
.clientId("123")
.clientSecret("abc")
.privateDecryptionKeyPaths(List.of("src/test/resources/private_decryption_test_key.json"))
.privateSigningKeyPath("src/test/resources/private_test_signing_key.json")
.build();
final HttpConfig httpConfig = HttpConfig.builder()
.proxyConfig(new ProxyConfig("https://proxy.test.de", 8080))
.proxyConfig(ProxyConfig.builder()
.host("https://proxy.test.de")
.port(8080)
.build())
.build();
final var subscriberConfig = ApplicationConfig.builder()
.httpConfig(httpConfig)
.environments(environments)
.subscriberConfig(subscriber)
.activeEnvironment(envName)
.build();
assertNotNull(ClientFactory.createSubscriberClient(subscriberConfig));
}
@Test
void testSubscriberClientConstructionWithJWKs() throws JOSEException {
final var envName = new EnvironmentName("DEV");
final var environments = Map.of(envName, new Environment("https://auth", "", List.of("https://submission.base.net"), "", true, false, false));
final RSAKeyGenerator rsaKeyGenerator = new RSAKeyGenerator(2048);
final JWK signingKey = rsaKeyGenerator.generate().toPublicJWK();
final JWK decryptionKey = rsaKeyGenerator.generate().toPublicJWK();
final var subscriber = SubscriberConfig.builder()
.clientId("123")
.clientSecret("abc")
.privateDecryptionKeys(List.of(decryptionKey))
.privateSigningKey(signingKey)
.build();
final HttpConfig httpConfig = HttpConfig.builder()
.proxyConfig(ProxyConfig.builder()
.host("https://proxy.test.de")
.port(8080)
.build())
.build();
final var subscriberConfig = ApplicationConfig.builder()
......@@ -100,7 +143,7 @@ class ClientFactoryTest {
final var environments = Map.of(envName, new Environment("https://auth", "", List.of(), "", true, false, false));
final var subscriberConfig = SubscriberConfig.builder()
.clientSecret("123")
.clientId("123")
.clientSecret("abc")
.privateDecryptionKeyPaths(List.of("src/test/resources/private_decryption_test_key.json"))
.privateSigningKeyPath("src/test/resources/invalid_signing_key.json")
......@@ -126,7 +169,7 @@ class ClientFactoryTest {
final var environments = Map.of(envName, new Environment("https://auth", "", List.of(), "", true, false, false));
final var subscriberConfigWithoutKey = SubscriberConfig.builder()
.clientSecret("123")
.clientId("123")
.clientSecret("abc")
.privateDecryptionKeyPaths(List.of("src/test/resources/invalid_private_key.json"))
.privateSigningKeyPath("src/test/resources/private_test_signing_key.json")
......@@ -150,7 +193,7 @@ class ClientFactoryTest {
final var environments = Map.of(envName, new Environment("https://auth", "", List.of(), "", true, false, false));
final var subscriberConfigWithoutKey = SubscriberConfig.builder()
.clientSecret("123")
.clientId("123")
.clientSecret("abc")
.privateDecryptionKeyPaths(List.of("src/test/resources/private_decryption_test_key.json", "src/test/resources/private_decryption_test_key.json"))
.privateSigningKeyPath("src/test/resources/private_test_signing_key.json")
......
......@@ -17,3 +17,6 @@ httpConfig:
proxyConfig:
host: "https://proxy.test.net"
port: 8080
basicAuth:
username: "user"
password: "secret"
......@@ -69,3 +69,6 @@ activeEnvironment: TEST
# proxyConfig:
# host: "https://proxy.test.net"
# port: 8080
# basicAuth:
# username: "username"
# password: "password"
......@@ -74,6 +74,11 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
......
......@@ -4,9 +4,12 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.fitko.fitconnect.api.config.http.HttpConfig;
import dev.fitko.fitconnect.api.config.http.ProxyAuth;
import dev.fitko.fitconnect.api.config.http.ProxyConfig;
import dev.fitko.fitconnect.api.config.http.Timeouts;
import dev.fitko.fitconnect.api.exceptions.internal.RestApiException;
import okhttp3.Authenticator;
import okhttp3.Credentials;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.MediaType;
......@@ -45,15 +48,29 @@ public class HttpClient {
}
private static void setProxy(ProxyConfig proxyConfig, OkHttpClient.Builder builder) {
if (proxyConfig.isProxySet()) {
final Proxy proxy = proxyConfig.getHttpProxy();
LOGGER.info("Creating HttpClient with proxy configuration: {}", proxy);
builder.proxy(proxy);
} else {
if (!proxyConfig.isProxySet()) {
LOGGER.info("Creating HttpClient without proxy configuration.");
return;
}
final Proxy proxy = proxyConfig.getHttpProxy();
LOGGER.info("Creating HttpClient with proxy configuration: {}", proxy);
builder.proxy(proxy);
if (proxyConfig.hasBasicAuthentication()) {
LOGGER.info("Creating proxy with basic authentication");
final Authenticator proxyAuthenticator = getProxyAuthenticator(proxyConfig.getBasicAuth());
builder.proxyAuthenticator(proxyAuthenticator);
}
}
private static Authenticator getProxyAuthenticator(ProxyAuth auth) {
return (route, response) -> {
String credential = Credentials.basic(auth.getUsername(), auth.getPassword());
return response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build();
};
}
private static void setInterceptors(List<Interceptor> interceptors, OkHttpClient.Builder builder) {
interceptors.forEach(builder::addInterceptor);
}
......
......@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import dev.fitko.fitconnect.api.config.http.HttpConfig;
import dev.fitko.fitconnect.api.config.http.ProxyAuth;
import dev.fitko.fitconnect.api.config.http.ProxyConfig;
import dev.fitko.fitconnect.api.config.http.Timeouts;
import dev.fitko.fitconnect.api.config.resources.BuildInfo;
......@@ -13,6 +14,10 @@ import dev.fitko.fitconnect.api.domain.model.metadata.SignatureType;
import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType;
import dev.fitko.fitconnect.api.exceptions.internal.RestApiException;
import dev.fitko.fitconnect.core.http.interceptors.UserAgentInterceptor;
import mockwebserver3.Dispatcher;
import mockwebserver3.MockResponse;
import mockwebserver3.MockWebServer;
import mockwebserver3.RecordedRequest;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
......@@ -23,6 +28,8 @@ import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Map;
......@@ -38,9 +45,11 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
......@@ -48,6 +57,7 @@ public class HttpClientTest {
private final WireMockServer wireMockServer = new WireMockServer();
@BeforeEach
public void startWireMock() {
wireMockServer.start();
......@@ -295,7 +305,7 @@ public class HttpClientTest {
// create timeout config that bypasses the endpoint delay of 2 seconds
final HttpConfig httpConfig = HttpConfig.builder()
.timeouts(new Timeouts(5,5,5))
.timeouts(new Timeouts(5, 5, 5))
.build();
// When
......@@ -307,6 +317,66 @@ public class HttpClientTest {
assertThat(httpResponse.getStatusCode(), is(200));
}
@Test
public void testProxyConfigurationWithBasicAuth() throws IOException {
// Given
WireMock.configureFor("localhost", 8080);
final MockWebServer proxyMock = getProxyMock(8081);
final HttpConfig httpConfig = HttpConfig.builder()
.proxyConfig(ProxyConfig.builder()
.host("localhost")
.port(8081)
.basicAuth(new ProxyAuth("user", "secret"))
.build())
.build();
String basicAuth = "Basic " + Base64.getEncoder().encodeToString("user:secret".getBytes(StandardCharsets.UTF_8));
final Map<String, String> headers = Map.of("Proxy-Authorization", basicAuth);
final HttpClient httpClient = new HttpClient(httpConfig, List.of());
// When
final HttpResponse<String> httpResponse = httpClient.get("http://localhost:8080/test", headers, String.class);
// Then
assertThat(httpResponse.getStatusCode(), is(200));
assertThat(httpResponse.getBody(), containsString("hi there, authenticated proxy here"));
proxyMock.shutdown();
}
@Test
public void testProxyConfigurationWithInvalidBasicAuthCredentials() throws IOException {
// Given
WireMock.configureFor("localhost", 8080);
final MockWebServer mockProxy = getProxyMock(8081);
final HttpConfig httpConfig = HttpConfig.builder()
.proxyConfig(ProxyConfig.builder()
.host("localhost")
.port(8081)
.basicAuth(new ProxyAuth("user", "secret"))
.build())
.build();
String basicAuth = "Basic " + Base64.getEncoder().encodeToString("user:WRONG_PASS".getBytes(StandardCharsets.UTF_8));
final Map<String, String> headers = Map.of("Proxy-Authorization", basicAuth);
final HttpClient httpClient = new HttpClient(httpConfig, List.of());
// When
final RestApiException exception = assertThrows(RestApiException.class, () -> httpClient.get("http://localhost:8080/test", headers, String.class));
// Then
assertThat(exception.getStatusCode(), is(401));
assertThat(exception.getMessage(), containsString("not authorised, wrong credentials"));
mockProxy.shutdown();
}
@Test
public void exceptionHasResponseBodyAsMessage() {
......@@ -340,4 +410,27 @@ public class HttpClientTest {
)
);
}
private static MockWebServer getProxyMock(int port) throws IOException {
MockWebServer mockProxy = new MockWebServer();
mockProxy.setDispatcher(new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest recordedRequest) {
final String basicAuthHeader = recordedRequest.getHeaders().get("Proxy-Authorization");
var decoded = new String(Base64.getDecoder().decode(basicAuthHeader.replace("Basic ", "")));
if (decoded.equals("user:secret")) {
return new MockResponse().newBuilder()
.body("hi there, authenticated proxy here")
.code(200)
.build();
}
return new MockResponse().newBuilder()
.body("not authorised, wrong credentials")
.code(401)
.build();
}
});
mockProxy.start(port);
return mockProxy;
}
}
......@@ -40,6 +40,9 @@ import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Purpose;
import dev.fitko.fitconnect.api.domain.model.metadata.data.Data;
import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType;
import dev.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema;
import dev.fitko.fitconnect.api.domain.model.metadata.payment.PaymentInformation;
import dev.fitko.fitconnect.api.domain.model.metadata.payment.PaymentMethod;
import dev.fitko.fitconnect.api.domain.model.metadata.payment.PaymentStatus;
import dev.fitko.fitconnect.api.domain.model.reply.replychannel.DeMail;
import dev.fitko.fitconnect.api.domain.model.reply.replychannel.Email;
import dev.fitko.fitconnect.api.domain.model.reply.replychannel.EncryptionPublicKey;
......@@ -58,6 +61,7 @@ import dev.fitko.fitconnect.core.schema.SchemaResourceProvider;
import dev.fitko.fitconnect.jwkvalidator.exceptions.JWKValidationException;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.io.IOException;
......@@ -1239,7 +1243,7 @@ class DefaultValidationServiceTest {
}
@Test
void testDataJsonSyntaxViolation() throws IOException {
void testDataJsonSyntaxViolation() {
// Given
final SchemaProvider schemaProvider = mock(SchemaProvider.class);
......@@ -1605,17 +1609,18 @@ class DefaultValidationServiceTest {
}
@Test
@Disabled("certificate expired, waiting for new one")
public void fitConnectTestCertificateIsValidAccordingToTestRootCertificates() throws ParseException {
// Given
var config = getApplicationConfig(false, false);
var trustedRootCert = getResourceAsString("/trusted-test-root-certificates/TEST-PCA20.pem");
var defaultValidationService = new DefaultValidationService(config, hashService, schemaProvider, List.of(trustedRootCert));
final var config = getApplicationConfig(false, false);
final var trustedRootCert = getResourceAsString("/trusted-test-root-certificates/TEST-PCA20.pem");
final var defaultValidationService = new DefaultValidationService(config, hashService, schemaProvider, List.of(trustedRootCert));
var rsaKey = RSAKey.parse(getResourceAsString("/certificates/grp-fitko-testzertifikat-fit-connect-1.json"));
final var rsaKey = RSAKey.parse(getResourceAsString("/certificates/grp-fitko-testzertifikat-fit-connect-1.json"));
// When
var validationResult = defaultValidationService.validatePublicKey(rsaKey, KeyOperation.WRAP_KEY);
final var validationResult = defaultValidationService.validatePublicKey(rsaKey, KeyOperation.WRAP_KEY);
// Then
assertTrue(validationResult.isValid());
......@@ -1623,17 +1628,18 @@ class DefaultValidationServiceTest {
}
@Test
@Disabled("certificate expired, waiting for new one")
public void revokedFitConnectTestCertificateIsInvalidAccordingToTestRootCertificates() throws ParseException {
// Given
var config = getApplicationConfig(false, false);
var trustedRootCert = getResourceAsString("/trusted-test-root-certificates/TEST-PCA20.pem");
var defaultValidationService = new DefaultValidationService(config, hashService, schemaProvider, List.of(trustedRootCert));
final var config = getApplicationConfig(false, false);
final var trustedRootCert = getResourceAsString("/trusted-test-root-certificates/TEST-PCA20.pem");
final var defaultValidationService = new DefaultValidationService(config, hashService, schemaProvider, List.of(trustedRootCert));
var rsaKey = RSAKey.parse(getResourceAsString("/certificates/grp-fitko-testzertifikat-fit-connect-2.json"));
final var rsaKey = RSAKey.parse(getResourceAsString("/certificates/grp-fitko-testzertifikat-fit-connect-2.json"));
// When
var validationResult = defaultValidationService.validatePublicKey(rsaKey, KeyOperation.WRAP_KEY);
final var validationResult = defaultValidationService.validatePublicKey(rsaKey, KeyOperation.WRAP_KEY);
//Then
assertTrue(validationResult.hasError());
......@@ -1657,6 +1663,30 @@ class DefaultValidationServiceTest {
assertThat(validationResult.getError().getMessage(), is("JWK with id LM0FPR9i-Yg1Cks-f_HG4dHocwLc2MU3rigME-Uc1Lc-wrapKey has invalid certificate chain"));
}
@Test
public void testPaymentTimestampValidation(){
// Given
final var paymentInformation = PaymentInformation.builder()
.paymentMethod(PaymentMethod.OTHER)
.paymentMethodDetail("Zahlart-andere")
.transactionReference("Vorgang xyz")
.transactionId("DUMMY1714983664004")
.grossAmount(25.0)
.status(PaymentStatus.BOOKED)
.transactionTimestamp("2024-05-06T10:21:04+02:00")
.build();
final var metadata = new Metadata();
metadata.setPaymentInformation(paymentInformation);
// When
final ValidationResult validationResult = underTest.validateMetadataSchema(metadata);
// Then
assertTrue(validationResult.isValid());
}
private String getResourceAsString(final String filename) throws RuntimeException {
return new String(getResourceAsByteArray(filename));
}
......@@ -1664,7 +1694,7 @@ class DefaultValidationServiceTest {
private byte[] getResourceAsByteArray(final String filename) throws RuntimeException {
try {
return DefaultValidationServiceTest.class.getResourceAsStream(filename).readAllBytes();
} catch (Exception e) {
} catch (final Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}
......
package dev.fitko.fitconnect.integrationtests;
import com.nimbusds.jose.jwk.JWK;
import dev.fitko.fitconnect.api.config.ApplicationConfig;
import dev.fitko.fitconnect.api.config.Environment;
import dev.fitko.fitconnect.api.config.EnvironmentName;
......@@ -11,6 +12,7 @@ import dev.fitko.fitconnect.client.SubscriberClient;
import dev.fitko.fitconnect.client.bootstrap.ClientFactory;
import java.io.IOException;
import java.text.ParseException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
......@@ -47,11 +49,14 @@ public class IntegrationTestBase {
final var sender = new SenderConfig(System.getenv("SENDER_CLIENT_ID"), System.getenv("SENDER_CLIENT_SECRET"));
var signingKey = getKeyAsJWK("/private_test_signing_key.json");
var decryptionKey = getKeyAsJWK("/private_decryption_test_key.json");
final var subscriber = SubscriberConfig.builder()
.clientId(System.getenv("SUBSCRIBER_CLIENT_ID"))
.clientSecret(System.getenv("SUBSCRIBER_CLIENT_SECRET"))
.privateDecryptionKeyPaths(List.of("src/test/resources/private_decryption_test_key.json"))
.privateSigningKeyPath("src/test/resources/private_test_signing_key.json")
.privateDecryptionKeys(List.of(decryptionKey, decryptionKey))
.privateSigningKey(signingKey)
.build();
final EnvironmentName envName = Environments.TEST.getEnvironmentName();
......@@ -67,7 +72,15 @@ public class IntegrationTestBase {
.build();
}
String getResourceAsString(final String filename) throws IOException {
private static JWK getKeyAsJWK(String key) {
try {
return JWK.parse(getResourceAsString(key));
} catch (ParseException | IOException e) {
throw new RuntimeException(e);
}
}
static String getResourceAsString(final String filename) throws IOException {
return new String(IntegrationTestBase.class.getResourceAsStream(filename).readAllBytes());
}
}