Skip to content

Commit b9eac32

Browse files
authored
Merge pull request #9 from Infisical/ENG-3808-add-aws-auth
[ENG-3808] Add aws auth login
2 parents d9f847a + f0be8e4 commit b9eac32

File tree

6 files changed

+369
-23
lines changed

6 files changed

+369
-23
lines changed

pom.xml

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@
5555
<gson-fire-version>1.8.3</gson-fire-version>
5656
</properties>
5757
<dependencies>
58-
58+
<dependency>
59+
<groupId>com.fasterxml.jackson.core</groupId>
60+
<artifactId>jackson-core</artifactId>
61+
<version>2.17.1</version>
62+
</dependency>
5963
<dependency>
6064
<groupId>com.fasterxml.jackson.core</groupId>
6165
<artifactId>jackson-databind</artifactId>
@@ -70,7 +74,7 @@
7074
<dependency>
7175
<groupId>org.projectlombok</groupId>
7276
<artifactId>lombok</artifactId>
73-
<version>1.18.30</version>
77+
<version>1.18.42</version>
7478
<scope>provided</scope>
7579
</dependency>
7680

@@ -119,16 +123,28 @@
119123
<version>${gson-fire-version}</version>
120124
</dependency>
121125

126+
<!-- AWS dependencies-->
127+
<dependency>
128+
<groupId>software.amazon.awssdk</groupId>
129+
<artifactId>auth</artifactId>
130+
<version>2.34.8</version>
131+
<optional>true</optional>
132+
</dependency>
122133
<!-- Testing dependencies-->
123134
<dependency>
124135
<groupId>org.junit.jupiter</groupId>
125136
<artifactId>junit-jupiter-engine</artifactId>
126137
<version>5.9.1</version>
127138
<scope>test</scope>
128139
</dependency>
140+
<dependency>
141+
<groupId>org.junit.jupiter</groupId>
142+
<artifactId>junit-jupiter-params</artifactId>
143+
<version>5.9.1</version>
144+
<scope>test</scope>
145+
</dependency>
129146
</dependencies>
130147

