Polishing.

Refactor User/Pass authentication to be used with LDAP, Okta, and RADIUS. Remove LdapAuthentication to avoid duplications.

See: gh-668.
This commit is contained in:
Mark Paluch
2021-11-19 10:54:18 +01:00
parent 1b33f4e195
commit 2c4862b425
11 changed files with 395 additions and 397 deletions

View File

@@ -1,79 +0,0 @@
/*
* Copyright 2021 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.vault.authentication;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;
import org.springframework.vault.VaultException;
import org.springframework.vault.client.VaultResponses;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultToken;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestOperations;
import static java.util.Collections.singletonMap;
import static org.springframework.vault.authentication.AuthenticationUtil.getLoginPath;
/**
* LDAP implementation of {@link ClientAuthentication}.
*
* @author Mikhael Sokolov
* @see LdapAuthenticationOptions
* @see RestOperations
* @see <a href="https://www.vaultproject.io/docs/auth/ldap">LDAP</a>
* @since 2.4
*/
public class LdapAuthentication implements ClientAuthentication, AuthenticationStepsFactory {
private static final Log logger = LogFactory.getLog(LdapAuthentication.class);
private final LdapAuthenticationOptions options;
private final RestOperations restOperations;
public LdapAuthentication(LdapAuthenticationOptions options, RestOperations restOperations) {
Assert.notNull(options, "LdapAuthenticationOptions must not be null");
Assert.notNull(restOperations, "RestOperations must not be null");
this.options = options;
this.restOperations = restOperations;
}
@Override
public VaultToken login() throws VaultException {
return createTokenUsingLdapAuthentication();
}
@Override
public AuthenticationSteps getAuthenticationSteps() {
return AuthenticationSteps
.fromSupplier(() -> singletonMap("password", options.getPassword()))
.login(String.format("%s/%s", getLoginPath(options.getPath()), options.getUsername()));
}
private VaultToken createTokenUsingLdapAuthentication() {
try {
VaultResponse response = restOperations.postForObject(String.format("%s/%s", getLoginPath(options.getPath()), options.getUsername()), singletonMap("password", options.getPassword()), VaultResponse.class);
logger.debug("Login successful using LDAP credentials");
return LoginTokenUtil.from(response.getAuth());
} catch (HttpStatusCodeException e) {
throw new VaultException(String.format("Cannot login using LDAP: %s", VaultResponses.getError(e.getResponseBodyAsString())), e);
}
}
}

View File

@@ -1,101 +0,0 @@
/*
* Copyright 2021 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.vault.authentication;
import org.springframework.util.Assert;
import javax.annotation.Nullable;
/**
* @author Mikhael Sokolov
*/
public class LdapAuthenticationOptions {
public static final String DEFAULT_LDAP_AUTHENTICATION_PATH = "ldap";
/**
* Path of the ldap authentication backend mount.
*/
private final String path;
/**
* Username of the ldap authentication backend mount.
*/
private final String username;
/**
* Password of the ldap authentication backend mount.
*/
private final CharSequence password;
private LdapAuthenticationOptions(String username, CharSequence password, String path) {
this.username = username;
this.password = password;
this.path = path;
}
public static LdapAuthenticationOptionsBuilder builder() {
return new LdapAuthenticationOptionsBuilder();
}
public String getUsername() {
return username;
}
public CharSequence getPassword() {
return password;
}
public String getPath() {
return path;
}
public static class LdapAuthenticationOptionsBuilder {
@Nullable
private String username;
@Nullable
private CharSequence password;
private String path = DEFAULT_LDAP_AUTHENTICATION_PATH;
LdapAuthenticationOptionsBuilder() {
}
public LdapAuthenticationOptionsBuilder username(String username) {
this.username = username;
return this;
}
public LdapAuthenticationOptionsBuilder password(CharSequence password) {
this.password = password;
return this;
}
public LdapAuthenticationOptionsBuilder path(String path) {
this.path = path;
return this;
}
public LdapAuthenticationOptions build() {
Assert.notNull(this.username, "Username must not be null");
Assert.notNull(this.password, "Password must not be null");
return new LdapAuthenticationOptions(username, password, path);
}
}
}

View File

