Support AppRole authentication.

We now support AppRole authentication. This authentication method uses a provided RoleId and optionally SecretId to authenticate against Vault.

Fixes gh-7.
This commit is contained in:
Mark Paluch
2016-10-11 17:33:33 +02:00
parent 76e52bb757
commit d67e4d40fb
6 changed files with 547 additions and 2 deletions

View File

@@ -49,6 +49,7 @@ high-level abstractions for interacting with Vault, freeing the user from infras
* Connection package as low-level abstraction
* Reading, writing and deleting data from Vault with object mapping support
* Multiple authentication mechanisms: AppId, AppRole, Client Certificates, and Cubbyhole (wrapped/stored token)
<span id="quick-start"></span>
@@ -109,4 +110,4 @@ public class Example {
{% include project_page.html %}
</html>
</html>

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.vault.authentication;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import org.springframework.vault.client.VaultClient;
import org.springframework.vault.client.VaultException;
import org.springframework.vault.client.VaultResponseEntity;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultToken;
/**
* AppRole implementation of {@link ClientAuthentication}. RoleId and SecretId (optional) are sent in the login request
* to Vault to obtain a {@link VaultToken}.
* <p>
* {@link AppRoleAuthentication} can be configured for push and pull mode by setting
* {@link AppRoleAuthenticationOptions#getSecretId()}.
*
* @author Mark Paluch
* @see AppRoleAuthenticationOptions
* @see VaultClient
* @see <a href="https://www.vaultproject.io/docs/auth/approle.html">Auth Backend: AppRole</a>
*/
public class AppRoleAuthentication implements ClientAuthentication {
private final static Logger logger = LoggerFactory.getLogger(AppRoleAuthentication.class);
private final AppRoleAuthenticationOptions options;
private final VaultClient vaultClient;
/**
* Creates a {@link AppRoleAuthentication} using {@link AppRoleAuthenticationOptions} and {@link VaultClient}.
*
* @param options must not be {@literal null}.
* @param vaultClient must not be {@literal null}.
*/
public AppRoleAuthentication(AppRoleAuthenticationOptions options, VaultClient vaultClient) {
Assert.notNull(options, "AppRoleAuthenticationOptions must not be null");
Assert.notNull(vaultClient, "VaultClient must not be null");
this.options = options;
this.vaultClient = vaultClient;
}
@Override
public VaultToken login() {
return createTokenUsingAppRole();
}
private VaultToken createTokenUsingAppRole() {
Map<String, String> login = getAppRoleLogin(options.getRoleId(), options.getSecretId());
VaultResponseEntity<VaultResponse> entity = vaultClient
.postForEntity(String.format("auth/%s/login", options.getPath()), login, VaultResponse.class);
if (!entity.isSuccessful()) {
throw new VaultException(String.format("Cannot login using AppRole: %s", entity.getMessage()));
}
logger.debug("Login successful using AppRole authentication");
return LoginTokenUtil.from(entity.getBody().getAuth());
}
private Map<String, String> getAppRoleLogin(String roleId, String secretId) {
Map<String, String> login = new HashMap<String, String>();
login.put("role_id", roleId);
if (secretId != null) {
login.put("secret_id", secretId);
}
return login;
}
}

View File

@@ -0,0 +1,153 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.vault.authentication;
import org.springframework.util.Assert;
/**
* Authentication options for {@link AppRoleAuthentication}.
* <p>
* Authentication options provide the path, roleId and pull/push mode. {@link AppRoleAuthentication} can be
* constructed using {@link #builder()}. Instances of this class are immutable once constructed.
*
* @author Mark Paluch
* @see AppRoleAuthentication
* @see #builder()
*/
public class AppRoleAuthenticationOptions {
public final static String DEFAULT_APPROLE_AUTHENTICATION_PATH = "approle";
/**
* Path of the apprile authentication backend mount.
*/
private final String path;
/**
* The RoleId.
*/
private final String roleId;
/**
* The Bind SecretId.
*/
private final String secretId;
private AppRoleAuthenticationOptions(String path, String roleId, String secretId) {
this.path = path;
this.roleId = roleId;
this.secretId = secretId;
}
/**
* @return a new {@link AppRoleAuthenticationOptionsBuilder}.
*/
public static AppRoleAuthenticationOptionsBuilder builder() {
return new AppRoleAuthenticationOptionsBuilder();
}
/**
* @return the mount path.
*/
public String getPath() {
return path;
}
/**
* @return the RoleId.
*/
public String getRoleId() {
return roleId;
}
/**
* @return the bound SecretId.
*/
public String getSecretId() {
return secretId;
}
/**
* Builder for {@link AppRoleAuthenticationOptions}.
*/
public static class AppRoleAuthenticationOptionsBuilder {
private String path = DEFAULT_APPROLE_AUTHENTICATION_PATH;
private String roleId;
private String secretId;
AppRoleAuthenticationOptionsBuilder() {}
/**
* Configure the mount path.
*
* @param path must not be empty or {@literal null}.
* @return {@code this} {@link AppRoleAuthenticationOptionsBuilder}.
* @see #DEFAULT_APPROLE_AUTHENTICATION_PATH
*/
public AppRoleAuthenticationOptionsBuilder path(String path) {
Assert.hasText(path, "Path must not be empty");
this.path = path;
return this;
}
/**
* Configure the RoleId.
*
* @param roleId must not be empty or {@literal null}.
* @return {@code this} {@link AppRoleAuthenticationOptionsBuilder}.
*/
public AppRoleAuthenticationOptionsBuilder roleId(String roleId) {
Assert.hasText(roleId, "RoleId must not be empty");
this.roleId = roleId;
return this;
}
/**
* Configure a {@code secretId}.
*
* @param secretId must not be empty or {@literal null}.
* @return {@code this} {@link AppRoleAuthenticationOptionsBuilder}.
*/
public AppRoleAuthenticationOptionsBuilder secretId(String secretId) {
Assert.hasText(secretId, "SecretId must not be empty");
this.secretId = secretId;
return this;
}
/**
* Build a new {@link AppRoleAuthenticationOptions} instance. Requires {@link #roleId(String)} to be configured.
*
* @return a new {@link AppRoleAuthenticationOptions}.
*/
public AppRoleAuthenticationOptions build() {
Assert.hasText(path, "Path must not be empty");
Assert.notNull(roleId, "RoleId must not be null");
return new AppRoleAuthenticationOptions(path, roleId, secretId);
}
}
}

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.vault.authentication;
import static org.assertj.core.api.Assertions.*;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assume.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.vault.client.VaultException;
import org.springframework.vault.core.VaultOperations;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.util.IntegrationTestSupport;
/**
* Integration tests for {@link AppRoleAuthentication}.
*
* @author Mark Paluch
*/
public class AppRoleAuthenticationIntegrationTests extends IntegrationTestSupport {
@Before
public void before() {
assumeThat(prepare().getVaultOperations().opsForSys().health().getVersion(),
not(anyOf(nullValue(), equalTo(""), containsString("0.5"), containsString("0.6.1"))));
if (!prepare().hasAuth("approle")) {
prepare().mountAuth("approle");
}
getVaultOperations().doWithVault(new VaultOperations.SessionCallback<Object>() {
@Override
public Object doWithVault(VaultOperations.VaultSession session) {
Map<String, String> withSecretId = new HashMap<String, String>();
withSecretId.put("policies", "dummy"); // policy
withSecretId.put("bound_cidr_list", "0.0.0.0/0");
withSecretId.put("bind_secret_id", "true");
session.postForEntity("auth/approle/role/with-secret-id", withSecretId, Map.class);
Map<String, String> noSecretIdRole = new HashMap<String, String>();
noSecretIdRole.put("policies", "dummy"); // policy
noSecretIdRole.put("bound_cidr_list", "0.0.0.0/0");
noSecretIdRole.put("bind_secret_id", "false");
session.postForEntity("auth/approle/role/no-secret-id", noSecretIdRole, Map.class);
return null;
}
});
}
@Test
public void shouldAuthenticateWithRoleIdOnly() {
String roleId = getRoleId("no-secret-id");
AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder().roleId(roleId).build();
AppRoleAuthentication authentication = new AppRoleAuthentication(options, prepare().getVaultClient());
assertThat(authentication.login()).isNotNull();
}
@Test
public void shouldAuthenticatePullModeWithGeneratedSecretId() {
String roleId = getRoleId("with-secret-id");
String secretId = (String) getVaultOperations()
.write(String.format("auth/approle/role/%s/secret-id", "with-secret-id"), null).getData().get("secret_id");
AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder().roleId(roleId).secretId(secretId)
.build();
AppRoleAuthentication authentication = new AppRoleAuthentication(options, prepare().getVaultClient());
assertThat(authentication.login()).isNotNull();
}
@Test(expected = VaultException.class)
public void shouldAuthenticatePullModeFailsWithoutSecretId() {
String roleId = getRoleId("with-secret-id");
AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder().roleId(roleId).build();
AppRoleAuthentication authentication = new AppRoleAuthentication(options, prepare().getVaultClient());
assertThat(authentication.login()).isNotNull();
}
@Test(expected = VaultException.class)
public void shouldAuthenticatePullModeFailsWithWrongSecretId() {
String roleId = getRoleId("with-secret-id");
AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder().roleId(roleId)
.secretId("this-is-a-wrong-secret-id").build();
AppRoleAuthentication authentication = new AppRoleAuthentication(options, prepare().getVaultClient());
assertThat(authentication.login()).isNotNull();
}
@Test
public void shouldAuthenticatePushModeWithProvidedSecretId() {
String roleId = getRoleId("with-secret-id");
final String secretId = "hello_world";
final VaultResponse customSecretIdResponse = getVaultOperations()
.write("auth/approle/role/with-secret-id/custom-secret-id", Collections.singletonMap("secret_id", secretId));
AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder().roleId(roleId).secretId(secretId)
.build();
AppRoleAuthentication authentication = new AppRoleAuthentication(options, prepare().getVaultClient());
assertThat(authentication.login()).isNotNull();
getVaultOperations().write("auth/approle/role/with-secret-id/secret-id-accessor/destroy",
customSecretIdResponse.getData());
}
private VaultOperations getVaultOperations() {
return prepare().getVaultOperations();
}
private String getRoleId(String roleName) {
return (String) getVaultOperations().read(String.format("auth/approle/role/%s/role-id", roleName)).getData()
.get("role_id");
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.vault.authentication;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
import static org.springframework.test.web.client.response.MockRestResponseCreators.*;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.vault.client.VaultClient;
import org.springframework.vault.client.VaultEndpoint;
import org.springframework.vault.client.VaultException;
import org.springframework.vault.support.VaultToken;
import org.springframework.web.client.RestTemplate;
/**
* Unit tests for {@link AppRoleAuthentication}.
*
* @author Mark Paluch
*/
public class AppRoleAuthenticationUnitTests {
private VaultClient vaultClient;
private MockRestServiceServer mockRest;
@Before
public void before() throws Exception {
RestTemplate restTemplate = new RestTemplate();
mockRest = MockRestServiceServer.createServer(restTemplate);
vaultClient = new VaultClient(restTemplate, new VaultEndpoint());
}
@Test
public void loginShouldObtainToken() throws Exception {
AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder().roleId("hello") //
.secretId("world") //
.build();
mockRest.expect(requestTo("https://localhost:8200/v1/auth/approle/login")) //
.andExpect(method(HttpMethod.POST)) //
.andExpect(jsonPath("$.role_id").value("hello")) //
.andExpect(jsonPath("$.secret_id").value("world")) //
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON)
.body("{" + "\"auth\":{\"client_token\":\"my-token\"}" + "}"));
AppRoleAuthentication sut = new AppRoleAuthentication(options, vaultClient);
VaultToken login = sut.login();
assertThat(login).isInstanceOf(LoginToken.class);
assertThat(login.getToken()).isEqualTo("my-token");
}
@Test
public void loginShouldObtainTokenWithoutSecretId() throws Exception {
AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder().roleId("hello") //
.build();
mockRest.expect(requestTo("https://localhost:8200/v1/auth/approle/login")) //
.andExpect(method(HttpMethod.POST)) //
.andExpect(jsonPath("$.role_id").value("hello")) //
.andExpect(jsonPath("$.secret_id").doesNotExist()) //
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON)
.body("{" + "\"auth\":{\"client_token\":\"my-token\", \"lease_duration\": 10, \"renewable\": true}" + "}"));
AppRoleAuthentication sut = new AppRoleAuthentication(options, vaultClient);
VaultToken login = sut.login();
assertThat(login).isInstanceOf(LoginToken.class);
assertThat(login.getToken()).isEqualTo("my-token");
assertThat(((LoginToken) login).getLeaseDuration()).isEqualTo(10);
assertThat(((LoginToken) login).isRenewable()).isTrue();
}
@Test(expected = VaultException.class)
public void loginShouldFail() throws Exception {
AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder().roleId("hello") //
.build();
mockRest.expect(requestTo("https://localhost:8200/v1/auth/approle/login")) //
.andRespond(withServerError());
new AppRoleAuthentication(options, vaultClient).login();
}
}

