diff --git a/src/main/de/fitconnect/api/Sender.java b/src/main/de/fitconnect/api/Sender.java new file mode 100644 index 0000000000000000000000000000000000000000..6a51e282983a574d38671d21e23f10a4d7c921f1 --- /dev/null +++ b/src/main/de/fitconnect/api/Sender.java @@ -0,0 +1,52 @@ +package de.fitconnect.api; + +import de.fitconnect.api.auth.OAuthToken; +import de.fitconnect.api.data.Data; +import de.fitconnect.api.data.Metadata; +import de.fitconnect.api.validation.ValidationResult; + +import java.io.ByteArrayInputStream; + +/** + * A technical system that creates a submission via the Submission API. + */ +public interface Sender { + + /** + * Authenticates the sender against the FIT-Co API and retrieves an authentication token + * + * @param clientId - a unique identifier within the FIT-Co platform environment + * @param clientSecret - a secret that is only known by the application and the OAuth server + * @param scope - OAuth scope that determines if a submission is accepted by the client + * @return {@link OAuthToken} that holds the access-token + */ + OAuthToken retrieveAuthenticationToken(final String clientId, final String clientSecret, String scope); + + // TODO + ValidationResult validateCertificateChain(byte[] chain); + + /** + * Encrypts the submission data with JWE. + * + * @param unencryptedData - unencrypted JSON or XML data + * @return encrypted JSON or XML data + */ + Data encryptData(final Data unencryptedData); + + /** + * Encrypts the submission attachment with JWE. + * + * @param unencryptedAttachmentBinary - the unencrypted binary data of the attachment + * @return the encrypted binary of the attachment + */ + ByteArrayInputStream encryptAttachment(final ByteArrayInputStream unencryptedAttachmentBinary); + + /** + * Generates the metadata consisting of the attachment and its hash values. + * + * @param attachment - the unencrypted binary data of the attachment + * @return metadata object + */ + Metadata generateMetadata(byte[] attachment); + +} diff --git a/src/main/de/fitconnect/api/auth/OAuthService.java b/src/main/de/fitconnect/api/auth/OAuthService.java new file mode 100644 index 0000000000000000000000000000000000000000..5e45fd8d807c51cfb1662dadf2b2807f0fb64800 --- /dev/null +++ b/src/main/de/fitconnect/api/auth/OAuthService.java @@ -0,0 +1,8 @@ +package de.fitconnect.api.auth; + +import java.util.Optional; + +public interface OAuthService { + + Optional<OAuthToken> authenticate(String clientId, String clientSecret, String scope); +} diff --git a/src/main/de/fitconnect/client/FitCoAuthService.java b/src/main/de/fitconnect/client/FitCoAuthService.java new file mode 100644 index 0000000000000000000000000000000000000000..c99a1384b34eb18dfc0bb2ee9b490d3ad8d073ce --- /dev/null +++ b/src/main/de/fitconnect/client/FitCoAuthService.java @@ -0,0 +1,68 @@ +package de.fitconnect.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.fitconnect.api.auth.OAuthService; +import de.fitconnect.api.auth.OAuthToken; +import de.fitconnect.api.logger.SdkLogger; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.HashMap; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static java.util.stream.Collectors.joining; + +public class FitCoAuthService implements OAuthService { + + private static final Logger logger = SdkLogger.defaultLogger(FitCoAuthService.class); + private static final ObjectMapper mapper = new ObjectMapper(); + + private final HttpClient client; + private final String tokenUrl; + + public FitCoAuthService(final HttpClient client, final String tokenUrl) { + this.client = client; + this.tokenUrl = tokenUrl; + } + + @Override + public Optional<OAuthToken> authenticate(String clientId, String clientSecret, String scope) { + final String requestBody = buildRequestBody(clientId, clientSecret, scope); + try { + return Optional.of(performTokenRequest(requestBody)); + } catch (IOException | InterruptedException e) { + logger.log(Level.SEVERE, "error retrieving access token", e); + return Optional.empty(); + } + } + + private String buildRequestBody(String clientId, String clientSecret, String scope) { + var data = new HashMap<String, String>() {{ + put("grant_type", "client_credentials"); + put("client_id", clientId); + put("client_secret", clientSecret); + put("scope", scope); + }}; + + return data.entrySet() + .stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .collect(joining("&")); + } + + private OAuthToken performTokenRequest(final String requestBody) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .header("Accept", "application/json") + .header("Content-Type","application/x-www-form-urlencoded;charset=UTF-8") + .uri(URI.create(tokenUrl)) + .POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .build(); + var responseBody = client.send(request, HttpResponse.BodyHandlers.ofString()).body(); + return mapper.readValue(responseBody,OAuthToken.class); + } +} diff --git a/src/main/de/fitconnect/client/SubmissionSender.java b/src/main/de/fitconnect/client/SubmissionSender.java new file mode 100644 index 0000000000000000000000000000000000000000..095366384bffc4486a8ee79d2e846475b7238c20 --- /dev/null +++ b/src/main/de/fitconnect/client/SubmissionSender.java @@ -0,0 +1,44 @@ +package de.fitconnect.client; + +import de.fitconnect.api.Sender; +import de.fitconnect.api.auth.OAuthToken; +import de.fitconnect.api.auth.OAuthService; +import de.fitconnect.api.data.Data; +import de.fitconnect.api.data.Metadata; +import de.fitconnect.api.validation.ValidationResult; + +import java.io.ByteArrayInputStream; + +public class SubmissionSender implements Sender { + + private final OAuthService authService; + + public SubmissionSender(final OAuthService authService){ + this.authService = authService; + } + + @Override + public OAuthToken retrieveAuthenticationToken(String clientId, String clientSecret, String scope) { + return authService.authenticate(clientId, clientSecret, scope).orElseThrow(); + } + + @Override + public ValidationResult validateCertificateChain(byte[] chain) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public Data encryptData(Data unencryptedData) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public ByteArrayInputStream encryptAttachment(ByteArrayInputStream unencryptedAttachmentBinary) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public Metadata generateMetadata(byte[] attachment) { + throw new UnsupportedOperationException("Not yet implemented"); + } +} diff --git a/src/test/java/OAuthTokenIntegrationTest.java b/src/test/java/OAuthTokenIntegrationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a3ec6a691ecb7a09b0ca10d87e8166b731dff344 --- /dev/null +++ b/src/test/java/OAuthTokenIntegrationTest.java @@ -0,0 +1,33 @@ +import de.fitconnect.api.auth.OAuthToken; +import de.fitconnect.client.FitCoAuthService; +import de.fitconnect.client.SubmissionSender; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpClient; + +import static org.junit.jupiter.api.Assertions.*; + +class OAuthTokenIntegrationTest { + + @Test + void retrieveAuthenticationToken() { + + // Given + var tokenUrl = "https://auth-testing.fit-connect.fitko.dev/token"; + var clientId = "781f6213-0f0f-4a79-9372-e7187ffda98b"; + var secret = "PnzR8Vbmhpv_VwTkT34wponqXWK8WBm-LADlryYdV4o"; + var scope = "send:region:DE"; + + var httpClient = HttpClient.newHttpClient(); + var authService = new FitCoAuthService(httpClient, tokenUrl); + var sender = new SubmissionSender(authService); + + // When + OAuthToken token = sender.retrieveAuthenticationToken(clientId, secret, scope); + + // Then + assertNull(token.error()); + assertNotNull(token.access_token()); + assertEquals(1800, token.expires_in()); + } +} \ No newline at end of file