@@ -15,8 +15,12 @@
*/
package org.springframework.vault.authentication;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;
import org.springframework.vault.VaultException;
import org.springframework.vault.client.VaultResponses;
@@ -25,55 +29,94 @@ import org.springframework.vault.support.VaultToken;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestOperations;
import static java.util.Collections.singletonMap;
import static org.springframework.vault.authentication.AuthenticationUtil.getLoginPath;
import static org.springframework.vault.authentication.AuthenticationUtil.*;
/**
* Username and password implementation of {@link ClientAuthentication}.
* Username and password implementation of {@link ClientAuthentication}. Can be used for
* {@code userpass}, {@code ldap}, {@code okta}, and {@code radius} authentication
* backends.
*
* @author Mikhael Sokolov
* @author Mark Paluch
* @see UsernamePasswordAuthenticationOptions
* @see RestOperations
* @see <a href="https://www.vaultproject.io/docs/auth/userpass">Username & password</a>
* @see <a href="https://www.vaultproject.io/docs/auth/userpass">Username and password</a>
* @see <a href="https://www.vaultproject.io/docs/auth/ldap">LDAP authentication</a>
* @see <a href="https://www.vaultproject.io/docs/auth/okta">Okta authentication</a>
* @see <a href="https://www.vaultproject.io/docs/auth/radius">RADIUS authentication</a>
* @since 2.4
*/
public class UsernamePasswordAuthentication implements ClientAuthentication, AuthenticationStepsFactory {
private static final Log logger = LogFactory.getLog(UsernamePasswordAuthentication.class);
private static final Log logger = LogFactory.getLog(UsernamePasswordAuthentication.class);
private final UsernamePasswordAuthenticationOptions options;
private final UsernamePasswordAuthenticationOptions options;
private final RestOperations restOperations;
private final RestOperations restOperations;
public UsernamePasswordAuthentication(UsernamePasswordAuthenticationOptions options, RestOperations restOperations) {
Assert.notNull(options, "UsernamePasswordAuthenticationOptions must not be null");
Assert.notNull(restOperations, "RestOperations must not be null");
public UsernamePasswordAuthentication(UsernamePasswordAuthenticationOptions options,
RestOperations restOperations) {
this.options = options;
this.restOperations = restOperations;
}
Assert.notNull(options, "UsernamePasswordAuthenticationOptions must not be null");
Assert.notNull(restOperations, "RestOperations must not be null");
@Override
public VaultToken login() throws VaultException {
return createTokenUsingUsernamePasswordAuthentication();
}
this.options = options;
this.restOperations = restOperations;
}
@Override
public AuthenticationSteps getAuthenticationSteps() {
return AuthenticationSteps
.fromSupplier(() -> singletonMap("password", options.getPassword()))
.login(String.format("%s/%s", getLoginPath(options.getPath()), options.getUsername()));
}
/**
* Creates a {@link AuthenticationSteps} for username/password authentication given
* {@link UsernamePasswordAuthenticationOptions}.
* @param options must not be {@literal null}.
* @return {@link AuthenticationSteps} for username/password authentication.
*/
public static AuthenticationSteps createAuthenticationSteps(UsernamePasswordAuthenticationOptions options) {
private VaultToken createTokenUsingUsernamePasswordAuthentication() {
try {
VaultResponse response = restOperations.postForObject(String.format("%s/%s", getLoginPath(options.getPath()), options.getUsername()), singletonMap("password", options.getPassword()), VaultResponse.class);
Assert.notNull(options, "UsernamePasswordAuthenticationOptions must not be null");
logger.debug("Login successful using username and password credentials");
Map<String, Object> body = createLoginBody(options);
return LoginTokenUtil.from(response.getAuth());
} catch (HttpStatusCodeException e) {
throw new VaultException(String.format("Cannot login using username and password: %s", VaultResponses.getError(e.getResponseBodyAsString())), e);
}
}
}
return AuthenticationSteps.fromSupplier(() -> body)
.login(String.format("%s/%s", getLoginPath(options.getPath()), options.getUsername()));
}
@Override
public VaultToken login() throws VaultException {
return createTokenUsingUsernamePasswordAuthentication();
}
@Override
public AuthenticationSteps getAuthenticationSteps() {
return createAuthenticationSteps(this.options);
}
private VaultToken createTokenUsingUsernamePasswordAuthentication() {
try {
VaultResponse response = restOperations.postForObject(
String.format("%s/%s", getLoginPath(options.getPath()), options.getUsername()),
createLoginBody(options), VaultResponse.class);
logger.debug("Login successful using username and password credentials");
return LoginTokenUtil.from(response.getAuth());
}
catch (HttpStatusCodeException e) {
throw new VaultException(String.format("Cannot login using username and password: %s",
VaultResponses.getError(e.getResponseBodyAsString())), e);
}
}
private static Map<String, Object> createLoginBody(UsernamePasswordAuthenticationOptions options) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("password", options.getPassword());
CharSequence totp = options.getTotp();
if (totp != null) {
body.put("totp", totp);
}
return body;
}
}

