Lookup remaining TTL and renewability in CubbyholeAuthentication.

We now perform a self-lookup by default for tokens retrieved from CubbyholeAuthentication to determine the remaining TTL and renewability. Static tokens and wrapped tokens with a TTL associated qualify for self-lookup. Wrapped tokens without a TTL are not self-looked up because all details are already given at the time of reading the wrapped response.

TTL starts at the time of the token creation and this delay can impact the first renewal time so the token can expire and then a renewal happens which fails because of the offset delay.

Fixes gh-88.
This commit is contained in:
Mark Paluch
2017-05-02 09:33:26 +02:00
parent 7fbd790268
commit a7d8cdb229
4 changed files with 242 additions and 52 deletions

View File

@@ -41,6 +41,7 @@ import org.springframework.web.client.RestOperations;
* 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.
*
* <h2>Wrapped token response usage</h2> <strong>Create a Token</strong>
*
* <pre>
@@ -108,14 +109,27 @@ import org.springframework.web.client.RestOperations;
* </code>
* </pre>
*
* <strong>Remaining TTL/Renewability</strong>
* <p>
* Tokens retrieved from Cubbyhole associated with a non-zero TTL start their TTL at the
* time of token creation. That time is not necessarily identical with application
* startup. To compensate for the initial delay, Cubbyhole authentication performs a
* {@link CubbyholeAuthenticationOptions#isSelfLookup() self lookup} for tokens associated
* with a non-zero TTL to retrieve the remaining TTL. Cubbyhole authentication will not
* self-lookup wrapped tokens without a TTL because a zero TTL indicates there is no TTL
* associated.
* <p>
* Non-wrapped tokens do not provide details regarding renewability and TTL by just
* retrieving the token. A self-lookup will lookup renewability and the remaining TTL.
*
* @author Mark Paluch
* @see CubbyholeAuthenticationOptions
* @see RestOperations
* @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
* @see <a href=
* "https://www.vaultproject.io/docs/concepts/response-wrapping.html">Response
* Wrapping</a>
*/
public class CubbyholeAuthentication implements ClientAuthentication {
@@ -146,32 +160,85 @@ public class CubbyholeAuthentication implements ClientAuthentication {
@Override
public VaultToken login() throws VaultException {
Map<String, Object> data = lookupToken();
VaultToken tokenToUse = getToken(data);
if (shouldEnhanceTokenWithSelfLookup(tokenToUse)) {
tokenToUse = augmentWithSelfLookup(tokenToUse);
}
logger.debug("Login successful using Cubbyhole authentication");
return tokenToUse;
}
private Map<String, Object> lookupToken() {
try {
ResponseEntity<VaultResponse> entity = restOperations.exchange(
options.getPath(),
HttpMethod.GET,
new HttpEntity<Object>(VaultHttpHeaders.from(options
.getInitialToken())), VaultResponse.class);
options.getPath(), HttpMethod.GET,
new HttpEntity<Object>(
VaultHttpHeaders.from(options.getInitialToken())),
VaultResponse.class);
Map<String, Object> data = entity.getBody().getData();
VaultToken token = getToken(data);
if (token != null) {
logger.debug("Login successful using Cubbyhole authentication");
return token;
}
throw new VaultException(String.format(
"Cannot retrieve Token from cubbyhole: %s", entity.getStatusCode()));
return entity.getBody().getData();
}
catch (HttpStatusCodeException e) {
throw new VaultException(String.format(
"Cannot retrieve Token from cubbyhole: %s %s", e.getStatusCode(),
VaultResponses.getError(e.getResponseBodyAsString())));
}
}
private boolean shouldEnhanceTokenWithSelfLookup(VaultToken token) {
if (!options.isSelfLookup()) {
return false;
}
if (token instanceof LoginToken) {
LoginToken loginToken = (LoginToken) token;
if (loginToken.getLeaseDuration() == 0) {
return false;
}
}
return true;
}
private VaultToken augmentWithSelfLookup(VaultToken token) {
Map<String, Object> data = lookupSelf(token);
Boolean renewable = (Boolean) data.get("renewable");
Number ttl = (Number) data.get("ttl");
if (renewable != null && renewable) {
return LoginToken.renewable(token.getToken(),
ttl == null ? 0 : ttl.longValue());
}
return LoginToken.of(token.getToken(), ttl == null ? 0 : ttl.longValue());
}
private Map<String, Object> lookupSelf(VaultToken token) {
try {
ResponseEntity<VaultResponse> entity = restOperations.exchange(
"/auth/token/lookup-self", HttpMethod.GET,
new HttpEntity<Object>(VaultHttpHeaders.from(token)),
VaultResponse.class);
return entity.getBody().getData();
}
catch (HttpStatusCodeException e) {
throw new VaultException(String.format(
"Cannot self-lookup Token from cubbyhole: %s %s", e.getStatusCode(),
VaultResponses.getError(e.getResponseBodyAsString())));
}
}
private VaultToken getToken(Map<String, Object> data) {
@@ -184,10 +251,9 @@ public class CubbyholeAuthentication implements ClientAuthentication {
}
if (data == null || data.isEmpty()) {
throw new VaultException(
String.format(
"Cannot retrieve Token from cubbyhole: Response at %s does not contain a token",
options.getPath()));
throw new VaultException(String.format(
"Cannot retrieve Token from cubbyhole: Response at %s does not contain a token",
options.getPath()));
}
if (data.size() == 1) {
@@ -195,9 +261,8 @@ public class CubbyholeAuthentication implements ClientAuthentication {
return VaultToken.of(token);
}
throw new VaultException(
String.format(
"Cannot retrieve Token from cubbyhole: Response at %s does not contain an unique token",
options.getPath()));
throw new VaultException(String.format(
"Cannot retrieve Token from cubbyhole: Response at %s does not contain an unique token",
options.getPath()));
}
}

View File

@@ -45,12 +45,19 @@ public class CubbyholeAuthenticationOptions {
*/
private final boolean wrappedToken;
/**
* Perform a self-lookup using the actual token to obtain the remaining TTL and
* renewability.
*/
private final boolean selfLookup;
private CubbyholeAuthenticationOptions(VaultToken initialToken, String path,
boolean wrappedToken) {
boolean wrappedToken, boolean selfLookup) {
this.initialToken = initialToken;
this.path = path;
this.wrappedToken = wrappedToken;
this.selfLookup = selfLookup;
}
/**
@@ -83,6 +90,17 @@ public class CubbyholeAuthenticationOptions {
return wrappedToken;
}
/**
* @return {@literal true} to perform a token self-lookup after token retrieval to
* determine the remaining TTL and renewability for static wrapped tokens. Defaults to
* {@literal true}.
*
* @since 1.0.1
*/
public boolean isSelfLookup() {
return selfLookup;
}
/**
* Builder for {@link CubbyholeAuthenticationOptions}.
*/
@@ -94,6 +112,8 @@ public class CubbyholeAuthenticationOptions {
private boolean wrappedToken;
private boolean selfLookup = true;
CubbyholeAuthenticationOptionsBuilder() {
}
@@ -103,7 +123,8 @@ public class CubbyholeAuthenticationOptions {
* @param initialToken must not be {@literal null}.
* @return {@code this} {@link CubbyholeAuthenticationOptionsBuilder}.
*/
public CubbyholeAuthenticationOptionsBuilder initialToken(VaultToken initialToken) {
public CubbyholeAuthenticationOptionsBuilder initialToken(
VaultToken initialToken) {
Assert.notNull(initialToken, "Initial Vault Token must not be null");
@@ -138,6 +159,21 @@ public class CubbyholeAuthenticationOptions {
return this;
}
/**
* Configure whether to perform a self-lookup after token retrieval. Defaults to
* {@literal true}.
*
* @param selfLookup {@literal true} to perform a self-lookup or {@literal false}
* to disable it.
* @return {@code this} {@link CubbyholeAuthenticationOptionsBuilder}.
* @since 1.0.1
*/
public CubbyholeAuthenticationOptionsBuilder selfLookup(boolean selfLookup) {
this.selfLookup = selfLookup;
return this;
}
/**
* Build a new {@link CubbyholeAuthenticationOptions} instance. Requires
* {@link #path(String)} or {@link #wrapped()} to be configured.
@@ -148,7 +184,8 @@ public class CubbyholeAuthenticationOptions {
Assert.notNull(initialToken, "Initial Vault Token must not be null");
return new CubbyholeAuthenticationOptions(initialToken, path, wrappedToken);
return new CubbyholeAuthenticationOptions(initialToken, path, wrappedToken,
selfLookup);
}
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.vault.authentication;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
import org.junit.Test;
@@ -22,8 +23,8 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.vault.VaultException;
import org.springframework.vault.client.VaultHttpHeaders;
import org.springframework.vault.client.VaultClients.PrefixAwareUriTemplateHandler;
import org.springframework.vault.client.VaultHttpHeaders;
import org.springframework.vault.support.VaultToken;
import org.springframework.web.client.RestTemplate;
@@ -41,6 +42,8 @@ import static org.springframework.test.web.client.response.MockRestResponseCreat
*/
public class CubbyholeAuthenticationUnitTests {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private RestTemplate restTemplate;
private MockRestServiceServer mockRest;
@@ -57,15 +60,18 @@ public class CubbyholeAuthenticationUnitTests {
@Test
public void shouldLoginUsingWrappedLogin() throws Exception {
String wrappedResponse = "{\"request_id\":\"058222ef-9ab9-ff39-f087-9d5bee64e46d\","
+ "\"auth\":{\"client_token\":\"5e6332cf-f003-6369-8cba-5bce2330f6cc\","
+ "\"lease_duration\":0,"
+ "\"accessor\":\"46b6aebb-187f-932a-26d7-4f3d86a68319\"} }";
mockRest.expect(requestTo("/cubbyhole/response"))
.andExpect(method(HttpMethod.GET))
.andExpect(header(VaultHttpHeaders.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\\\"}}\" } }"));
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON)
.body("{\"data\":{\"response\":"
+ OBJECT_MAPPER.writeValueAsString(wrappedResponse)
+ "} }"));
CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder()
.initialToken(VaultToken.of("hello")).wrapped().build();
@@ -77,21 +83,63 @@ public class CubbyholeAuthenticationUnitTests {
assertThat(login).isInstanceOf(LoginToken.class);
assertThat(login.getToken()).isEqualTo("5e6332cf-f003-6369-8cba-5bce2330f6cc");
LoginToken loginToken = (LoginToken) login;
assertThat(loginToken.isRenewable()).isFalse();
assertThat(loginToken.getLeaseDuration()).isEqualTo(0);
}
@Test
public void shouldLoginUsingWrappedLoginWithSelfLookup() throws Exception {
String wrappedResponse = "{\"request_id\":\"058222ef-9ab9-ff39-f087-9d5bee64e46d\","
+ "\"auth\":{\"client_token\":\"5e6332cf-f003-6369-8cba-5bce2330f6cc\","
+ "\"lease_duration\":10,"
+ "\"accessor\":\"46b6aebb-187f-932a-26d7-4f3d86a68319\"} }";
mockRest.expect(requestTo("/cubbyhole/response"))
.andExpect(method(HttpMethod.GET))
.andExpect(header(VaultHttpHeaders.VAULT_TOKEN, "hello"))
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON)
.body("{\"data\":{\"response\":"
+ OBJECT_MAPPER.writeValueAsString(wrappedResponse)
+ "} }"));
mockRest.expect(requestTo("/auth/token/lookup-self"))
.andExpect(method(HttpMethod.GET))
.andExpect(header(VaultHttpHeaders.VAULT_TOKEN,
"5e6332cf-f003-6369-8cba-5bce2330f6cc"))
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON)
.body("{\"data\": {\n" + " \"creation_ttl\": 600,\n"
+ " \"renewable\": false,\n" + " \"ttl\": 456} }"));
CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder()
.initialToken(VaultToken.of("hello")).wrapped().build();
CubbyholeAuthentication authentication = new CubbyholeAuthentication(options,
restTemplate);
VaultToken login = authentication.login();
assertThat(login).isInstanceOf(LoginToken.class);
assertThat(login.getToken()).isEqualTo("5e6332cf-f003-6369-8cba-5bce2330f6cc");
LoginToken loginToken = (LoginToken) login;
assertThat(loginToken.isRenewable()).isFalse();
assertThat(loginToken.getLeaseDuration()).isEqualTo(456);
}
@Test
public void shouldLoginUsingStoredLogin() throws Exception {
mockRest.expect(requestTo("/cubbyhole/token"))
.andExpect(method(HttpMethod.GET))
mockRest.expect(requestTo("/cubbyhole/token")).andExpect(method(HttpMethod.GET))
.andExpect(header(VaultHttpHeaders.VAULT_TOKEN, "hello"))
.andRespond(
withSuccess()
.contentType(MediaType.APPLICATION_JSON)
.body("{\"data\":{\"mytoken\":\"058222ef-9ab9-ff39-f087-9d5bee64e46d\"} }"));
.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();
.initialToken(VaultToken.of("hello")).path("cubbyhole/token")
.selfLookup(false).build();
CubbyholeAuthentication authentication = new CubbyholeAuthentication(options,
restTemplate);
@@ -102,15 +150,45 @@ public class CubbyholeAuthenticationUnitTests {
assertThat(login.getToken()).isEqualTo("058222ef-9ab9-ff39-f087-9d5bee64e46d");
}
@Test
public void shouldRetrieveRenewabulityUsingStoredLogin() throws Exception {
mockRest.expect(requestTo("/cubbyhole/token")).andExpect(method(HttpMethod.GET))
.andExpect(header(VaultHttpHeaders.VAULT_TOKEN, "hello"))
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON).body(
"{\"data\":{\"mytoken\":\"058222ef-9ab9-ff39-f087-9d5bee64e46d\"} }"));
mockRest.expect(requestTo("/auth/token/lookup-self"))
.andExpect(method(HttpMethod.GET))
.andExpect(header(VaultHttpHeaders.VAULT_TOKEN,
"058222ef-9ab9-ff39-f087-9d5bee64e46d"))
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON)
.body("{\"data\": {\n" + " \"creation_ttl\": 600,\n"
+ " \"renewable\": true,\n" + " \"ttl\": 456} }"));
CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder()
.initialToken(VaultToken.of("hello")).path("cubbyhole/token").build();
CubbyholeAuthentication authentication = new CubbyholeAuthentication(options,
restTemplate);
VaultToken login = authentication.login();
assertThat(login).isInstanceOf(LoginToken.class);
assertThat(login.getToken()).isEqualTo("058222ef-9ab9-ff39-f087-9d5bee64e46d");
LoginToken loginToken = (LoginToken) login;
assertThat(loginToken.isRenewable()).isTrue();
assertThat(loginToken.getLeaseDuration()).isEqualTo(456);
}
@Test
public void shouldFailUsingStoredLoginNoData() throws Exception {
mockRest.expect(requestTo("/cubbyhole/token"))
.andExpect(method(HttpMethod.GET))
mockRest.expect(requestTo("/cubbyhole/token")).andExpect(method(HttpMethod.GET))
.andExpect(header(VaultHttpHeaders.VAULT_TOKEN, "hello"))
.andRespond(
withSuccess().contentType(MediaType.APPLICATION_JSON).body(
"{\"data\":{} }"));
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON)
.body("{\"data\":{} }"));
CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder()
.initialToken(VaultToken.of("hello")).path("cubbyhole/token").build();
@@ -130,12 +208,10 @@ public class CubbyholeAuthenticationUnitTests {
@Test
public void shouldFailUsingStoredMultipleEntries() throws Exception {
mockRest.expect(requestTo("/cubbyhole/token"))
.andExpect(method(HttpMethod.GET))
mockRest.expect(requestTo("/cubbyhole/token")).andExpect(method(HttpMethod.GET))
.andExpect(header(VaultHttpHeaders.VAULT_TOKEN, "hello"))
.andRespond(
withSuccess().contentType(MediaType.APPLICATION_JSON).body(
"{\"data\":{\"key1\":1, \"key2\":2} }"));
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON)
.body("{\"data\":{\"key1\":1, \"key2\":2} }"));
CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder()
.initialToken(VaultToken.of("hello")).path("cubbyhole/token").build();

View File

@@ -418,6 +418,18 @@ class AppConfig extends AbstractVaultConfiguration {
----
====
*Remaining TTL/Renewability*
Tokens retrieved from Cubbyhole associated with a non-zero TTL start their TTL at the
time of token creation. That time is not necessarily identical with application
startup. To compensate for the initial delay, Cubbyhole authentication performs a
self lookup for tokens associated with a non-zero TTL to retrieve the remaining TTL.
Cubbyhole authentication will not self-lookup wrapped tokens without a TTL because a
zero TTL indicates there is no TTL associated.
Non-wrapped tokens do not provide details regarding renewability and TTL by just
retrieving the token. A self-lookup will lookup renewability and the remaining TTL.
See also:
* https://www.vaultproject.io/docs/concepts/tokens.html[Vault Documentation: Tokens]