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]