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
index 435f4ff6..bd8a5347 100644
--- 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
@@ -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.
+ *
*
Wrapped token response usage Create a Token
*
*
@@ -108,14 +109,27 @@ import org.springframework.web.client.RestOperations;
*
*
*
+ * 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
+ * {@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.
+ *
+ * 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 Auth Backend: Token
* @see Cubbyhole
* Secret Backend
- * @see Response
+ * @see Response
* Wrapping
*/
public class CubbyholeAuthentication implements ClientAuthentication {
@@ -146,32 +160,85 @@ public class CubbyholeAuthentication implements ClientAuthentication {
@Override
public VaultToken login() throws VaultException {
+ Map data = lookupToken();
+
+ VaultToken tokenToUse = getToken(data);
+
+ if (shouldEnhanceTokenWithSelfLookup(tokenToUse)) {
+ tokenToUse = augmentWithSelfLookup(tokenToUse);
+ }
+
+ logger.debug("Login successful using Cubbyhole authentication");
+ return tokenToUse;
+ }
+
+ private Map lookupToken() {
+
try {
ResponseEntity entity = restOperations.exchange(
- options.getPath(),
- HttpMethod.GET,
- new HttpEntity(VaultHttpHeaders.from(options
- .getInitialToken())), VaultResponse.class);
+ options.getPath(), HttpMethod.GET,
+ new HttpEntity(
+ VaultHttpHeaders.from(options.getInitialToken())),
+ VaultResponse.class);
- Map 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 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 lookupSelf(VaultToken token) {
+
+ try {
+ ResponseEntity entity = restOperations.exchange(
+ "/auth/token/lookup-self", HttpMethod.GET,
+ new HttpEntity(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 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()));
}
}
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
index 95a89027..40831a2b 100644
--- 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
@@ -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);
}
}
}
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
index 2697bca5..0a0015b2 100644
--- 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
@@ -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();
diff --git a/src/main/asciidoc/reference/authentication.adoc b/src/main/asciidoc/reference/authentication.adoc
index 8183c5d0..f2a06a0a 100644
--- a/src/main/asciidoc/reference/authentication.adoc
+++ b/src/main/asciidoc/reference/authentication.adoc
@@ -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]