View File

@@ -34,6 +34,7 @@ class AppConfig extends AbstractVaultConfiguration {
See also: https://www.vaultproject.io/docs/concepts/tokens.html[Vault Documentation: Tokens]
[[vault.authentication.appid]]
== AppId authentication
Vault supports https://www.vaultproject.io/docs/auth/app-id.html[AppId]
@@ -143,6 +144,44 @@ public class MyUserIdMechanism implements AppIdUserIdMechanism {
See also: https://www.vaultproject.io/docs/auth/app-id.html[Vault Documentation: Using the App ID auth backend]
== AppRole authentication
https://www.vaultproject.io/docs/auth/app-id.html[AppRole] allows machine
authentication, like the deprecated (since Vault 0.6.1) <<vault.authentication.appid>>.
AppRole authentication consists of two hard to guess (secret) tokens: RoleId and SecretId.
Spring Vault supports AppRole authentication by providing either RoleId only
or together with a provided SecretId (push or pull mode).
RoleId and optionally SecretId must be provided to `AppRoleAuthenticationOptions`,
Spring Vault will not look up these or create a custom SecretId.
====
[source,java]
----
@Configuration
class AppConfig extends AbstractVaultConfiguration {
// …
@Override
public ClientAuthentication clientAuthentication() {
AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder()
.roleId("…")
.secretId("…")
.build();
return new AppRoleAuthentication(options, vaultClient());
}
// …
}
----
====
See also: https://www.vaultproject.io/docs/auth/approle.html[Vault Documentation: Using the AppRole auth backend]
== AWS-EC2 authentication
The https://www.vaultproject.io/docs/auth/aws-ec2.html[aws-ec2]
@@ -335,4 +374,4 @@ See also:
* https://www.vaultproject.io/docs/concepts/tokens.html[Vault Documentation: Tokens]
* https://www.vaultproject.io/docs/secrets/cubbyhole/index.html[Vault Documentation:Cubbyhole Secret Backend]
* https://www.vaultproject.io/docs/concepts/response-wrapping.html[Vault Documentation: Response Wrapping]
* https://www.vaultproject.io/docs/concepts/response-wrapping.html[Vault Documentation: Response Wrapping]