View File

@@ -15,87 +15,171 @@
*/
package org.springframework.vault.authentication;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import javax.annotation.Nullable;
/**
* Authentication options for {@link UsernamePasswordAuthentication}.
*
* @author Mikhael Sokolov
* @author Mark Paluch
* @since 2.4
* @see UsernamePasswordAuthentication
* @see #builder()
*/
public class UsernamePasswordAuthenticationOptions {
public static final String DEFAULT_USERPASS_AUTHENTICATION_PATH = "userpass";
public static final String DEFAULT_USERPASS_AUTHENTICATION_PATH = "userpass";
/**
* Path of the userpass authentication backend mount.
*/
private final String path;
/**
* Path of the userpass authentication backend mount.
*/
private final String path;
/**
* Username of the userpass authentication backend mount.
*/
private final String username;
/**
* Username of the userpass authentication backend mount.
*/
private final String username;
/**
* Password of the userpass authentication backend mount.
*/
private final CharSequence password;
/**
* Password of the userpass authentication backend mount.
*/
private final CharSequence password;
private UsernamePasswordAuthenticationOptions(String username, CharSequence password, String path) {
this.username = username;
this.password = password;
this.path = path;
}
/**
* TOTP (one-time-token, optional).
*/
@Nullable
private final CharSequence totp;
public String getUsername() {
return username;
}
private UsernamePasswordAuthenticationOptions(String path, String username, CharSequence password,
@Nullable CharSequence totp) {
this.username = username;
this.password = password;
this.path = path;
this.totp = totp;
}
public CharSequence getPassword() {
return password;
}
/**
* @return a new {@link UsernamePasswordAuthenticationBuilder}.
*/
public static UsernamePasswordAuthenticationBuilder builder() {
return new UsernamePasswordAuthenticationBuilder();
}
public String getPath() {
return path;
}
/**
* @return the path of the userpass authentication backend mount.
*/
public String getPath() {
return this.path;
}
public static UsernamePasswordAuthenticationBuilder builder() {
return new UsernamePasswordAuthenticationBuilder();
}
/**
* @return the username.
*/
public String getUsername() {
return this.username;
}
public static class UsernamePasswordAuthenticationBuilder {
/**
* @return the password.
*/
public CharSequence getPassword() {
return this.password;
}
@Nullable
private String username;
/**
* @return the totp (one-time-token). Can be {@code null}.
*/
@Nullable
public CharSequence getTotp() {
return this.totp;
}
@Nullable
private CharSequence password;
/**
* Builder for {@link UsernamePasswordAuthenticationOptions}.
*/
public static class UsernamePasswordAuthenticationBuilder {
private String path = DEFAULT_USERPASS_AUTHENTICATION_PATH;
private String path = DEFAULT_USERPASS_AUTHENTICATION_PATH;
UsernamePasswordAuthenticationBuilder() {
}
@Nullable
private String username;
public UsernamePasswordAuthenticationBuilder username(String username) {
this.username = username;
return this;
}
@Nullable
private CharSequence password;
public UsernamePasswordAuthenticationBuilder password(CharSequence password) {
this.password = password;
return this;
}
@Nullable
private CharSequence totp;
public UsernamePasswordAuthenticationBuilder path(String path) {
this.path = path;
return this;
}
UsernamePasswordAuthenticationBuilder() {
}
public UsernamePasswordAuthenticationOptions build() {
Assert.notNull(this.username, "Username must not be null");
Assert.notNull(this.password, "Password must not be null");
/**
* Configure a {@code username} for userpass authentication.
* @param username must not be empty or {@literal null}.
* @return {@code this} {@link UsernamePasswordAuthenticationBuilder}.
*/
public UsernamePasswordAuthenticationBuilder username(String username) {
Assert.hasText(username, "Username must not be null and not be empty");
this.username = username;
return this;
}
/**
* Configure a {@code password} for userpass authentication.
* @param password must not be {@literal null}.
* @return {@code this} {@link UsernamePasswordAuthenticationBuilder}.
*/
public UsernamePasswordAuthenticationBuilder password(CharSequence password) {
Assert.notNull(password, "Password must not be null");
this.password = password;
return this;
}
/**
* Configure an optional {@code totp} (time-based one-time token) for
* userpass/Okta authentication.
* @param totp must not be {@literal null}.
* @return {@code this} {@link UsernamePasswordAuthenticationBuilder}.
*/
public UsernamePasswordAuthenticationBuilder totp(CharSequence totp) {
Assert.notNull(password, "One-time token must not be null");
this.totp = totp;
return this;
}
/**
* Configure the mount path.
* @param path must not be {@literal null} or empty.
* @return {@code this} {@link UsernamePasswordAuthenticationBuilder}.
*/
public UsernamePasswordAuthenticationBuilder path(String path) {
Assert.hasText(path, "Path must not be empty");
this.path = path;
return this;
}
/**
* Build a new {@link UsernamePasswordAuthenticationOptions} instance.
* @return a new {@link UsernamePasswordAuthenticationOptions}.
*/
public UsernamePasswordAuthenticationOptions build() {
Assert.hasText(this.username, "Username must not be null and not be empty");
Assert.notNull(this.password, "Password must not be null");
return new UsernamePasswordAuthenticationOptions(path, username, password, totp);
}
}
return new UsernamePasswordAuthenticationOptions(username, password, path);
}
}
}

