diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AwsIamAuthentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AwsIamAuthentication.java index 6ca22361..d81904c5 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AwsIamAuthentication.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AwsIamAuthentication.java @@ -24,6 +24,7 @@ import java.util.Map.Entry; import com.amazonaws.DefaultRequest; import com.amazonaws.auth.AWS4Signer; +import com.amazonaws.auth.AWSCredentials; import com.amazonaws.http.HttpMethodName; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -65,7 +66,8 @@ import org.springframework.web.client.RestOperations; * href="http://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html">AWS: * GetCallerIdentity */ -public class AwsIamAuthentication implements ClientAuthentication { +public class AwsIamAuthentication implements ClientAuthentication, + AuthenticationStepsFactory { private static final Log logger = LogFactory.getLog(AwsIamAuthentication.class); @@ -98,11 +100,45 @@ public class AwsIamAuthentication implements ClientAuthentication { this.vaultRestOperations = vaultRestOperations; } + /** + * Creates a {@link AuthenticationSteps} for AWS-IAM authentication given + * {@link AwsIamAuthenticationOptions}. The resulting {@link AuthenticationSteps} + * reuse eagerly-fetched {@link AWSCredentials} to prevent blocking I/O during + * authentication. + * + * @param options must not be {@literal null}. + * @return {@link AuthenticationSteps} for AWS-IAM authentication. + * @since 2.2 + */ + public static AuthenticationSteps createAuthenticationSteps( + AwsIamAuthenticationOptions options) { + + Assert.notNull(options, "AwsIamAuthenticationOptions must not be null"); + + AWSCredentials credentials = options.getCredentialsProvider().getCredentials(); + + return createAuthenticationSteps(options, credentials); + } + + protected static AuthenticationSteps createAuthenticationSteps( + AwsIamAuthenticationOptions options, AWSCredentials credentials) { + + return AuthenticationSteps.fromSupplier( + () -> createRequestBody(options, credentials)) // + .login("auth/{mount}/login", options.getPath()); + } + @Override public VaultToken login() throws VaultException { return createTokenUsingAwsIam(); } + @Override + public AuthenticationSteps getAuthenticationSteps() { + return createAuthenticationSteps(this.options, this.options + .getCredentialsProvider().getCredentials()); + } + @SuppressWarnings("unchecked") private VaultToken createTokenUsingAwsIam() { @@ -147,6 +183,19 @@ public class AwsIamAuthentication implements ClientAuthentication { */ protected static Map createRequestBody( AwsIamAuthenticationOptions options) { + return createRequestBody(options, options.getCredentialsProvider() + .getCredentials()); + } + + /** + * Create the request body to perform a Vault login using the AWS-IAM authentication + * method. + * + * @param options must not be {@literal null}. + * @return the map containing body key-value pairs. + */ + private static Map createRequestBody( + AwsIamAuthenticationOptions options, AWSCredentials credentials) { Map login = new HashMap<>(); @@ -155,7 +204,7 @@ public class AwsIamAuthentication implements ClientAuthentication { .toString().getBytes())); login.put("iam_request_body", REQUEST_BODY_BASE64_ENCODED); - String headerJson = getSignedHeaders(options); + String headerJson = getSignedHeaders(options, credentials); login.put("iam_request_headers", Base64Utils.encodeToString(headerJson.getBytes())); @@ -166,7 +215,8 @@ public class AwsIamAuthentication implements ClientAuthentication { return login; } - private static String getSignedHeaders(AwsIamAuthenticationOptions options) { + private static String getSignedHeaders(AwsIamAuthenticationOptions options, + AWSCredentials credentials) { Map headers = createIamRequestHeaders(options); @@ -180,7 +230,7 @@ public class AwsIamAuthentication implements ClientAuthentication { request.setEndpoint(options.getEndpointUri()); signer.setServiceName(request.getServiceName()); - signer.sign(request, options.getCredentialsProvider().getCredentials()); + signer.sign(request, credentials); Map map = new LinkedHashMap<>(); diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AwsIamAuthenticationUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AwsIamAuthenticationUnitTests.java index 97d69904..cc7c679e 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AwsIamAuthenticationUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AwsIamAuthenticationUnitTests.java @@ -46,7 +46,7 @@ public class AwsIamAuthenticationUnitTests { private MockRestServiceServer mockRest; @Before - public void before() throws Exception { + public void before() { RestTemplate restTemplate = VaultClients.createRestTemplate(); restTemplate.setUriTemplateHandler(new PrefixAwareUriTemplateHandler()); @@ -85,4 +85,39 @@ public class AwsIamAuthenticationUnitTests { Duration.ofSeconds(10)); assertThat(((LoginToken) login).isRenewable()).isTrue(); } + + @Test + public void shouldUsingAuthenticationSteps() { + + mockRest.expect(requestTo("/auth/aws/login")) + .andExpect(method(HttpMethod.POST)) + .andExpect(jsonPath("$.iam_http_request_method").value("POST")) + .andExpect(jsonPath("$.iam_request_url").exists()) + .andExpect(jsonPath("$.iam_request_body").exists()) + .andExpect(jsonPath("$.iam_request_headers").exists()) + .andExpect(jsonPath("$.role").value("foo-role")) + .andRespond( + withSuccess() + .contentType(MediaType.APPLICATION_JSON) + .body("{" + + "\"auth\":{\"client_token\":\"my-token\", \"renewable\": true, \"lease_duration\": 10}" + + "}")); + + AwsIamAuthenticationOptions options = AwsIamAuthenticationOptions.builder() + .role("foo-role").credentials(new BasicAWSCredentials("foo", "bar")) + .build(); + + AuthenticationSteps steps = AwsIamAuthentication + .createAuthenticationSteps(options); + AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor(steps, + restTemplate); + + VaultToken login = executor.login(); + + assertThat(login).isInstanceOf(LoginToken.class); + assertThat(login.getToken()).isEqualTo("my-token"); + assertThat(((LoginToken) login).getLeaseDuration()).isEqualTo( + Duration.ofSeconds(10)); + assertThat(((LoginToken) login).isRenewable()).isTrue(); + } }