diff --git a/pom.xml b/pom.xml
index 655462e..646c55b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -55,7 +55,11 @@
1.8.3
-
+
+ com.fasterxml.jackson.core
+ jackson-core
+ 2.17.1
+
com.fasterxml.jackson.core
jackson-databind
@@ -70,7 +74,7 @@
org.projectlombok
lombok
- 1.18.30
+ 1.18.42
provided
@@ -119,6 +123,13 @@
${gson-fire-version}
+
+
+ software.amazon.awssdk
+ auth
+ 2.34.8
+ true
+
org.junit.jupiter
@@ -126,9 +137,14 @@
5.9.1
test
+
+ org.junit.jupiter
+ junit-jupiter-params
+ 5.9.1
+ test
+
-
@@ -171,7 +187,7 @@
org.projectlombok
lombok
- 1.18.30
+ 1.18.42
diff --git a/src/main/java/com/infisical/sdk/auth/AwsAuthProvider.java b/src/main/java/com/infisical/sdk/auth/AwsAuthProvider.java
new file mode 100644
index 0000000..aa6a337
--- /dev/null
+++ b/src/main/java/com/infisical/sdk/auth/AwsAuthProvider.java
@@ -0,0 +1,159 @@
+package com.infisical.sdk.auth;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.infisical.sdk.models.AwsAuthParameters;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NonNull;
+import software.amazon.awssdk.auth.credentials.AwsCredentials;
+import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
+import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider;
+import software.amazon.awssdk.http.ContentStreamProvider;
+import software.amazon.awssdk.http.SdkHttpFullRequest;
+import software.amazon.awssdk.http.SdkHttpMethod;
+import software.amazon.awssdk.http.SdkHttpRequest;
+import software.amazon.awssdk.http.auth.aws.signer.AwsV4FamilyHttpSigner;
+import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner;
+import software.amazon.awssdk.http.auth.spi.signer.HttpSigner;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain;
+
+@Data
+@Builder
+public class AwsAuthProvider {
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ @NonNull @Builder.Default private final String serviceName = "sts";
+ @NonNull @Builder.Default private final SdkHttpMethod httpMethod = SdkHttpMethod.POST;
+ @NonNull @Builder.Default private final String endpointTemplate = "https://sts.%s.amazonaws.com";
+
+ @NonNull @Builder.Default
+ private final String contentType = "application/x-www-form-urlencoded; charset=utf-8";
+
+ @NonNull @Builder.Default
+ private final Map> params =
+ Map.ofEntries(
+ Map.entry("Action", List.of("GetCallerIdentity")),
+ Map.entry("Version", List.of("2011-06-15")));
+
+ private final Instant overrideInstant;
+
+ /**
+ * Create AwsAuthLoginInput from given AWS credentials.
+ *
+ * @param region region of AWS identity
+ * @param credentials AWS credentials for creating the login input
+ * @param sessionToken Session token for creating the login input
+ * @return the AwsAuthLoginInput created from the given credentials for exchanging access token
+ */
+ public AwsAuthParameters fromCredentials(
+ String region, AwsCredentials credentials, String sessionToken) {
+ final AwsV4HttpSigner signer = AwsV4HttpSigner.create();
+ final String iamRequestURL = endpointTemplate.formatted(region);
+ final String iamRequestBody = encodeParameters(params);
+ final SdkHttpFullRequest.Builder requestBuilder =
+ SdkHttpFullRequest.builder()
+ .uri(URI.create(iamRequestURL))
+ .method(httpMethod)
+ .appendHeader("Content-Type", contentType);
+ if (sessionToken != null) {
+ requestBuilder.appendHeader("X-Amz-Security-Token", sessionToken);
+ }
+ final SdkHttpFullRequest request = requestBuilder.build();
+ final SdkHttpRequest signedRequest =
+ signer
+ .sign(
+ signingRequest -> {
+ var req =
+ signingRequest
+ .request(request)
+ .identity(credentials)
+ .payload(
+ ContentStreamProvider.fromByteArray(
+ iamRequestBody.getBytes(StandardCharsets.UTF_8)))
+ .putProperty(AwsV4FamilyHttpSigner.SERVICE_SIGNING_NAME, serviceName)
+ .putProperty(AwsV4HttpSigner.REGION_NAME, region);
+ if (overrideInstant != null) {
+ req.putProperty(
+ HttpSigner.SIGNING_CLOCK, Clock.fixed(overrideInstant, ZoneOffset.UTC));
+ }
+ })
+ .request();
+ final Map requestHeaders =
+ signedRequest.headers().entrySet().stream()
+ .map(entry -> Map.entry(entry.getKey(), entry.getValue().getFirst()))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ requestHeaders.put("Content-Length", String.valueOf(iamRequestBody.length()));
+ final String encodedHeader;
+ try {
+ encodedHeader =
+ Base64.getEncoder()
+ .encodeToString(
+ objectMapper.writeValueAsString(requestHeaders).getBytes(StandardCharsets.UTF_8));
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ final String encodedBody =
+ Base64.getEncoder().encodeToString(iamRequestBody.getBytes(StandardCharsets.UTF_8));
+ return AwsAuthParameters.builder()
+ .iamHttpRequestMethod(httpMethod.name())
+ .iamRequestHeaders(encodedHeader)
+ .iamRequestBody(encodedBody)
+ .build();
+ }
+
+ /**
+ * Create AwsAuthLoginInput from the instance profile in the current environment.
+ *
+ * @return the AwsAuthLoginInput created from the current instance profile for exchanging access
+ * token
+ */
+ public AwsAuthParameters fromInstanceProfile() {
+ try (InstanceProfileCredentialsProvider provider =
+ InstanceProfileCredentialsProvider.create()) {
+ final AwsSessionCredentials credentials =
+ (AwsSessionCredentials) provider.resolveCredentials();
+ final DefaultAwsRegionProviderChain regionProvider =
+ DefaultAwsRegionProviderChain.builder().build();
+ final Region region = regionProvider.getRegion();
+ final String sessionToken = credentials.sessionToken();
+ return fromCredentials(region.id(), credentials, sessionToken);
+ }
+ }
+
+ /**
+ * Encode given parameters with URL encoding for the body of form posting request.
+ *
+ * @param params parameters mapping key to values to encode
+ * @return URL-encoded string of the parameters
+ */
+ public static String encodeParameters(Map> params) {
+ return params.entrySet().stream()
+ .flatMap(entry -> entry.getValue().stream().map(item -> Map.entry(entry.getKey(), item)))
+ // Notice: this is not really needed for real world usage, but it makes the
+ // body encoded in a deterministic order, so that unit test is much easier
+ .sorted(Map.Entry.comparingByKey())
+ .map(
+ entry ->
+ String.format(
+ "%s=%s",
+ URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8),
+ URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)))
+ .collect(Collectors.joining("&"));
+ }
+
+ public static AwsAuthProvider defaultProvider() {
+ return builder().build();
+ }
+}
diff --git a/src/main/java/com/infisical/sdk/models/AwsAuthLoginInput.java b/src/main/java/com/infisical/sdk/models/AwsAuthLoginInput.java
new file mode 100644
index 0000000..60401f9
--- /dev/null
+++ b/src/main/java/com/infisical/sdk/models/AwsAuthLoginInput.java
@@ -0,0 +1,34 @@
+package com.infisical.sdk.models;
+
+import com.infisical.sdk.util.Helper;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NonNull;
+
+@Data
+@Builder(toBuilder = true)
+public class AwsAuthLoginInput {
+ @NonNull private final String identityId;
+ @NonNull private final String iamHttpRequestMethod;
+ @NonNull private final String iamRequestHeaders;
+ @NonNull private final String iamRequestBody;
+
+ public String validate() {
+ if (Helper.isNullOrEmpty(identityId)) {
+ return "Identity ID is required";
+ }
+
+ if (Helper.isNullOrEmpty(iamHttpRequestMethod)) {
+ return "IamHttpRequestMethod is required";
+ }
+
+ if (Helper.isNullOrEmpty(iamRequestHeaders)) {
+ return "IamRequestHeaders is required";
+ }
+
+ if (Helper.isNullOrEmpty(iamRequestBody)) {
+ return "IamRequestBody is required";
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/infisical/sdk/models/AwsAuthParameters.java b/src/main/java/com/infisical/sdk/models/AwsAuthParameters.java
new file mode 100644
index 0000000..5b0a511
--- /dev/null
+++ b/src/main/java/com/infisical/sdk/models/AwsAuthParameters.java
@@ -0,0 +1,22 @@
+package com.infisical.sdk.models;
+
+import lombok.Builder;
+import lombok.Data;
+import lombok.NonNull;
+
+@Data
+@Builder(toBuilder = true)
+public class AwsAuthParameters {
+ @NonNull private final String iamHttpRequestMethod;
+ @NonNull private final String iamRequestHeaders;
+ @NonNull private final String iamRequestBody;
+
+ public AwsAuthLoginInput toLoginInput(String identityId) {
+ return AwsAuthLoginInput.builder()
+ .identityId(identityId)
+ .iamRequestHeaders(iamRequestHeaders)
+ .iamHttpRequestMethod(iamHttpRequestMethod)
+ .iamRequestBody(iamRequestBody)
+ .build();
+ }
+}
diff --git a/src/main/java/com/infisical/sdk/resources/AuthClient.java b/src/main/java/com/infisical/sdk/resources/AuthClient.java
index 73851e2..bf334df 100644
--- a/src/main/java/com/infisical/sdk/resources/AuthClient.java
+++ b/src/main/java/com/infisical/sdk/resources/AuthClient.java
@@ -1,12 +1,13 @@
package com.infisical.sdk.resources;
+import com.infisical.sdk.api.ApiClient;
+import com.infisical.sdk.auth.AwsAuthProvider;
+import com.infisical.sdk.models.AwsAuthLoginInput;
import com.infisical.sdk.models.LdapAuthLoginInput;
import com.infisical.sdk.models.MachineIdentityCredential;
+import com.infisical.sdk.models.UniversalAuthLoginInput;
import com.infisical.sdk.util.InfisicalException;
import java.util.function.Consumer;
-import com.infisical.sdk.api.ApiClient;
-
-import com.infisical.sdk.models.UniversalAuthLoginInput;
public class AuthClient {
private final ApiClient apiClient;
@@ -18,29 +19,44 @@ public AuthClient(ApiClient apiClient, Consumer onAuthenticate) {
}
public void UniversalAuthLogin(String clientId, String clientSecret) throws InfisicalException {
- var params = UniversalAuthLoginInput.builder()
- .clientId(clientId)
- .clientSecret(clientSecret)
- .build();
-
- var url = String.format("%s%s", this.apiClient.GetBaseUrl(), "/api/v1/auth/universal-auth/login");
- var credential = this.apiClient.post(url, params, MachineIdentityCredential.class);
- this.onAuthenticate.accept(credential.getAccessToken());
+ var params =
+ UniversalAuthLoginInput.builder().clientId(clientId).clientSecret(clientSecret).build();
+
+ var url =
+ String.format("%s%s", this.apiClient.GetBaseUrl(), "/api/v1/auth/universal-auth/login");
+ var credential = this.apiClient.post(url, params, MachineIdentityCredential.class);
+ this.onAuthenticate.accept(credential.getAccessToken());
}
public void LdapAuthLogin(LdapAuthLoginInput input) throws InfisicalException {
- var validationMsg = input.validate();
+ var validationMsg = input.validate();
+
+ if (validationMsg != null) {
+ throw new InfisicalException(validationMsg);
+ }
+
+ var url = String.format("%s%s", this.apiClient.GetBaseUrl(), "/api/v1/auth/ldap-auth/login");
+ var credential = this.apiClient.post(url, input, MachineIdentityCredential.class);
+ this.onAuthenticate.accept(credential.getAccessToken());
+ }
+
+ public void AwsAuthLogin(String identityId) throws InfisicalException {
+ AwsAuthLogin(AwsAuthProvider.defaultProvider().fromInstanceProfile().toLoginInput(identityId));
+ }
+
+ public void AwsAuthLogin(AwsAuthLoginInput input) throws InfisicalException {
+ var validationMsg = input.validate();
- if (validationMsg != null) {
- throw new InfisicalException(validationMsg);
- }
+ if (validationMsg != null) {
+ throw new InfisicalException(validationMsg);
+ }
- var url = String.format("%s%s", this.apiClient.GetBaseUrl(), "/api/v1/auth/ldap-auth/login");
- var credential = this.apiClient.post(url, input, MachineIdentityCredential.class);
- this.onAuthenticate.accept(credential.getAccessToken());
+ var url = String.format("%s%s", this.apiClient.GetBaseUrl(), "/api/v1/auth/aws-auth/login");
+ var credential = this.apiClient.post(url, input, MachineIdentityCredential.class);
+ this.onAuthenticate.accept(credential.getAccessToken());
}
public void SetAccessToken(String accessToken) {
this.onAuthenticate.accept(accessToken);
}
-}
\ No newline at end of file
+}
diff --git a/src/test/java/com/infisical/sdk/auth/AwsAuthProviderTest.java b/src/test/java/com/infisical/sdk/auth/AwsAuthProviderTest.java
new file mode 100644
index 0000000..a124a9b
--- /dev/null
+++ b/src/test/java/com/infisical/sdk/auth/AwsAuthProviderTest.java
@@ -0,0 +1,99 @@
+package com.infisical.sdk.auth;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.infisical.sdk.models.AwsAuthParameters;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+
+class AwsAuthProviderTest {
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ @Test
+ void testFromCredentials() throws JsonProcessingException {
+ final AwsAuthProvider provider =
+ AwsAuthProvider.builder().overrideInstant(Instant.ofEpochSecond(1759446719)).build();
+ final AwsAuthParameters loginInput =
+ provider.fromCredentials(
+ "us-west-2",
+ AwsBasicCredentials.create("MOCK_ACCESS_KEY", "MOCK_SECRET_KEY"),
+ "MOCK_SESSION_TOKEN");
+ assertEquals("POST", loginInput.getIamHttpRequestMethod());
+
+ final String decodedBody =
+ new String(
+ Base64.getDecoder().decode(loginInput.getIamRequestBody()), StandardCharsets.UTF_8);
+ final Map> bodyParams =
+ Arrays.stream(decodedBody.split("&"))
+ .map(
+ item -> {
+ final String[] parts =
+ URLDecoder.decode(item, StandardCharsets.UTF_8).split("=", 2);
+ return Map.entry(parts[0], List.of(parts[1]));
+ })
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ assertEquals(provider.getParams(), bodyParams);
+
+ final String decodedHeaders =
+ new String(
+ Base64.getDecoder().decode(loginInput.getIamRequestHeaders()), StandardCharsets.UTF_8);
+ final Map actualHeaders =
+ objectMapper.readValue(decodedHeaders, new TypeReference<>() {});
+ assertEquals(
+ Map.ofEntries(
+ Map.entry("Host", "sts.us-west-2.amazonaws.com"),
+ Map.entry("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"),
+ Map.entry("Content-Length", "43"),
+ Map.entry(
+ "x-amz-content-sha256",
+ "ab821ae955788b0e33ebd34c208442ccfc2d406e2edc5e7a39bd6458fbb4f843"),
+ Map.entry("X-Amz-Security-Token", "MOCK_SESSION_TOKEN"),
+ Map.entry("X-Amz-Date", "20251002T231159Z"),
+ Map.entry(
+ "Authorization",
+ "AWS4-HMAC-SHA256 Credential=MOCK_ACCESS_KEY/20251002/us-west-2/sts/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=9b1b93454bea36297168ed67a861df12d17136f47cbdf5d23b1daa0fe704742b")),
+ actualHeaders);
+ }
+
+ static Stream encodeParametersCases() {
+ return Stream.of(
+ // empty
+ Arguments.of(Map.of(), ""),
+ // simple
+ Arguments.of(
+ Map.ofEntries(Map.entry("a", List.of("123")), Map.entry("b", List.of("456"))),
+ "a=123&b=456"),
+ // sorting the key
+ Arguments.of(
+ Map.ofEntries(
+ Map.entry("d", List.of("3")),
+ Map.entry("a", List.of("0")),
+ Map.entry("c", List.of("2")),
+ Map.entry("b", List.of("1"))),
+ "a=0&b=1&c=2&d=3"),
+ Arguments.of(
+ Map.ofEntries(Map.entry("a", List.of("!@#$%^&*(){}[]"))),
+ "a=%21%40%23%24%25%5E%26*%28%29%7B%7D%5B%5D"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("encodeParametersCases")
+ void testEncodeParameters(Map> params, String expected) {
+ assertEquals(expected, AwsAuthProvider.encodeParameters(params));
+ }
+}