View File

@@ -1,50 +0,0 @@
/*
* Copyright 2017-2021 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.vault.authentication;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.vault.support.Policy;
import org.springframework.vault.util.IntegrationTestSupport;
import java.util.Map;
import static java.util.Collections.singletonMap;
import static org.springframework.vault.authentication.LdapAuthenticationOptions.DEFAULT_LDAP_AUTHENTICATION_PATH;
import static org.springframework.vault.support.Policy.BuiltinCapabilities.*;
/**
* Integration test base class for {@link LdapAuthentication} tests.
*
* @author Mikhael Sokolov
*/
public abstract class LdapAuthenticationIntegrationTestBase extends IntegrationTestSupport {
static final Policy POLICY = Policy.of(Policy.Rule.builder().path("/*").capabilities(READ, CREATE, UPDATE).build());
protected final String username = "admin";
protected final String password = "qwerty";
@BeforeEach
public void before() {
if (!prepare().hasAuth(DEFAULT_LDAP_AUTHENTICATION_PATH)) {
prepare().mountAuth(DEFAULT_LDAP_AUTHENTICATION_PATH);
}
prepare().getVaultOperations().opsForSys().createOrUpdatePolicy(DEFAULT_LDAP_AUTHENTICATION_PATH, POLICY);
prepare().getVaultOperations().doWithSession(restOperations -> restOperations.postForEntity(String.format("auth/%s/users/%s", DEFAULT_LDAP_AUTHENTICATION_PATH, username), singletonMap("password", password), Map.class));
}
}

View File

@@ -1,46 +0,0 @@
/*
* Copyright 2016-2021 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.vault.authentication;
import org.junit.jupiter.api.Test;
import org.springframework.vault.client.VaultClients;
import org.springframework.vault.support.VaultToken;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link LdapAuthentication}.
*
* @author Mikhael Sokolov
*/
class LdapAuthenticationIntegrationTests extends LdapAuthenticationIntegrationTestBase {
@Test
void shouldLoginSuccessfully() {
RestTemplate restTemplate = VaultClients.createRestTemplate();
LdapAuthenticationOptions options = LdapAuthenticationOptions.builder()
.username(username)
.password(password)
.build();
LdapAuthentication authentication = new LdapAuthentication(options, restTemplate);
VaultToken login = authentication.login();
assertThat(login.getToken()).isNotEmpty();
}
}

