+ * In case this test should fail because of SSL make sure you run the test within the
+ * spring-cloud-vault-config/spring-cloud-vault-config directory as the keystore is
+ * referenced with {@code ../work/keystore.jks}.
+ *
+ * @author Issam El-atif
+ */
+class VaultConfigLdapTests {
+
+ private static InMemoryDirectoryServer ldapServer;
+
+ VaultTestContextRunner contextRunner = VaultTestContextRunner.of(VaultConfigLdapTests.class)
+ .withAuthentication(VaultProperties.AuthenticationMethod.LDAP)
+ .withConfiguration(VaultConfigLdapTests.TestApplication.class)
+ .withProperties("spring.cloud.vault.ldap.username", "testuser")
+ .withProperties("spring.cloud.vault.ldap.password", "testpass")
+ .withSettings(VaultTestContextRunner.TestSettings::bootstrap);
+
+ @BeforeAll
+ static void beforeClass() throws LDAPException, LDIFException {
+ // LDAP server
+ ldapServer = createLdapServer();
+ ldapServer.startListening();
+
+ // Vault config
+ VaultRule vaultRule = new VaultRule();
+ vaultRule.before();
+
+ assumeTrue(vaultRule.prepare().getVersion().isGreaterThanOrEqualTo(Version.parse("0.8.0")));
+
+ VaultProperties vaultProperties = Settings.createVaultProperties();
+
+ if (!vaultRule.prepare().hasAuth(vaultProperties.getLdap().getPath())) {
+ vaultRule.prepare().mountAuth(vaultProperties.getLdap().getPath());
+ }
+
+ VaultOperations vaultOperations = vaultRule.prepare().getVaultOperations();
+
+ String rules = """
+ { "name": "testpolicy",
+ "path": {
+ "*": { "policy": "read" }
+ }
+ }""";
+
+ vaultOperations.write("sys/policy/testpolicy", Collections.singletonMap("rules", rules));
+
+ vaultOperations.write("secret/" + VaultConfigLdapTests.class.getSimpleName(),
+ Collections.singletonMap("vault.value", "foo"));
+
+ Map
+ * This test uses Testcontainers to run Vault and MockWebServer to simulate the Okta
+ * authentication API. Testcontainers is required here because Vault's Okta auth method
+ * makes outbound HTTPS calls to the Okta API (e.g., {@code https://mock.localhost:8443}).
+ * Without Testcontainers, this would require modifying {@code /etc/hosts} to resolve
+ * {@code mock.localhost} to {@code 127.0.0.1}, which is not feasible in automated tests.
+ * Testcontainers solves this DNS limitation by using
+ * {@code withExtraHost("mock.localhost", "host-gateway")}, which adds a host entry inside
+ * the container that maps {@code mock.localhost} to the host machine, allowing Vault to
+ * reach the MockWebServer running on the host.
+ *
+ * @author Issam El-atif
+ */
+@Testcontainers
+class VaultConfigOktaTests {
+
+ private static final String VAULT_TOKEN = "00000000-0000-0000-0000-000000000000";
+
+ private static final int MOCK_SERVER_PORT = 8443;
+
+ private static final String OKTA_USERNAME = "testuser";
+
+ private static final String OKTA_PASSWORD = "testpass";
+
+ private static final String OKTA_TOTP = "totp";
+
+ private static MockWebServer oktaServer;
+
+ private static final HeldCertificate caCertificate;
+
+ private static final HeldCertificate serverCertificate;
+
+ private static final Path caCertFile;
+
+ @Container
+ static VaultContainer> vaultContainer;
+
+ static {
+ try {
+ caCertificate = new HeldCertificate.Builder().certificateAuthority(0).build();
+
+ serverCertificate = new HeldCertificate.Builder().addSubjectAlternativeName("mock.localhost")
+ .signedBy(caCertificate)
+ .build();
+
+ caCertFile = Files.createTempFile("ca-cert", ".pem");
+ Files.writeString(caCertFile, caCertificate.certificatePem());
+
+ vaultContainer = new VaultContainer<>("hashicorp/vault:1.13.3").withVaultToken(VAULT_TOKEN)
+ .withExtraHost("mock.localhost", "host-gateway")
+ .withAccessToHost(true)
+ .withFileSystemBind(caCertFile.toString(), "/etc/ssl/certs/mock-ca.pem", BindMode.READ_ONLY)
+ .withEnv("SSL_CERT_FILE", "/etc/ssl/certs/mock-ca.pem");
+ }
+ catch (IOException ex) {
+ throw new RuntimeException("Failed to create certificates", ex);
+ }
+ }
+
+ VaultTestContextRunner contextRunner = VaultTestContextRunner.of(VaultConfigOktaTests.class)
+ .withAuthentication(VaultProperties.AuthenticationMethod.OKTA)
+ .withConfiguration(TestApplication.class)
+ .withProperties("spring.cloud.vault.uri", vaultContainer.getHttpHostAddress())
+ .withProperties("spring.cloud.vault.okta.username", OKTA_USERNAME)
+ .withProperties("spring.cloud.vault.okta.password", OKTA_PASSWORD)
+ .withProperties("spring.cloud.vault.okta.totp", OKTA_TOTP)
+ .withSettings(VaultTestContextRunner.TestSettings::bootstrap);
+
+ @BeforeAll
+ static void beforeClass() throws Exception {
+ oktaServer = createMockOktaServer();
+ oktaServer.start(MOCK_SERVER_PORT);
+
+ VaultTemplate vaultTemplate = createVaultTemplate();
+ configureVaultSecrets(vaultTemplate);
+ configureOktaAuth(vaultTemplate);
+ }
+
+ private static VaultTemplate createVaultTemplate() {
+ VaultEndpoint endpoint = VaultEndpoint.from(vaultContainer.getHttpHostAddress());
+ return new VaultTemplate(endpoint, new SimpleClientHttpRequestFactory(),
+ () -> new TokenAuthentication(VAULT_TOKEN).login());
+ }
+
+ private static void configureVaultSecrets(VaultTemplate vaultTemplate) {
+ vaultTemplate.opsForSys().unmount("secret");
+ vaultTemplate.opsForSys()
+ .mount("secret", VaultMount.builder().type("kv").options(Collections.singletonMap("version", "1")).build());
+
+ vaultTemplate.write("secret/" + VaultConfigOktaTests.class.getSimpleName(),
+ Collections.singletonMap("vault.value", "foo"));
+ }
+
+ private static void configureOktaAuth(VaultTemplate vaultTemplate) {
+ vaultTemplate.opsForSys().authMount("okta", VaultMount.create("okta"));
+
+ String rules = """
+ path "*" {
+ capabilities = ["read", "list"]
+ }
+ """;
+ vaultTemplate.write("sys/policy/testpolicy", Collections.singletonMap("rules", rules));
+
+ // Vault constructs Okta URL as: https://{org_name}.{base_url}/api/v1/authn
+ vaultTemplate.write("auth/okta/config",
+ Map.of("org_name", "mock", "base_url", "localhost:" + MOCK_SERVER_PORT));
+
+ vaultTemplate.write("auth/okta/users/" + OKTA_USERNAME, Map.of("policies", "testpolicy"));
+ }
+
+ private static MockWebServer createMockOktaServer() {
+ MockWebServer server = new MockWebServer();
+
+ HandshakeCertificates serverCertificates = new HandshakeCertificates.Builder()
+ .heldCertificate(serverCertificate, caCertificate.certificate())
+ .build();
+
+ server.useHttps(serverCertificates.sslSocketFactory(), false);
+ server.setDispatcher(new OktaDispatcher());
+
+ return server;
+ }
+
+ @AfterAll
+ static void afterClass() throws IOException {
+ if (oktaServer != null) {
+ oktaServer.shutdown();
+ }
+ if (caCertFile != null) {
+ Files.deleteIfExists(caCertFile);
+ }
+ }
+
+ @Test
+ void contextLoads() {
+ this.contextRunner.run(ctx -> {
+ TestApplication application = ctx.getBean(TestApplication.class);
+ assertThat(application.configValue).isEqualTo("foo");
+ });
+ }
+
+ static class OktaDispatcher extends Dispatcher {
+
+ @NotNull
+ @Override
+ public MockResponse dispatch(RecordedRequest request) {
+ if ("/api/v1/authn".equals(request.getPath()) && "POST".equals(request.getMethod())) {
+ String requestBody = request.getBody().readUtf8();
+
+ if (requestBody.contains("\"username\":\"" + OKTA_USERNAME + "\"")
+ && requestBody.contains("\"password\":\"" + OKTA_PASSWORD + "\"")) {
+
+ return new MockResponse().setResponseCode(200)
+ .setHeader("Content-Type", "application/json")
+ .setBody("""
+ {
+ "status": "SUCCESS",
+ "sessionToken": "mock-session-token"
+ }
+ """);
+ }
+
+ return new MockResponse().setResponseCode(401).setHeader("Content-Type", "application/json").setBody("""
+ {
+ "errorCode": "E0000004",
+ "errorSummary": "Authentication failed"
+ }
+ """);
+ }
+
+ return new MockResponse().setResponseCode(404);
+ }
+
+ }
+
+ @SpringBootApplication
+ public static class TestApplication {
+
+ @Value("${vault.value}")
+ String configValue;
+
+ public static void main(String[] args) {
+ SpringApplication.run(TestApplication.class, args);
+ }
+
+ }
+
+}
diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigRadiusTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigRadiusTests.java
new file mode 100644
index 00000000..d0bfe904
--- /dev/null
+++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigRadiusTests.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2016-present 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
+ *
+ * https://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.cloud.vault.config;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.images.builder.Transferable;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.vault.VaultContainer;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.vault.util.VaultTestContextRunner;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.vault.authentication.TokenAuthentication;
+import org.springframework.vault.client.VaultEndpoint;
+import org.springframework.vault.core.VaultTemplate;
+import org.springframework.vault.support.VaultMount;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Integration test using config infrastructure with RADIUS authentication.
+ *
+ *
+ * This test uses Testcontainers to run both Vault and FreeRADIUS in a shared Docker
+ * network. Testcontainers is required here because Vault's RADIUS auth method uses UDP
+ * protocol for authentication. UDP port forwarding from Docker to the host is unreliable,
+ * so both containers must be in the same network to communicate directly.
+ *
+ * @author Issam El-atif
+ */
+@Testcontainers
+class VaultConfigRadiusTests {
+
+ private static final String VAULT_TOKEN = "00000000-0000-0000-0000-000000000000";
+
+ private static final String RADIUS_USERNAME = "testuser";
+
+ private static final String RADIUS_PASSWORD = "testpass";
+
+ private static final String RADIUS_SECRET = "testing123";
+
+ private static final String RADIUS_ALIAS = "freeradius";
+
+ static Network network = Network.newNetwork();
+
+ // FreeRADIUS users config with test user
+ private static final String USERS_CONFIG = "%s Cleartext-Password := \"%s\"%n".formatted(RADIUS_USERNAME,
+ RADIUS_PASSWORD);
+
+ // Custom FreeRADIUS clients config allowing any client
+ private static final String CLIENTS_CONFIG = """
+ client localhost {
+ ipaddr = 127.0.0.1
+ secret = testing123
+ }
+ client any {
+ ipaddr = 0.0.0.0/0
+ secret = %s
+ }
+ """.formatted(RADIUS_SECRET);
+
+ @Container
+ static GenericContainer> radiusContainer = new GenericContainer<>("freeradius/freeradius-server:latest")
+ .withNetwork(network)
+ .withNetworkAliases(RADIUS_ALIAS)
+ .withCommand("-X")
+ .withCopyToContainer(Transferable.of(USERS_CONFIG), "/etc/freeradius/mods-config/files/authorize")
+ .withCopyToContainer(Transferable.of(CLIENTS_CONFIG), "/etc/freeradius/clients.conf")
+ .waitingFor(Wait.forLogMessage(".*Ready to process requests.*\\n", 1));
+
+ @Container
+ static VaultContainer> vaultContainer = new VaultContainer<>("hashicorp/vault:1.13.3").withVaultToken(VAULT_TOKEN)
+ .withNetwork(network);
+
+ VaultTestContextRunner contextRunner = VaultTestContextRunner.of(VaultConfigRadiusTests.class)
+ .withAuthentication(VaultProperties.AuthenticationMethod.RADIUS)
+ .withConfiguration(TestApplication.class)
+ .withProperties("spring.cloud.vault.uri", vaultContainer.getHttpHostAddress())
+ .withProperties("spring.cloud.vault.radius.username", RADIUS_USERNAME)
+ .withProperties("spring.cloud.vault.radius.password", RADIUS_PASSWORD)
+ .withSettings(VaultTestContextRunner.TestSettings::bootstrap);
+
+ @BeforeAll
+ static void beforeClass() {
+ VaultTemplate vaultTemplate = createVaultTemplate();
+ configureVaultSecrets(vaultTemplate);
+ configureRadiusAuth(vaultTemplate);
+ }
+
+ private static VaultTemplate createVaultTemplate() {
+ VaultEndpoint endpoint = VaultEndpoint.from(vaultContainer.getHttpHostAddress());
+ return new VaultTemplate(endpoint, new SimpleClientHttpRequestFactory(),
+ () -> new TokenAuthentication(VAULT_TOKEN).login());
+ }
+
+ private static void configureVaultSecrets(VaultTemplate vaultTemplate) {
+ vaultTemplate.opsForSys().unmount("secret");
+ vaultTemplate.opsForSys()
+ .mount("secret", VaultMount.builder().type("kv").options(Collections.singletonMap("version", "1")).build());
+
+ vaultTemplate.write("secret/" + VaultConfigRadiusTests.class.getSimpleName(),
+ Collections.singletonMap("vault.value", "foo"));
+ }
+
+ private static void configureRadiusAuth(VaultTemplate vaultTemplate) {
+ vaultTemplate.opsForSys().authMount("radius", VaultMount.create("radius"));
+
+ String rules = """
+ path "*" {
+ capabilities = ["read", "list"]
+ }
+ """;
+ vaultTemplate.write("sys/policy/testpolicy", Collections.singletonMap("rules", rules));
+
+ // Configure RADIUS auth to point to the FreeRADIUS container by network alias
+ vaultTemplate.write("auth/radius/config", Map.of("host", RADIUS_ALIAS, "secret", RADIUS_SECRET));
+
+ vaultTemplate.write("auth/radius/users/" + RADIUS_USERNAME, Map.of("policies", "testpolicy"));
+ }
+
+ @Test
+ void contextLoads() {
+ this.contextRunner.run(ctx -> {
+ TestApplication application = ctx.getBean(TestApplication.class);
+ assertThat(application.configValue).isEqualTo("foo");
+ });
+ }
+
+ @SpringBootApplication
+ public static class TestApplication {
+
+ @Value("${vault.value}")
+ String configValue;
+
+ public static void main(String[] args) {
+ SpringApplication.run(TestApplication.class, args);
+ }
+
+ }
+
+}
diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigUserPassTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigUserPassTests.java
new file mode 100644
index 00000000..16a97285
--- /dev/null
+++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigUserPassTests.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2016-present 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
+ *
+ * https://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.cloud.vault.config;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.vault.util.Settings;
+import org.springframework.cloud.vault.util.VaultRule;
+import org.springframework.cloud.vault.util.VaultTestContextRunner;
+import org.springframework.cloud.vault.util.Version;
+import org.springframework.vault.core.VaultOperations;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assume.assumeTrue;
+
+/**
+ * Integration test using config infrastructure with UserPass authentication.
+ *
+ *
+ * In case this test should fail because of SSL make sure you run the test within the
+ * spring-cloud-vault-config/spring-cloud-vault-config directory as the keystore is
+ * referenced with {@code ../work/keystore.jks}.
+ *
+ * @author Issam El-atif
+ */
+class VaultConfigUserPassTests {
+
+ VaultTestContextRunner contextRunner = VaultTestContextRunner.of(VaultConfigUserPassTests.class)
+ .withAuthentication(VaultProperties.AuthenticationMethod.USERPASS)
+ .withConfiguration(VaultConfigUserPassTests.TestApplication.class)
+ .withProperties("spring.cloud.vault.userpass.username", "testuser")
+ .withProperties("spring.cloud.vault.userpass.password", "testpass")
+ .withSettings(VaultTestContextRunner.TestSettings::bootstrap);
+
+ @BeforeAll
+ static void beforeClass() {
+
+ VaultRule vaultRule = new VaultRule();
+ vaultRule.before();
+
+ assumeTrue(vaultRule.prepare().getVersion().isGreaterThanOrEqualTo(Version.parse("0.8.0")));
+
+ VaultProperties vaultProperties = Settings.createVaultProperties();
+
+ if (!vaultRule.prepare().hasAuth(vaultProperties.getUserpass().getPath())) {
+ vaultRule.prepare().mountAuth(vaultProperties.getUserpass().getPath());
+ }
+
+ VaultOperations vaultOperations = vaultRule.prepare().getVaultOperations();
+
+ String rules = """
+ { "name": "testpolicy",
+ "path": {
+ "*": { "policy": "read" }
+ }
+ }""";
+
+ vaultOperations.write("sys/policy/testpolicy", Collections.singletonMap("rules", rules));
+
+ vaultOperations.write("secret/" + VaultConfigUserPassTests.class.getSimpleName(),
+ Collections.singletonMap("vault.value", "foo"));
+
+ Map