diff --git a/pom.xml b/pom.xml index a5dd7ab6..0b27a4f7 100644 --- a/pom.xml +++ b/pom.xml @@ -200,6 +200,16 @@ commons-exec 1.3 + + com.fasterxml.jackson.core + jackson-databind + 2.15.3 + + + io.github.jopenlibs + vault-java-driver + 6.2.0 + diff --git a/src/main/java/org/onedatashare/transferservice/odstransferservice/OdsTransferService.java b/src/main/java/org/onedatashare/transferservice/odstransferservice/OdsTransferService.java index 0a890310..6f3903a6 100644 --- a/src/main/java/org/onedatashare/transferservice/odstransferservice/OdsTransferService.java +++ b/src/main/java/org/onedatashare/transferservice/odstransferservice/OdsTransferService.java @@ -1,5 +1,6 @@ package org.onedatashare.transferservice.odstransferservice; +import org.onedatashare.transferservice.odstransferservice.config.VaultConfiguration; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -13,6 +14,7 @@ public class OdsTransferService { public static void main(String[] args) { + VaultConfiguration.loadSecrets(); SpringApplication.run(OdsTransferService.class, args); } diff --git a/src/main/java/org/onedatashare/transferservice/odstransferservice/config/VaultConfiguration.java b/src/main/java/org/onedatashare/transferservice/odstransferservice/config/VaultConfiguration.java new file mode 100644 index 00000000..a42e4fb0 --- /dev/null +++ b/src/main/java/org/onedatashare/transferservice/odstransferservice/config/VaultConfiguration.java @@ -0,0 +1,107 @@ +package org.onedatashare.transferservice.odstransferservice.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultConfig; + +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + + +import org.onedatashare.transferservice.odstransferservice.service.AuthenticationService; + +public class VaultConfiguration { + private static Vault vaultServiceInstance; + private static String vaultServerAddress = System.getenv("VAULT_URI"); + private static String secretsPath = System.getenv("VAULT_SECRETS_PATH"); + private static String shouldLoadSecrets = System.getenv("VAULT_LOAD_SECRETS"); + private static String vaultTransferServiceUserRole = System.getenv("VAULT_TRANSFER_SERVICE_USER_ROLE"); + private VaultConfiguration() { + + } + + public static void loadSecrets() { + try { + + if (shouldLoadSecrets == null || !shouldLoadSecrets.equals("true")) { + return; + } + Vault vaultServiceInstance = getInstance(); + var secrets = vaultServiceInstance.logical().read(secretsPath).getData(); + for (Map.Entry kv : secrets.entrySet()) { + System.setProperty(kv.getKey(), kv.getValue()); + } + } catch (Exception e) { + System.out.println("Exception while loading secrets from vault:" + e.getMessage()); + } + } + + public static Vault getInstance() throws Exception { + + if (vaultServiceInstance == null) { + AuthenticationService authenticator = new AuthenticationService(); + Map authTokens = authenticator.startDeviceAuthentication(); + setOdsUserFromToken(authTokens.get("id_token").toString()); + return authenticateVaultWithIdToken(authTokens.get("id_token").toString()); + + } + return vaultServiceInstance; + } + + private static void setOdsUserFromToken(String idToken) throws Exception { + String payload = idToken.split("\\.")[1]; + String decodedPayload = new String(Base64.getDecoder().decode(payload)); + ObjectMapper objectMapper = new ObjectMapper(); + HashMap decodedPayloadMap = objectMapper.readValue(decodedPayload, HashMap.class); + System.setProperty("ods.user", decodedPayloadMap.get("email").toString()); + } + + private static Vault authenticateVaultWithIdToken(String idToken) throws Exception { + + + String jwtLoginUri = vaultServerAddress + "/v1/auth/jwt/login"; + HttpClient httpClient = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_2) + .build(); + ObjectMapper objectMapper = new ObjectMapper(); + Map vaultAuthPayload = new HashMap<>(); + vaultAuthPayload.put("jwt", idToken); + vaultAuthPayload.put("role", vaultTransferServiceUserRole); + HttpRequest vaultAuthrequest = HttpRequest.newBuilder() + .POST(buildFormDataFromMap(vaultAuthPayload)) + .uri(URI.create(jwtLoginUri)) + .setHeader("User-Agent", "Java HttpClient Bot") // add request header + .header("Content-Type", "application/x-www-form-urlencoded") + .build(); + + HttpResponse vaultAuthresponse = httpClient.send(vaultAuthrequest, HttpResponse.BodyHandlers.ofString()); + HashMap vaultAuthRespnseMap = objectMapper.readValue(vaultAuthresponse.body(), HashMap.class); + HashMap vaultAuthData = (HashMap) vaultAuthRespnseMap.get("auth"); + String vaultToken = vaultAuthData.get("client_token").toString(); + + // Configure the Vault client with the server address + VaultConfig config = new VaultConfig().address(vaultServerAddress).token(vaultToken).engineVersion(2).build(); + vaultServiceInstance = Vault.create(config); + return vaultServiceInstance; + } + + private static HttpRequest.BodyPublisher buildFormDataFromMap(Map data) { + StringBuilder builder = new StringBuilder(); + for (Map.Entry entry : data.entrySet()) { + if (builder.length() > 0) { + builder.append("&"); + } + builder.append(URLEncoder.encode(entry.getKey().toString(), StandardCharsets.UTF_8)); + builder.append("="); + builder.append(URLEncoder.encode(entry.getValue().toString(), StandardCharsets.UTF_8)); + } + return HttpRequest.BodyPublishers.ofString(builder.toString()); + } +} diff --git a/src/main/java/org/onedatashare/transferservice/odstransferservice/service/AuthenticationService.java b/src/main/java/org/onedatashare/transferservice/odstransferservice/service/AuthenticationService.java new file mode 100644 index 00000000..5a78b92f --- /dev/null +++ b/src/main/java/org/onedatashare/transferservice/odstransferservice/service/AuthenticationService.java @@ -0,0 +1,144 @@ +package org.onedatashare.transferservice.odstransferservice.service; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +public class AuthenticationService { + private static final String CLIENT_ID = System.getenv("OAUTH_CLIENT_ID"); + private static final String CLIENT_SECRET = System.getenv("OAUTH_CLIENT_SECRET"); + + public Map startDeviceAuthentication() throws Exception { + Map tokenResponsePayload = new HashMap<>(); + String SCOPE = "openid profile email org.cilogon.userinfo"; + String HOST = "cilogon.org"; + + String DEVICE_ENDPOINT = String.format("https://%s/oauth2/device_authorization", HOST); + String TOKEN_ENDPOINT = String.format("https://%s/oauth2/token", HOST); + + HttpClient httpClient = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_2) + .build(); + ObjectMapper objectMapper = new ObjectMapper(); + + //DEVICE REQUEST + Map deviceRequestPayload = new HashMap<>(); + deviceRequestPayload.put("client_id", CLIENT_ID); + deviceRequestPayload.put("client_secret", CLIENT_SECRET); + deviceRequestPayload.put("scope", SCOPE); + + HttpRequest deviceRequest = HttpRequest.newBuilder() + .POST(buildFormDataFromMap(deviceRequestPayload)) + .uri(URI.create(DEVICE_ENDPOINT)) + .setHeader("User-Agent", "Java HttpClient Bot") // add request header + .header("Content-Type", "application/x-www-form-urlencoded") + .build(); + HttpResponse deviceResponse = httpClient.send(deviceRequest, HttpResponse.BodyHandlers.ofString()); + Map deviceResponsePayload = objectMapper.readValue(deviceResponse.body(), HashMap.class); + + //DEVICE RESPONSE CHECK + if (deviceResponsePayload.containsKey("error")) { + String errorDescription = deviceResponsePayload.get("error_description").toString(); + if (errorDescription.length() > 0) { + System.out.format("Error description : %s", deviceResponsePayload.get("error_description")); + } + throw new Exception(errorDescription); + } + String DEVICE_CODE = deviceResponsePayload.get("device_code").toString(); + String USER_CODE = deviceResponsePayload.get("user_code").toString(); + String VERIFICATION_URI_COMPLETE = deviceResponsePayload.get("verification_uri_complete").toString(); + String VERIFICATION_URI = deviceResponsePayload.get("verification_uri").toString(); + int INTERVAL = 5; + if (deviceResponsePayload.containsKey("interval")) { + INTERVAL = Integer.parseInt(deviceResponsePayload.get("interval").toString()); + } + + boolean checks_failed = false; + if (DEVICE_CODE.length() == 0) { + System.out.println("Error: No device code found in response"); + checks_failed = true; + } + if (USER_CODE.length() == 0) { + System.out.println("Error: No user code found in response"); + checks_failed = true; + } + if (checks_failed) { + throw new Exception("device code or user code not found in response"); + } + + //USER MESSAGE + System.out.println("Device code successful"); + System.out.format("On your computer or mobile device navigate to: %s \n", VERIFICATION_URI_COMPLETE); + System.out.format("OR \n Navigate to %s and enter the following code: %s \n", VERIFICATION_URI, USER_CODE); + + + //TOKEN REQUEST + String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"; + Map tokenRequestPayload = new HashMap<>(); + tokenRequestPayload.put("client_id", CLIENT_ID); + tokenRequestPayload.put("client_secret", CLIENT_SECRET); + tokenRequestPayload.put("device_code", DEVICE_CODE); + tokenRequestPayload.put("grant_type", GRANT_TYPE); + + + boolean isAuthenticated = false; + + //WAIT FOR AUTH TOKENS + while (!isAuthenticated) { + System.out.println("Checking if user completed the flow"); + HttpRequest tokenRequest = HttpRequest.newBuilder() + .POST(buildFormDataFromMap(tokenRequestPayload)) + .uri(URI.create(TOKEN_ENDPOINT)) + .setHeader("User-Agent", "Java HttpClient") // add request header + .header("Content-Type", "application/x-www-form-urlencoded") + .build(); + HttpResponse tokenResponse = httpClient.send(tokenRequest, HttpResponse.BodyHandlers.ofString()); + tokenResponsePayload = objectMapper.readValue(tokenResponse.body(), HashMap.class); + if (tokenResponsePayload.containsKey("error")) { + String error = tokenResponsePayload.get("error").toString(); + String error_description = tokenResponsePayload.get("error_description").toString(); + System.out.println(error_description); + if (error.equals("authorization_pending") || error.equals("slow_down")) { + if (error.equals("slow_down")) { + INTERVAL += 5; + } + Thread.sleep(INTERVAL * 1000); + } else { + if (error_description.equals("no pending request")) { + System.out.println("Error: User denied user_code. \n Please begin a new device code request"); + } else { + System.out.format("Error %s \n", error_description); + if (error_description.equals("device code expired")) { + System.out.println("Please begin new device code request"); + } + } + throw new Exception(error_description); + } + } else { + isAuthenticated = true; + break; + } + } + return tokenResponsePayload; + } + + private HttpRequest.BodyPublisher buildFormDataFromMap(Map data) { + StringBuilder builder = new StringBuilder(); + for (Map.Entry entry : data.entrySet()) { + if (builder.length() > 0) { + builder.append("&"); + } + builder.append(URLEncoder.encode(entry.getKey().toString(), StandardCharsets.UTF_8)); + builder.append("="); + builder.append(URLEncoder.encode(entry.getValue().toString(), StandardCharsets.UTF_8)); + } + return HttpRequest.BodyPublishers.ofString(builder.toString()); + } +}