View File

@@ -15,14 +15,15 @@
*/
package org.springframework.vault.authentication;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.vault.support.Policy;
import org.springframework.vault.util.IntegrationTestSupport;
import java.util.Map;
import static java.util.Collections.singletonMap;
import static org.springframework.vault.authentication.UsernamePasswordAuthenticationOptions.DEFAULT_USERPASS_AUTHENTICATION_PATH;
import static java.util.Collections.*;
import static org.springframework.vault.authentication.UsernamePasswordAuthenticationOptions.*;
import static org.springframework.vault.support.Policy.BuiltinCapabilities.*;
/**
@@ -32,19 +33,24 @@ import static org.springframework.vault.support.Policy.BuiltinCapabilities.*;
*/
public abstract class UsernamePasswordAuthenticationIntegrationTestBase extends IntegrationTestSupport {
static final Policy POLICY = Policy.of(Policy.Rule.builder().path("/*").capabilities(READ, CREATE, UPDATE).build());
static final Policy POLICY = Policy.of(Policy.Rule.builder().path("/*").capabilities(READ, CREATE, UPDATE).build());
protected final String username = "admin";
protected final String password = "qwerty";
protected final String username = "admin";
@BeforeEach
public void before() {
protected final String password = "qwerty";
if (!prepare().hasAuth(DEFAULT_USERPASS_AUTHENTICATION_PATH)) {
prepare().mountAuth(DEFAULT_USERPASS_AUTHENTICATION_PATH);
}
@BeforeEach
public void before() {
prepare().getVaultOperations().opsForSys().createOrUpdatePolicy(DEFAULT_USERPASS_AUTHENTICATION_PATH, POLICY);
prepare().getVaultOperations().doWithSession(restOperations -> restOperations.postForEntity(String.format("auth/%s/users/%s", DEFAULT_USERPASS_AUTHENTICATION_PATH, username), singletonMap("password", password), Map.class));
}
}
if (!prepare().hasAuth(DEFAULT_USERPASS_AUTHENTICATION_PATH)) {
prepare().mountAuth(DEFAULT_USERPASS_AUTHENTICATION_PATH);
}
prepare().getVaultOperations().opsForSys().createOrUpdatePolicy(DEFAULT_USERPASS_AUTHENTICATION_PATH, POLICY);
prepare().getVaultOperations()
.doWithSession(restOperations -> restOperations.postForEntity(
String.format("auth/%s/users/%s", DEFAULT_USERPASS_AUTHENTICATION_PATH, username),
singletonMap("password", password), Map.class));
}
}

View File

