diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/CubbyholeAuthentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/CubbyholeAuthentication.java new file mode 100644 index 00000000..5b1c2c48 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/CubbyholeAuthentication.java @@ -0,0 +1,178 @@ +/* + * 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.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; + +/** + * Cubbyhole {@link ClientAuthentication} implementation. + *

+ * Cubbyhole authentication uses Vault primitives to provide a secured authentication workflow. Cubbyhole authentication + * uses {@link VaultToken tokens} as primary login method. An ephemeral token is used to obtain a second, login + * {@link VaultToken} from Vault's Cubbyhole secret backend. The login token is usually longer-lived and used to + * interact with Vault. The login token can be retrieved either from a wrapped response or from the {@code data} + * section. + *

+ *

Wrapped token response usage

Create a Token + * + *
+ * 
+ $ vault token-create -wrap-ttl="10m"
+ Key                          	Value
+ ---                          	-----
+ wrapping_token:              	397ccb93-ff6c-b17b-9389-380b01ca2645
+ wrapping_token_ttl:          	0h10m0s
+ wrapping_token_creation_time:	2016-09-18 20:29:48.652957077 +0200 CEST
+ wrapped_accessor:            	46b6aebb-187f-932a-26d7-4f3d86a68319
+ * 
+ * 
+ * + * Setup {@link CubbyholeAuthentication} + * + *
+ * 
+ CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions
+		.builder()
+		.initialToken(VaultToken.of("397ccb93-ff6c-b17b-9389-380b01ca2645"))
+		.wrapped()
+ 		.build();
+ CubbyholeAuthentication authentication = new CubbyholeAuthentication(options, vaultClient);
+ * 
+ * 
+ * + *

Stored token response usage

Create a Token + * + *
+ * 
+ $ vault token-create
+ Key            	Value
+ ---            	-----
+ token          	f9e30681-d46a-cdaf-aaa0-2ae0a9ad0819
+ token_accessor 	4eee9bd9-81bb-06d6-af01-723c54a72148
+ token_duration 	0s
+ token_renewable	false
+ token_policies 	[root]
+
+ $ token-create -use-limit=2 -orphan -no-default-policy -policy=none
+ Key            	Value
+ ---            	-----
+ token          	895cb88b-aef4-0e33-ba65-d50007290780
+ token_accessor 	e84b661c-8aa8-2286-b788-f258f30c8325
+ token_duration 	0s
+ token_renewable	false
+ token_policies 	[none]
+
+ $ export VAULT_TOKEN=895cb88b-aef4-0e33-ba65-d50007290780
+ $ vault write cubbyhole/token token=f9e30681-d46a-cdaf-aaa0-2ae0a9ad0819
+ * 
+ * 
+ * + * Setup {@link CubbyholeAuthentication} + * + *
+ * 
+ CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions
+		.builder()
+		.initialToken(VaultToken.of("895cb88b-aef4-0e33-ba65-d50007290780"))
+		.path("cubbyhole/token")
+		.build();
+ CubbyholeAuthentication authentication = new CubbyholeAuthentication(options, vaultClient);
+ * 
+ * 
+ * + * @author Mark Paluch + * @see CubbyholeAuthenticationOptions + * @see Auth Backend: Token + * @see Cubbyhole Secret Backend + * @see Response Wrapping + */ +public class CubbyholeAuthentication implements ClientAuthentication { + + private final static Logger logger = LoggerFactory.getLogger(CubbyholeAuthentication.class); + + private final CubbyholeAuthenticationOptions options; + + private final VaultClient vaultClient; + + /** + * Create a new {@link CubbyholeAuthentication} given {@link CubbyholeAuthenticationOptions} and {@link VaultClient}. + * + * @param options must not be {@literal null}. + * @param vaultClient must not be {@literal null}. + */ + public CubbyholeAuthentication(CubbyholeAuthenticationOptions options, VaultClient vaultClient) { + + Assert.notNull(options, "CubbyholeAuthenticationOptions must not be null"); + Assert.notNull(vaultClient, "VaultClient must not be null"); + + this.options = options; + this.vaultClient = vaultClient; + } + + @Override + public VaultToken login() throws VaultException { + + VaultResponseEntity entity = vaultClient.getForEntity(options.getPath(), options.getInitialToken(), + VaultResponse.class); + + if (entity.isSuccessful() && entity.hasBody()) { + + VaultResponse body = entity.getBody(); + Map data = body.getData(); + + VaultToken token = getToken(entity, data); + if (token != null) { + + logger.debug("Login successful using Cubbyhole authentication"); + return token; + } + } + + throw new VaultException( + String.format("Cannot retrieve Token from cubbyhole: %s %s", entity.getStatusCode(), entity.getMessage())); + } + + private VaultToken getToken(VaultResponseEntity entity, Map data) { + + if (options.isWrappedToken()) { + + VaultResponse response = vaultClient.unwrap((String) data.get("response"), VaultResponse.class); + return VaultToken.of((String) response.getAuth().get("client_token")); + } + + if (data == null || data.isEmpty()) { + throw new VaultException(String + .format("Cannot retrieve Token from cubbyhole: Response at %s does not contain a token", entity.getUri())); + } + + if (data.size() == 1) { + String token = (String) data.get(data.keySet().iterator().next()); + return VaultToken.of(token); + } + + throw new VaultException(String.format( + "Cannot retrieve Token from cubbyhole: Response at %s does not contain an unique token", entity.getUri())); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/CubbyholeAuthenticationOptions.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/CubbyholeAuthenticationOptions.java new file mode 100644 index 00000000..66bbff4e --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/CubbyholeAuthenticationOptions.java @@ -0,0 +1,151 @@ +/* + * 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; +import org.springframework.vault.support.VaultToken; + +/** + * Authentication options for {@link CubbyholeAuthentication}. + *

+ * Authentication options provide the path below cubbyhole and the cubbyhole mode. Instances of this class are immutable + * once constructed. + * + * @author Mark Paluch + * @see CubbyholeAuthentication + * @see #builder() + */ +public class CubbyholeAuthenticationOptions { + + /** + * Initial {@link VaultToken} to access Cubbyhole. + */ + private final VaultToken initialToken; + + /** + * Path of the Cubbyhole response path. + */ + private final String path; + + /** + * Indicates whether the Cubbyhole contains a wrapped token. + */ + private final boolean wrappedToken; + + private CubbyholeAuthenticationOptions(VaultToken initialToken, String path, boolean wrappedToken) { + + this.initialToken = initialToken; + this.path = path; + this.wrappedToken = wrappedToken; + } + + /** + * @return a new {@link CubbyholeAuthenticationOptionsBuilder}. + */ + public static CubbyholeAuthenticationOptionsBuilder builder() { + return new CubbyholeAuthenticationOptionsBuilder(); + } + + /** + * @return the initial {@link VaultToken} to access Cubbyhole. + */ + public VaultToken getInitialToken() { + return initialToken; + } + + /** + * @return the path of the Cubbyhole response path. + */ + public String getPath() { + return path; + } + + /** + * @return {@literal true} indicates that the Cubbyhole response contains a wrapped token, otherwise {@literal false} + * to expect a token in the {@literal data} response. + */ + public boolean isWrappedToken() { + return wrappedToken; + } + + /** + * Builder for {@link CubbyholeAuthenticationOptions}. + */ + public static class CubbyholeAuthenticationOptionsBuilder { + + private VaultToken initialToken; + + private String path; + + private boolean wrappedToken; + + CubbyholeAuthenticationOptionsBuilder() {} + + /** + * Configures the initial {@link VaultToken} to access Cubbyhole. + * + * @param initialToken must not be {@literal null}. + * @return {@code this} {@link CubbyholeAuthenticationOptionsBuilder}. + */ + public CubbyholeAuthenticationOptionsBuilder initialToken(VaultToken initialToken) { + + Assert.notNull(initialToken, "Initial Vault Token must not be null"); + + this.initialToken = initialToken; + return this; + } + + /** + * Configures the cubbyhole path, such as {@code cubbyhole/token}. Expects a token in the {@code data} response. + * + * @param path must not be empty or {@literal null}. + * @return {@code this} {@link CubbyholeAuthenticationOptionsBuilder}. + */ + public CubbyholeAuthenticationOptionsBuilder path(String path) { + + Assert.hasText(path, "Path must not be empty"); + + this.path = path; + return this; + } + + /** + * Configures whether to use wrapped token responses. + * + * @return {@code this} {@link CubbyholeAuthenticationOptionsBuilder}. + */ + public CubbyholeAuthenticationOptionsBuilder wrapped() { + + this.path = "cubbyhole/response"; + this.wrappedToken = true; + return this; + } + + /** + * Builds a new {@link CubbyholeAuthenticationOptions} instance. Requires {@link #path(String)} or + * {@link #wrapped()} to be configured. + * + * @return a new {@link CubbyholeAuthenticationOptions}. + */ + public CubbyholeAuthenticationOptions build() { + + Assert.notNull(initialToken, "Initial Vault Token must not be null"); + Assert.hasText(path, "Path must not be empty"); + + return new CubbyholeAuthenticationOptions(initialToken, path, wrappedToken); + } + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/client/VaultClient.java b/spring-vault-core/src/main/java/org/springframework/vault/client/VaultClient.java index f75d4a5a..4c2d743c 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/client/VaultClient.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/client/VaultClient.java @@ -15,13 +15,18 @@ */ package org.springframework.vault.client; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.util.Map; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpMethod; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.util.Assert; import org.springframework.vault.core.VaultTemplate; import org.springframework.vault.support.VaultToken; @@ -33,10 +38,9 @@ import org.springframework.web.client.RestTemplate; * {@link HttpMethod HTTP methods}. {@link VaultClient} is configured with an {@link VaultEndpoint} and * {@link RestTemplate}. It does not maintain any session or token state. See {@link VaultTemplate} and * {@link org.springframework.vault.authentication.SessionManager} for authenticated and stateful Vault access. - *

* {@link VaultClient} encapsulates base URI and path construction and uses {@link VaultAccessor} for request and error * handling by returning {@link VaultResponseEntity} for requests. - * + * * @author Mark Paluch * @see VaultResponseEntity * @see VaultTemplate @@ -45,11 +49,13 @@ public class VaultClient extends VaultAccessor { public static final String VAULT_TOKEN = "X-Vault-Token"; + private static final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + private final VaultEndpoint endpoint; /** * Creates a new {@link VaultClient} with a default a {@link RestTemplate} and {@link VaultEndpoint}. - * + * * @see VaultEndpoint */ public VaultClient() { @@ -99,7 +105,7 @@ public class VaultClient extends VaultAccessor { /** * Issue a POST request using the given object to the path, and returns the response as {@link VaultResponseEntity}. - * + * * @param path the path. * @param request the Object to be POSTed, may be {@code null}. * @param responseType the type of the return value @@ -176,9 +182,8 @@ public class VaultClient extends VaultAccessor { /** * Execute the HTTP method to the given URI template, writing the given request entity to the request, and returns the * response as {@link VaultResponseEntity}. - *

* URI Template variables are using the given URI variables, if any. - * + * * @param pathTemplate the path template. * @param method the HTTP method (GET, POST, etc). * @param requestEntity the entity (headers and/or body) to write to the request, may be {@code null}. @@ -198,12 +203,12 @@ public class VaultClient extends VaultAccessor { * Execute the HTTP method to the given path template, writing the given request entity to the request, and returns * the response as {@link VaultResponseEntity}. The given {@link ParameterizedTypeReference} is used to pass generic * type information: - * + * *

 	 * ParameterizedTypeReference<List<MyBean>> myBean = new ParameterizedTypeReference<List<MyBean>>() {};
 	 * ResponseEntity<List<MyBean>> response = client.exchange("http://example.com", HttpMethod.GET, null, myBean, null);
 	 * 
- * + * * @param pathTemplate the path template. * @param method the HTTP method (GET, POST, etc). * @param requestEntity the entity (headers and/or body) to write to the request, may be {@code null}. @@ -249,8 +254,8 @@ public class VaultClient extends VaultAccessor { * * @param pathTemplate must not be empty or {@literal null}. * @param uriVariables must not be {@literal null}. - * @see org.springframework.web.util.UriComponentsBuilder * @return + * @see org.springframework.web.util.UriComponentsBuilder */ protected URI buildUri(String pathTemplate, Map uriVariables) { @@ -261,7 +266,7 @@ public class VaultClient extends VaultAccessor { /** * Create {@link HttpHeaders} for a {@link VaultToken}. - * + * * @param vaultToken must not be {@literal null}. * @return {@link HttpHeaders} for a {@link VaultToken}. */ @@ -273,4 +278,32 @@ public class VaultClient extends VaultAccessor { headers.add(VAULT_TOKEN, vaultToken.getToken()); return headers; } + + /** + * Unwrap a wrapped response created by Vault Response Wrapping + * @param wrappedResponse the wrapped response , must not be empty or {@literal null}. + * @param responseType the type of the return value. + * @return the unwrapped response. + */ + @SuppressWarnings("unchecked") + public T unwrap(final String wrappedResponse, Class responseType) { + + Assert.hasText(wrappedResponse, "Wrapped response must not be empty"); + + try { + return (T) converter.read(responseType, new HttpInputMessage() { + @Override + public InputStream getBody() throws IOException { + return new ByteArrayInputStream(wrappedResponse.getBytes()); + } + + @Override + public HttpHeaders getHeaders() { + return new HttpHeaders(); + } + }); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTemplate.java index 49402cd1..dd6a16ff 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTemplate.java @@ -367,10 +367,13 @@ public class VaultTemplate implements InitializingBean, VaultOperations { } private HttpEntity getHttpEntity(HttpEntity requestEntity) { - HttpHeaders httpHeaders = VaultClient.createHeaders(sessionManager.getSessionToken()); + HttpHeaders httpHeaders = VaultClient.createHeaders(sessionManager.getSessionToken()); HttpEntity requestEntityToUse = requestEntity; + if (requestEntityToUse != null) { + + httpHeaders.putAll(requestEntity.getHeaders()); requestEntityToUse = new HttpEntity(requestEntityToUse.getBody(), httpHeaders); } else { requestEntityToUse = new HttpEntity(httpHeaders); diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationIntegrationTests.java new file mode 100644 index 00000000..53245af6 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationIntegrationTests.java @@ -0,0 +1,97 @@ +/* + * 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.junit.Assume.assumeNotNull; + +import java.util.Map; + +import org.junit.Test; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.vault.client.VaultClient; +import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.client.VaultException; +import org.springframework.vault.client.VaultResponseEntity; +import org.springframework.vault.core.VaultOperations.SessionCallback; +import org.springframework.vault.core.VaultOperations.VaultSession; +import org.springframework.vault.support.VaultResponse; +import org.springframework.vault.support.VaultToken; +import org.springframework.vault.util.IntegrationTestSupport; +import org.springframework.vault.util.Settings; +import org.springframework.vault.util.TestRestTemplateFactory; + +/** + * Integration tests for {@link CubbyholeAuthentication}. + * + * @author Mark Paluch + */ +public class CubbyholeAuthenticationIntegrationTests extends IntegrationTestSupport { + + @Test + public void shouldCreateWrappedToken() throws Exception { + + VaultResponseEntity response = prepare().getVaultOperations() + .doWithVault(new SessionCallback>() { + @Override + public VaultResponseEntity doWithVault(VaultSession session) { + + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Vault-Wrap-TTL", "10m"); + + return session.exchange("auth/token/create", HttpMethod.POST, new HttpEntity(headers), + VaultResponse.class, null); + } + }); + + Map wrapInfo = response.getBody().getWrapInfo(); + + // Response Wrapping requires Vault 0.6.0+ + assumeNotNull(wrapInfo); + + String initialToken = wrapInfo.get("token"); + + CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder() + .initialToken(VaultToken.of(initialToken)).wrapped().build(); + + VaultClient vaultClient = new VaultClient(TestRestTemplateFactory.create(Settings.createSslConfiguration()), + new VaultEndpoint()); + + CubbyholeAuthentication authentication = new CubbyholeAuthentication(options, vaultClient); + VaultToken login = authentication.login(); + assertThat(login.getToken()).doesNotContain(Settings.token().getToken()); + } + + @Test + public void loginShouldFail() throws Exception { + + CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder() + .initialToken(VaultToken.of("Hello")).wrapped().build(); + + VaultClient vaultClient = new VaultClient(TestRestTemplateFactory.create(Settings.createSslConfiguration()), + new VaultEndpoint()); + + CubbyholeAuthentication authentication = new CubbyholeAuthentication(options, vaultClient); + try { + authentication.login(); + fail("Missing VaultException"); + } catch (VaultException e) { + assertThat(e).hasMessageContaining("Cannot retrieve Token from cubbyhole").hasMessageContaining("permission denied"); + } + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationUnitTests.java new file mode 100644 index 00000000..99b993ee --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationUnitTests.java @@ -0,0 +1,130 @@ +/* + * 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 CubbyholeAuthentication}. + * + * @author Mark Paluch + */ +public class CubbyholeAuthenticationUnitTests { + + 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 shouldLoginUsingWrappedLogin() throws Exception { + + mockRest.expect(requestTo("https://localhost:8200/v1/cubbyhole/response")) // + .andExpect(method(HttpMethod.GET)) // + .andExpect(header(VaultClient.VAULT_TOKEN, "hello")) // + .andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON) + .body("{\"data\":{\"response\":\"{\\\"request_id\\\":\\\"058222ef-9ab9-ff39-f087-9d5bee64e46d\\\"," + + "\\\"auth\\\":{\\\"client_token\\\":\\\"5e6332cf-f003-6369-8cba-5bce2330f6cc\\\"," + + "\\\"accessor\\\":\\\"46b6aebb-187f-932a-26d7-4f3d86a68319\\\"}}\" } }")); + + CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder() + .initialToken(VaultToken.of("hello")).wrapped().build(); + + CubbyholeAuthentication authentication = new CubbyholeAuthentication(options, vaultClient); + VaultToken vaultToken = authentication.login(); + + assertThat(vaultToken.getToken()).isEqualTo("5e6332cf-f003-6369-8cba-5bce2330f6cc"); + } + + @Test + public void shouldLoginUsingStoredLogin() throws Exception { + + mockRest.expect(requestTo("https://localhost:8200/v1/cubbyhole/token")) // + .andExpect(method(HttpMethod.GET)) // + .andExpect(header(VaultClient.VAULT_TOKEN, "hello")) // + .andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON) + .body("{\"data\":{\"mytoken\":\"058222ef-9ab9-ff39-f087-9d5bee64e46d\"} }")); + + CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder() + .initialToken(VaultToken.of("hello")).path("cubbyhole/token").build(); + + CubbyholeAuthentication authentication = new CubbyholeAuthentication(options, vaultClient); + VaultToken vaultToken = authentication.login(); + + assertThat(vaultToken.getToken()).isEqualTo("058222ef-9ab9-ff39-f087-9d5bee64e46d"); + } + + @Test + public void shouldFailUsingStoredLoginNoData() throws Exception { + + mockRest.expect(requestTo("https://localhost:8200/v1/cubbyhole/token")) // + .andExpect(method(HttpMethod.GET)) // + .andExpect(header(VaultClient.VAULT_TOKEN, "hello")) // + .andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON).body("{\"data\":{} }")); + + CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder() + .initialToken(VaultToken.of("hello")).path("cubbyhole/token").build(); + + CubbyholeAuthentication authentication = new CubbyholeAuthentication(options, vaultClient); + + try { + authentication.login(); + fail("Missing VaultException"); + } catch (VaultException e) { + assertThat(e).hasMessageContaining("does not contain a token"); + } + } + + @Test + public void shouldFailUsingStoredMultipleEntries() throws Exception { + + mockRest.expect(requestTo("https://localhost:8200/v1/cubbyhole/token")) // + .andExpect(method(HttpMethod.GET)) // + .andExpect(header(VaultClient.VAULT_TOKEN, "hello")) // + .andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON).body("{\"data\":{\"key1\":1, \"key2\":2} }")); + + CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder() + .initialToken(VaultToken.of("hello")).path("cubbyhole/token").build(); + + CubbyholeAuthentication authentication = new CubbyholeAuthentication(options, vaultClient); + + try { + authentication.login(); + fail("Missing VaultException"); + } catch (VaultException e) { + assertThat(e).hasMessageContaining("does not contain an unique token"); + } + } +} diff --git a/src/main/asciidoc/reference/vault.adoc b/src/main/asciidoc/reference/vault.adoc index 797a1dd3..b99dd84f 100644 --- a/src/main/asciidoc/reference/vault.adoc +++ b/src/main/asciidoc/reference/vault.adoc @@ -381,7 +381,7 @@ and the `createUserId` method. Spring Vault will obtain the UserId by calling `createUserId` each time it authenticates using AppId to obtain a token. - +==== [source,java] .MyUserIdMechanism.java ---- @@ -394,6 +394,7 @@ public class MyUserIdMechanism implements AppIdUserIdMechanism { } } ---- +==== See also: https://www.vaultproject.io/docs/auth/app-id.html[Vault Documentation: Using the App ID auth backend] @@ -477,6 +478,120 @@ class AppConfig extends AbstractVaultConfiguration { See also: https://www.vaultproject.io/docs/auth/cert.html[Vault Documentation: Using the cert auth backend] +=== Cubbyhole authentication + +Cubbyhole authentication uses Vault primitives to provide a secured authentication +workflow. Cubbyhole authentication uses tokens as primary login method. +An ephemeral token is used to obtain a second, login VaultToken from Vault's +Cubbyhole secret backend. The login token is usually longer-lived and used to +interact with Vault. The login token can be retrieved either from a wrapped +response or from the `data` section. + +*Creating a wrapped token* + +NOTE: Response Wrapping for token creation requires Vault 0.6.0 or higher. + +.Crating and storing tokens +==== +[source,shell] +---- +$ vault token-create -wrap-ttl="10m" +Key Value +--- ----- +wrapping_token: 397ccb93-ff6c-b17b-9389-380b01ca2645 +wrapping_token_ttl: 0h10m0s +wrapping_token_creation_time: 2016-09-18 20:29:48.652957077 +0200 CEST +wrapped_accessor: 46b6aebb-187f-932a-26d7-4f3d86a68319 +---- +==== + +.Wrapped token response usage +==== +[source,java] +---- +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + + CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions + .builder() + .initialToken(VaultToken.of("…")) + .wrapped() + .build(); + + return new CubbyholeAuthentication(options, vaultClient()); + } + + // … +} +---- +==== + +*Using stored tokens* + +.Crating and storing tokens +==== +[source,shell] +---- +$ vault token-create +Key Value +--- ----- +token f9e30681-d46a-cdaf-aaa0-2ae0a9ad0819 +token_accessor 4eee9bd9-81bb-06d6-af01-723c54a72148 +token_duration 0s +token_renewable false +token_policies [root] + +$ token-create -use-limit=2 -orphan -no-default-policy -policy=none +Key Value +--- ----- +token 895cb88b-aef4-0e33-ba65-d50007290780 +token_accessor e84b661c-8aa8-2286-b788-f258f30c8325 +token_duration 0s +token_renewable false +token_policies [none] + +$ export VAULT_TOKEN=895cb88b-aef4-0e33-ba65-d50007290780 +$ vault write cubbyhole/token token=f9e30681-d46a-cdaf-aaa0-2ae0a9ad0819 +---- +==== + +.Stored token response usage +==== +[source,java] +---- +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + + CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions + .builder() + .initialToken(VaultToken.of("…")) + .path("cubbyhole/token") + .build(); + + return new CubbyholeAuthentication(options, vaultClient()); + } + + // … +} +---- +==== + +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] + [[vault.client-ssl]] == Vault Client SSL configuration