131-
132148
<build>
133149
<plugins>
134150
<plugin>
@@ -171,7 +187,7 @@
171187
<path>
172188
<groupId>org.projectlombok</groupId>
173189
<artifactId>lombok</artifactId>
174-
<version>1.18.30</version>
190+
<version>1.18.42</version>
175191
</path>
176192
</annotationProcessorPaths>
177193
</configuration>
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package com.infisical.sdk.auth;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.infisical.sdk.models.AwsAuthParameters;
6+
import java.net.URI;
7+
import java.net.URLEncoder;
8+
import java.nio.charset.StandardCharsets;
9+
import java.time.Clock;
10+
import java.time.Instant;
11+
import java.time.ZoneOffset;
12+
import java.util.Base64;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.stream.Collectors;
16+
import lombok.Builder;
17+
import lombok.Data;
18+
import lombok.NonNull;
19+
import software.amazon.awssdk.auth.credentials.AwsCredentials;
20+
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
21+
import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider;
22+
import software.amazon.awssdk.http.ContentStreamProvider;
23+
import software.amazon.awssdk.http.SdkHttpFullRequest;
24+
import software.amazon.awssdk.http.SdkHttpMethod;
25+
import software.amazon.awssdk.http.SdkHttpRequest;
26+
import software.amazon.awssdk.http.auth.aws.signer.AwsV4FamilyHttpSigner;
27+
import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner;
28+
import software.amazon.awssdk.http.auth.spi.signer.HttpSigner;
29+
import software.amazon.awssdk.regions.Region;
30+
import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain;
31+
32+
@Data
33+
@Builder
34+
public class AwsAuthProvider {
35+
private static final ObjectMapper objectMapper = new ObjectMapper();
36+
37+
@NonNull @Builder.Default private final String serviceName = "sts";
38+
@NonNull @Builder.Default private final SdkHttpMethod httpMethod = SdkHttpMethod.POST;
39+
@NonNull @Builder.Default private final String endpointTemplate = "https://sts.%s.amazonaws.com";
40+
41+
@NonNull @Builder.Default
42+
private final String contentType = "application/x-www-form-urlencoded; charset=utf-8";
43+
44+
@NonNull @Builder.Default
45+
private final Map<String, List<String>> params =
46+
Map.ofEntries(
47+
Map.entry("Action", List.of("GetCallerIdentity")),
48+
Map.entry("Version", List.of("2011-06-15")));
49+
50+
private final Instant overrideInstant;
51+
52+
/**
53+
* Create AwsAuthLoginInput from given AWS credentials.
54+
*
55+
* @param region region of AWS identity
56+
* @param credentials AWS credentials for creating the login input
57+
* @param sessionToken Session token for creating the login input
58+
* @return the AwsAuthLoginInput created from the given credentials for exchanging access token
59+
*/
60+
public AwsAuthParameters fromCredentials(
61+
String region, AwsCredentials credentials, String sessionToken) {
62+
final AwsV4HttpSigner signer = AwsV4HttpSigner.create();
63+
final String iamRequestURL = endpointTemplate.formatted(region);
64+
final String iamRequestBody = encodeParameters(params);
65+
final SdkHttpFullRequest.Builder requestBuilder =
66+
SdkHttpFullRequest.builder()
67+
.uri(URI.create(iamRequestURL))
68+
.method(httpMethod)
69+
.appendHeader("Content-Type", contentType);
70+
if (sessionToken != null) {
71+
requestBuilder.appendHeader("X-Amz-Security-Token", sessionToken);
72+
}
73+
final SdkHttpFullRequest request = requestBuilder.build();
74+
final SdkHttpRequest signedRequest =
75+
signer
76+
.sign(
77+
signingRequest -> {
78+
var req =
79+
signingRequest
80+
.request(request)
81+
.identity(credentials)
82+
.payload(
83+
ContentStreamProvider.fromByteArray(
84+
iamRequestBody.getBytes(StandardCharsets.UTF_8)))
85+
.putProperty(AwsV4FamilyHttpSigner.SERVICE_SIGNING_NAME, serviceName)
86+
.putProperty(AwsV4HttpSigner.REGION_NAME, region);
87+
if (overrideInstant != null) {
88+
req.putProperty(
89+
HttpSigner.SIGNING_CLOCK, Clock.fixed(overrideInstant, ZoneOffset.UTC));
90+
}
91+
})
92+
.request();
93+
final Map<String, String> requestHeaders =
94+
signedRequest.headers().entrySet().stream()
95+
.map(entry -> Map.entry(entry.getKey(), entry.getValue().getFirst()))
96+
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
97+
requestHeaders.put("Content-Length", String.valueOf(iamRequestBody.length()));
98+
final String encodedHeader;
99+
try {
100+
encodedHeader =
101+
Base64.getEncoder()
102+
.encodeToString(
103+
objectMapper.writeValueAsString(requestHeaders).getBytes(StandardCharsets.UTF_8));
104+
} catch (JsonProcessingException e) {
105+
throw new RuntimeException(e);
106+
}
107+
final String encodedBody =
108+
Base64.getEncoder().encodeToString(iamRequestBody.getBytes(StandardCharsets.UTF_8));
109+
return AwsAuthParameters.builder()
110+
.iamHttpRequestMethod(httpMethod.name())
111+
.iamRequestHeaders(encodedHeader)
112+
.iamRequestBody(encodedBody)
113+
.build();
114+
}
115+
116+
/**
117+
* Create AwsAuthLoginInput from the instance profile in the current environment.
118+
*
119+
* @return the AwsAuthLoginInput created from the current instance profile for exchanging access
120+
* token
121+
*/
122+
public AwsAuthParameters fromInstanceProfile() {
123+
try (InstanceProfileCredentialsProvider provider =
124+
InstanceProfileCredentialsProvider.create()) {
125+
final AwsSessionCredentials credentials =
126+
(AwsSessionCredentials) provider.resolveCredentials();
127+
final DefaultAwsRegionProviderChain regionProvider =
128+
DefaultAwsRegionProviderChain.builder().build();
129+
final Region region = regionProvider.getRegion();
130+
final String sessionToken = credentials.sessionToken();
131+
return fromCredentials(region.id(), credentials, sessionToken);
132+
}
133+
}
134+
135+
/**
136+
* Encode given parameters with URL encoding for the body of form posting request.
137+
*
138+
* @param params parameters mapping key to values to encode
139+
* @return URL-encoded string of the parameters
140+
*/
141+
public static String encodeParameters(Map<String, List<String>> params) {
142+
return params.entrySet().stream()
143+
.flatMap(entry -> entry.getValue().stream().map(item -> Map.entry(entry.getKey(), item)))
144+
// Notice: this is not really needed for real world usage, but it makes the
145+
// body encoded in a deterministic order, so that unit test is much easier
146+
.sorted(Map.Entry.comparingByKey())
147+
.map(
148+
entry ->
149+
String.format(
150+
"%s=%s",
151+
URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8),
152+
URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)))
153+
.collect(Collectors.joining("&"));
154+
}
155+
156+
public static AwsAuthProvider defaultProvider() {
157+
return builder().build();
158+
}
159+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.infisical.sdk.models;
2+
3+
import com.infisical.sdk.util.Helper;
4+
import lombok.Builder;
5+
import lombok.Data;
6+
import lombok.NonNull;
7+
8+
@Data
9+
@Builder(toBuilder = true)
10+
public class AwsAuthLoginInput {
11+
@NonNull private final String identityId;
12+
@NonNull private final String iamHttpRequestMethod;
13+
@NonNull private final String iamRequestHeaders;
14+
@NonNull private final String iamRequestBody;
15+
16+
public String validate() {
17+
if (Helper.isNullOrEmpty(identityId)) {
18+
return "Identity ID is required";
19+
}
20+
21+
if (Helper.isNullOrEmpty(iamHttpRequestMethod)) {
22+
return "IamHttpRequestMethod is required";
23+
}
24+
25+
if (Helper.isNullOrEmpty(iamRequestHeaders)) {
26+
return "IamRequestHeaders is required";
27+
}
28+
29+
if (Helper.isNullOrEmpty(iamRequestBody)) {
30+
return "IamRequestBody is required";
31+
}
32+
return null;
33+
}
34+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.infisical.sdk.models;
2+
3+
import lombok.Builder;
4+
import lombok.Data;
5+
import lombok.NonNull;
6+
7+
@Data
8+
@Builder(toBuilder = true)
9+
public class AwsAuthParameters {
10+
@NonNull private final String iamHttpRequestMethod;
11+
@NonNull private final String iamRequestHeaders;
12+
@NonNull private final String iamRequestBody;
13+
14+
public AwsAuthLoginInput toLoginInput(String identityId) {
15+
return AwsAuthLoginInput.builder()
16+
.identityId(identityId)
17+
.iamRequestHeaders(iamRequestHeaders)
18+
.iamHttpRequestMethod(iamHttpRequestMethod)
19+
.iamRequestBody(iamRequestBody)
20+
.build();
21+
}
22+
}
Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package com.infisical.sdk.resources;
22

3+
import com.infisical.sdk.api.ApiClient;
4+
import com.infisical.sdk.auth.AwsAuthProvider;
5+
import com.infisical.sdk.models.AwsAuthLoginInput;
36
import com.infisical.sdk.models.LdapAuthLoginInput;
47
import com.infisical.sdk.models.MachineIdentityCredential;
8+
import com.infisical.sdk.models.UniversalAuthLoginInput;
59
import com.infisical.sdk.util.InfisicalException;
610
import java.util.function.Consumer;
7-
import com.infisical.sdk.api.ApiClient;
8-
9-
import com.infisical.sdk.models.UniversalAuthLoginInput;
1011

1112
public class AuthClient {
1213
private final ApiClient apiClient;
@@ -18,29 +19,44 @@ public AuthClient(ApiClient apiClient, Consumer<String> onAuthenticate) {
1819
}
1920

2021
public void UniversalAuthLogin(String clientId, String clientSecret) throws InfisicalException {
21-
var params = UniversalAuthLoginInput.builder()
22-
.clientId(clientId)
23-
.clientSecret(clientSecret)
24-
.build();
25-
26-
var url = String.format("%s%s", this.apiClient.GetBaseUrl(), "/api/v1/auth/universal-auth/login");
27-
var credential = this.apiClient.post(url, params, MachineIdentityCredential.class);
28-
this.onAuthenticate.accept(credential.getAccessToken());
22+
var params =
23+
UniversalAuthLoginInput.builder().clientId(clientId).clientSecret(clientSecret).build();
24+
25+
var url =
26+
String.format("%s%s", this.apiClient.GetBaseUrl(), "/api/v1/auth/universal-auth/login");
27+
var credential = this.apiClient.post(url, params, MachineIdentityCredential.class);
28+
this.onAuthenticate.accept(credential.getAccessToken());
2929
}
3030

3131
public void LdapAuthLogin(LdapAuthLoginInput input) throws InfisicalException {
32-
var validationMsg = input.validate();
32+
var validationMsg = input.validate();
33+
34+
if (validationMsg != null) {
35+
throw new InfisicalException(validationMsg);
36+
}
37+
38+
var url = String.format("%s%s", this.apiClient.GetBaseUrl(), "/api/v1/auth/ldap-auth/login");
39+
var credential = this.apiClient.post(url, input, MachineIdentityCredential.class);
40+
this.onAuthenticate.accept(credential.getAccessToken());
41+
}
42+
43+
public void AwsAuthLogin(String identityId) throws InfisicalException {
44+
AwsAuthLogin(AwsAuthProvider.defaultProvider().fromInstanceProfile().toLoginInput(identityId));
45+
}
46+
47+
public void AwsAuthLogin(AwsAuthLoginInput input) throws InfisicalException {
48+
var validationMsg = input.validate();
3349

34-
if (validationMsg != null) {
35-
throw new InfisicalException(validationMsg);
36-
}
50+
if (validationMsg != null) {
51+
throw new InfisicalException(validationMsg);
52+
}
3753

38-
var url = String.format("%s%s", this.apiClient.GetBaseUrl(), "/api/v1/auth/ldap-auth/login");
39-
var credential = this.apiClient.post(url, input, MachineIdentityCredential.class);
40-
this.onAuthenticate.accept(credential.getAccessToken());
54+
var url = String.format("%s%s", this.apiClient.GetBaseUrl(), "/api/v1/auth/aws-auth/login");
55+
var credential = this.apiClient.post(url, input, MachineIdentityCredential.class);
56+
this.onAuthenticate.accept(credential.getAccessToken());
4157
}
4258

4359
public void SetAccessToken(String accessToken) {
4460
this.onAuthenticate.accept(accessToken);
4561
}
46-
}
62+
}

0 commit comments

Comments
 (0)