diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesAuthenticationOptions.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesAuthenticationOptions.java
index c9db71c3..620777d4 100644
--- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesAuthenticationOptions.java
+++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesAuthenticationOptions.java
@@ -26,11 +26,16 @@ import org.springframework.util.Assert;
* Authentication options provide the path, role and jwt supplier.
* {@link KubernetesAuthentication} can be constructed using {@link #builder()}. Instances
* of this class are immutable once constructed.
+ *
+ * Default to obtain a cached token from
+ * {@code /var/run/secrets/kubernetes.io/serviceaccount/token}.
*
* @author Michal Budzyn
* @author Mark Paluch
* @since 2.0
* @see KubernetesAuthentication
+ * @see KubernetesJwtSupplier
+ * @see KubernetesServiceAccountTokenFile
* @see #builder()
*/
public class KubernetesAuthenticationOptions {
@@ -135,6 +140,7 @@ public class KubernetesAuthenticationOptions {
*
* @param jwtSupplier the supplier, must not be {@literal null}.
* @return {@code this} {@link KubernetesAuthenticationOptionsBuilder}.
+ * @see KubernetesJwtSupplier
*/
public KubernetesAuthenticationOptionsBuilder jwtSupplier(
Supplier jwtSupplier) {
@@ -156,7 +162,7 @@ public class KubernetesAuthenticationOptions {
return new KubernetesAuthenticationOptions(path, role,
jwtSupplier == null ? new KubernetesServiceAccountTokenFile()
- : jwtSupplier);
+ .cached() : jwtSupplier);
}
}
}
diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesJwtSupplier.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesJwtSupplier.java
index 1f6c01a8..63a8f091 100644
--- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesJwtSupplier.java
+++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesJwtSupplier.java
@@ -36,4 +36,21 @@ public interface KubernetesJwtSupplier extends Supplier {
*/
@Override
String get();
+
+ /**
+ * Retrieve a cached {@link KubernetesJwtSupplier} that obtains the JWT early and
+ * reuses the token for each {@link #get()} call. This is useful to prevent I/O
+ * operations in e.g. reactive usage.
+ *
+ * Reusing a cached token can lead to authentication failures if the token expires.
+ *
+ * @return a caching {@link KubernetesJwtSupplier}.
+ * @since 2.2
+ */
+ default KubernetesJwtSupplier cached() {
+
+ String jwt = get();
+
+ return () -> jwt;
+ }
}
diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesServiceAccountTokenFile.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesServiceAccountTokenFile.java
index a65c8101..8b087eb3 100644
--- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesServiceAccountTokenFile.java
+++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesServiceAccountTokenFile.java
@@ -44,7 +44,7 @@ public class KubernetesServiceAccountTokenFile implements KubernetesJwtSupplier
*/
public static final String DEFAULT_KUBERNETES_SERVICE_ACCOUNT_TOKEN_FILE = "/var/run/secrets/kubernetes.io/serviceaccount/token";
- private byte[] token;
+ private final Resource resource;
/**
* Create a new {@link KubernetesServiceAccountTokenFile} pointing to the
@@ -92,18 +92,19 @@ public class KubernetesServiceAccountTokenFile implements KubernetesJwtSupplier
Assert.isTrue(resource.exists(),
() -> String.format("Resource %s does not exist", resource));
- try {
- this.token = readToken(resource);
- }
- catch (IOException e) {
- throw new VaultException(String.format(
- "Kube JWT token retrieval from %s failed", resource), e);
- }
+ this.resource = resource;
}
@Override
public String get() {
- return new String(token, StandardCharsets.US_ASCII);
+
+ try {
+ return new String(readToken(this.resource), StandardCharsets.US_ASCII);
+ }
+ catch (IOException e) {
+ throw new VaultException(String.format(
+ "Kube JWT token retrieval from %s failed", this.resource), e);
+ }
}
/**
@@ -113,7 +114,7 @@ public class KubernetesServiceAccountTokenFile implements KubernetesJwtSupplier
* @return the new byte array that has been copied to (possibly empty).
* @throws IOException in case of I/O errors.
*/
- protected static byte[] readToken(Resource resource) throws IOException {
+ private static byte[] readToken(Resource resource) throws IOException {
Assert.notNull(resource, "Resource must not be null");
diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTestBase.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTestBase.java
index e667c89a..87ce8545 100644
--- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTestBase.java
+++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTestBase.java
@@ -48,26 +48,30 @@ class AppRoleAuthenticationIntegrationTestBase extends IntegrationTestSupport {
prepare().mountAuth("approle");
}
- getVaultOperations().doWithSession(restOperations -> {
+ getVaultOperations().doWithSession(
+ restOperations -> {
- Map withSecretId = new HashMap();
- withSecretId.put("policies", "dummy"); // policy
- withSecretId.put("bound_cidr_list", "0.0.0.0/0");
- withSecretId.put("bind_secret_id", "true");
+ Map withSecretId = new HashMap<>();
+ withSecretId.put("policies", "dummy");
+ withSecretId.put("bound_cidr_list", "0.0.0.0/0");
+ withSecretId.put("bind_secret_id", "true");
+ withSecretId.put("token_ttl", "60s");
+ withSecretId.put("token_max_ttl", "60s");
- restOperations.postForEntity("auth/approle/role/with-secret-id",
- withSecretId, Map.class);
+ restOperations.postForEntity("auth/approle/role/with-secret-id",
+ withSecretId, Map.class);
- Map noSecretIdRole = new HashMap();
- noSecretIdRole.put("policies", "dummy"); // policy
- noSecretIdRole.put("bound_cidr_list", "0.0.0.0/0");
- noSecretIdRole.put("bind_secret_id", "false");
+ Map noSecretIdRole = new HashMap<>();
+ noSecretIdRole.put("policies", "dummy"); // policy
+ noSecretIdRole.put("bound_cidr_list", "0.0.0.0/0");
+ noSecretIdRole.put("bind_secret_id", "false");
+ noSecretIdRole.put("max_ttl", "60s");
- restOperations.postForEntity("auth/approle/role/no-secret-id",
- noSecretIdRole, Map.class);
+ restOperations.postForEntity("auth/approle/role/no-secret-id",
+ noSecretIdRole, Map.class);
- return null;
- });
+ return null;
+ });
}
VaultOperations getVaultOperations() {
@@ -77,8 +81,7 @@ class AppRoleAuthenticationIntegrationTestBase extends IntegrationTestSupport {
String getRoleId(String roleName) {
return (String) getVaultOperations()
.read(String.format("auth/approle/role/%s/role-id", roleName))
- .getRequiredData()
- .get("role_id");
+ .getRequiredData().get("role_id");
}
VaultToken generateWrappedSecretIdResponse() {
diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationStepsIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationStepsIntegrationTests.java
index 16549f8f..5864627a 100644
--- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationStepsIntegrationTests.java
+++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationStepsIntegrationTests.java
@@ -19,6 +19,7 @@ import java.util.Collections;
import org.junit.jupiter.api.Test;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.vault.VaultException;
import org.springframework.vault.authentication.AppRoleAuthenticationOptions.RoleId;
import org.springframework.vault.authentication.AppRoleAuthenticationOptions.SecretId;
@@ -40,7 +41,8 @@ class AppRoleAuthenticationStepsIntegrationTests extends
AppRoleAuthenticationIntegrationTestBase {
@Test
- void authenticationStepsShouldAuthenticateWithWrappedSecretId() {
+ void authenticationStepsShouldAuthenticateWithWrappedSecretId()
+ throws InterruptedException {
String roleId = getRoleId("with-secret-id");
VaultToken unwrappingToken = generateWrappedSecretIdResponse();
@@ -53,7 +55,7 @@ class AppRoleAuthenticationStepsIntegrationTests extends
AppRoleAuthentication.createAuthenticationSteps(options), prepare()
.getRestTemplate());
- assertThat(executor.login()).isNotNull();
+
}
@Test
@@ -77,7 +79,7 @@ class AppRoleAuthenticationStepsIntegrationTests extends
}
@Test
- void shouldAuthenticateWithFullPullMode() {
+ void shouldAuthenticateWithFullPullMode() throws InterruptedException {
AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder()
.appRole("with-secret-id").roleId(RoleId.pull(Settings.token()))
@@ -87,7 +89,18 @@ class AppRoleAuthenticationStepsIntegrationTests extends
AppRoleAuthentication.createAuthenticationSteps(options), prepare()
.getRestTemplate());
- assertThat(executor.login()).isNotNull();
+ ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
+ taskScheduler.afterPropertiesSet();
+ LifecycleAwareSessionManager sessionManager = new LifecycleAwareSessionManager(
+ executor, taskScheduler, prepare().getRestTemplate());
+
+ VaultToken sessionToken = sessionManager.getSessionToken();
+ System.out.println(sessionToken);
+
+ Thread.sleep(90000);
+ VaultToken sessionToken2 = sessionManager.getSessionToken();
+ System.out.println(sessionToken2);
+
}
@Test
diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubernetesAuthenticationUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubernetesAuthenticationUnitTests.java
index 3016f4eb..272bb121 100644
--- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubernetesAuthenticationUnitTests.java
+++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubernetesAuthenticationUnitTests.java
@@ -15,6 +15,8 @@
*/
package org.springframework.vault.authentication;
+import java.util.concurrent.atomic.AtomicReference;
+
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -91,4 +93,28 @@ class KubernetesAuthenticationUnitTests {
assertThatExceptionOfType(VaultException.class).isThrownBy(
() -> new KubernetesAuthentication(options, restTemplate).login());
}
+
+ @Test
+ void shouldReuseCachedToken() {
+
+ AtomicReference token = new AtomicReference<>("foo");
+ KubernetesAuthenticationOptions options = KubernetesAuthenticationOptions
+ .builder().role("hello") //
+ .jwtSupplier(((KubernetesJwtSupplier) token::get).cached()).build();
+
+ token.set("bar");
+
+ mockRest.expect(requestTo("/auth/kubernetes/login"))
+ .andExpect(method(HttpMethod.POST))
+ .andExpect(jsonPath("$.role").value("hello"))
+ .andExpect(jsonPath("$.jwt").value("foo"))
+ .andRespond(
+ withSuccess().contentType(MediaType.APPLICATION_JSON).body(
+ "{" + "\"auth\":{\"client_token\":\"my-token\"}" + "}"));
+
+ KubernetesAuthentication authentication = new KubernetesAuthentication(options,
+ restTemplate);
+
+ authentication.login();
+ }
}
diff --git a/src/main/asciidoc/reference/authentication.adoc b/src/main/asciidoc/reference/authentication.adoc
index c4121b58..e9fb3c9f 100644
--- a/src/main/asciidoc/reference/authentication.adoc
+++ b/src/main/asciidoc/reference/authentication.adoc
@@ -184,7 +184,7 @@ public class MyUserIdMechanism implements AppIdUserIdMechanism {
@Override
public String createUserId() {
- String userId = ...
+ String userId = …
return userId;
}
}
@@ -694,9 +694,9 @@ See also:
Vault supports since 0.8.3 https://www.vaultproject.io/docs/auth/kubernetes.html[kubernetes]-based authentication using Kubernetes tokens.
Using Kubernetes authentication requires a Kubernetes Service Account Token,
-usually mounted at `/var/run/secrets/kubernetes.io/serviceaccount/token`. The file contains
-the token which is read and sent to Vault. Vault verifies its validity using Kubernets' API
-during login.
+usually mounted at `/var/run/secrets/kubernetes.io/serviceaccount/token`.
+The file contains the token which is read and sent to Vault.
+Vault verifies its validity using Kubernetes' API during login.
Configuring Kubernetes authentication requires at least the role name to be provided:
@@ -712,7 +712,7 @@ class AppConfig extends AbstractVaultConfiguration {
public ClientAuthentication clientAuthentication() {
KubernetesAuthenticationOptions options = KubernetesAuthenticationOptions.builder()
- .role(…).build();
+ .role(…).jwtSupplier(…).build();
return new KubernetesAuthentication(options, restOperations());
}