@@ -16,11 +16,13 @@
package org.springframework.vault.authentication;
import org.junit.jupiter.api.Test;
import org.springframework.vault.client.VaultClients;
import org.springframework.vault.support.VaultToken;
import org.springframework.vault.util.Settings;
import org.springframework.vault.util.TestRestTemplateFactory;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.*;
/**
* Integration tests for {@link UsernamePasswordAuthentication}.
@@ -29,18 +31,31 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
class UsernamePasswordAuthenticationIntegrationTests extends UsernamePasswordAuthenticationIntegrationTestBase {
@Test
void shouldLoginSuccessfully() {
RestTemplate restTemplate = TestRestTemplateFactory.create(Settings.createSslConfiguration());
RestTemplate restTemplate = VaultClients.createRestTemplate();
UsernamePasswordAuthenticationOptions options = UsernamePasswordAuthenticationOptions.builder()
.username(username)
.password(password)
.build();
@Test
void shouldLoginSuccessfully() {
UsernamePasswordAuthentication authentication = new UsernamePasswordAuthentication(options, restTemplate);
VaultToken login = authentication.login();
UsernamePasswordAuthenticationOptions options = UsernamePasswordAuthenticationOptions.builder()
.username(username).password(password).build();
assertThat(login.getToken()).isNotEmpty();
}
}
UsernamePasswordAuthentication authentication = new UsernamePasswordAuthentication(options, restTemplate);
VaultToken login = authentication.login();
assertThat(login.getToken()).isNotEmpty();
}
@Test
void shouldLoginUsingAuthenticationSteps() {
UsernamePasswordAuthenticationOptions options = UsernamePasswordAuthenticationOptions.builder()
.username(username).password(password).build();
AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor(
UsernamePasswordAuthentication.createAuthenticationSteps(options), restTemplate);
VaultToken login = executor.login();
assertThat(login.getToken()).isNotEmpty();
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2021 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.vault.authentication;
import java.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.vault.client.VaultClients;
import org.springframework.vault.support.VaultToken;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
import static org.springframework.test.web.client.response.MockRestResponseCreators.*;
/**
* Unit tests for {@link UsernamePasswordAuthentication}.
*
* @author Mark Paluch
*/
class UsernamePasswordAuthenticationUnitTests {
RestTemplate restTemplate;
MockRestServiceServer mockRest;
@BeforeEach
void before() {
RestTemplate restTemplate = VaultClients.createRestTemplate();
restTemplate.setUriTemplateHandler(new VaultClients.PrefixAwareUriTemplateHandler());
this.mockRest = MockRestServiceServer.createServer(restTemplate);
this.restTemplate = restTemplate;
}
@Test
void shouldLoginWithTotp() {
UsernamePasswordAuthenticationOptions options = UsernamePasswordAuthenticationOptions.builder().path("okta")
.username("walter").password("heisenberg").totp("123456").build();
UsernamePasswordAuthentication sut = new UsernamePasswordAuthentication(options, this.restTemplate);
this.mockRest.expect(requestTo("/auth/okta/login/walter")).andExpect(method(HttpMethod.POST))
.andExpect(jsonPath("$.password").value("heisenberg")).andExpect(jsonPath("$.totp").value("123456"))
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON).body(
"{" + "\"auth\":{\"client_token\":\"my-token\", \"renewable\": true, \"lease_duration\": 10}"
+ "}"));
VaultToken login = sut.login();
assertThat(login).isInstanceOf(LoginToken.class);
assertThat(login.getToken()).isEqualTo("my-token");
assertThat(((LoginToken) login).getLeaseDuration()).isEqualTo(Duration.ofSeconds(10));
assertThat(((LoginToken) login).isRenewable()).isTrue();
}
}

View File

@@ -1,6 +1,11 @@
[[new-features]]
== New & Noteworthy
[[new-features.2-4-0]]
=== What's new in Spring Vault 2.4
* Support for <<vault.authentication.userpass,Username/Password authentication>> for Username/Password, LDAP, Okta, and RADIUS authentication.
[[new-features.2-3-0]]
=== What's new in Spring Vault 2.3

View File

@@ -784,6 +784,50 @@ See also:
* https://www.vaultproject.io/docs/auth/kubernetes.html[Vault Documentation: Using the Kubernetes auth backend]
* https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/[Kubernetes Documentation: Configure Service Accounts for Pods]
[[vault.authentication.userpass]]
== Username/Password authentication
Username/Password is typically a end-user authentication scheme.
Using username and password is supported by multiple Vault authentication backends:
* Username and Password (`userpass`)
* LDAP (`ldap`)
* Okta (`okta`, supports additionaly time-based one-time tokens)
* RADIUS (`radius`)
`UserPasswordAuthenticationOptions` can be used with all above mentioned authentication backends as the Login API is similar across all mechanisms.
Please ensure to use the appropriate auth mount path when configuring `UserPasswordAuthenticationOptions`.
.Configuring `UserPasswordAuthentication`
====
[source,java]
----
@Configuration
class AppConfig extends AbstractVaultConfiguration {
// …
@Override
public ClientAuthentication clientAuthentication() {
UserPasswordAuthenticationOptions options = UserPasswordAuthenticationOptions.builder()
.username(…).password(…).build();
return new UserPasswordAuthentication(options, restOperations());
}
// …
}
----
====
See also:
* https://www.vaultproject.io/api-docs/auth/userpass[Vault Documentation: Using the Userpass auth backend]
* https://www.vaultproject.io/api-docs/auth/ldap[Vault Documentation: Using the LDAP auth backend]
* https://www.vaultproject.io/api-docs/auth/radius[Vault Documentation: Using the RADIUS auth backend]
* https://www.vaultproject.io/api-docs/auth/okta[Vault Documentation: Using the Okta auth backend]
[[vault.authentication.steps]]
== Authentication Steps