Support 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. Fixes gh-3
This commit is contained in:
@@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* <h2>Wrapped token response usage</h2> <strong>Create a Token</strong>
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
$ 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
|
||||
* </code>
|
||||
* </pre>
|
||||
*
|
||||
* <strong>Setup {@link CubbyholeAuthentication}</strong>
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions
|
||||
.builder()
|
||||
.initialToken(VaultToken.of("397ccb93-ff6c-b17b-9389-380b01ca2645"))
|
||||
.wrapped()
|
||||
.build();
|
||||
CubbyholeAuthentication authentication = new CubbyholeAuthentication(options, vaultClient);
|
||||
* </code>
|
||||
* </pre>
|
||||
*
|
||||
* <h2>Stored token response usage</h2> <strong>Create a Token</strong>
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
$ 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
|
||||
* </code>
|
||||
* </pre>
|
||||
*
|
||||
* <strong>Setup {@link CubbyholeAuthentication}</strong>
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions
|
||||
.builder()
|
||||
.initialToken(VaultToken.of("895cb88b-aef4-0e33-ba65-d50007290780"))
|
||||
.path("cubbyhole/token")
|
||||
.build();
|
||||
CubbyholeAuthentication authentication = new CubbyholeAuthentication(options, vaultClient);
|
||||
* </code>
|
||||
* </pre>
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @see CubbyholeAuthenticationOptions
|
||||
* @see <a href="https://www.vaultproject.io/docs/auth/token.html">Auth Backend: Token</a>
|
||||
* @see <a href="https://www.vaultproject.io/docs/secrets/cubbyhole/index.html">Cubbyhole Secret Backend</a>
|
||||
* @see <a href="https://www.vaultproject.io/docs/concepts/response-wrapping.html">Response Wrapping</a>
|
||||
*/
|
||||
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<VaultResponse> entity = vaultClient.getForEntity(options.getPath(), options.getInitialToken(),
|
||||
VaultResponse.class);
|
||||
|
||||
if (entity.isSuccessful() && entity.hasBody()) {
|
||||
|
||||
VaultResponse body = entity.getBody();
|
||||
Map<String, Object> 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<VaultResponse> entity, Map<String, Object> 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()));
|
||||
}
|
||||
}
|
||||
@@ -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}.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
* <p>
|
||||
* {@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}.
|
||||
* <p>
|
||||
* 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:
|
||||
*
|
||||
*
|
||||
* <pre class="code">
|
||||
* ParameterizedTypeReference<List<MyBean>> myBean = new ParameterizedTypeReference<List<MyBean>>() {};
|
||||
* ResponseEntity<List<MyBean>> response = client.exchange("http://example.com", HttpMethod.GET, null, myBean, null);
|
||||
* </pre>
|
||||
*
|
||||
*
|
||||
* @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<String, ?> 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> T unwrap(final String wrappedResponse, Class<T> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Object>(requestEntityToUse.getBody(), httpHeaders);
|
||||
} else {
|
||||
requestEntityToUse = new HttpEntity<Object>(httpHeaders);
|
||||
|
||||
@@ -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<VaultResponse> response = prepare().getVaultOperations()
|
||||
.doWithVault(new SessionCallback<VaultResponseEntity<VaultResponse>>() {
|
||||
@Override
|
||||
public VaultResponseEntity<VaultResponse> doWithVault(VaultSession session) {
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add("X-Vault-Wrap-TTL", "10m");
|
||||
|
||||
return session.exchange("auth/token/create", HttpMethod.POST, new HttpEntity<Object>(headers),
|
||||
VaultResponse.class, null);
|
||||
}
|
||||
});
|
||||
|
||||
Map<String, String> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user