Polishing.
Reformat code. Simplify flow. See gh-689 Original pull request: gh-802
This commit is contained in:
@@ -17,9 +17,11 @@ package org.springframework.vault.authentication;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.vault.VaultException;
|
||||
@@ -34,7 +36,7 @@ import org.springframework.web.client.RestOperations;
|
||||
* to obtain a {@link VaultToken}.
|
||||
*
|
||||
* @author Nanne Baars
|
||||
* @since 3.0.4
|
||||
* @since 3.1
|
||||
* @see JwtAuthenticationOptions
|
||||
* @see RestOperations
|
||||
* @see <a href="https://www.vaultproject.io/api-docs/auth/jwt">Vault Auth Backend:
|
||||
@@ -57,6 +59,7 @@ public class JwtAuthentication implements ClientAuthentication, AuthenticationSt
|
||||
* @param restOperations must not be {@literal null}.
|
||||
*/
|
||||
public JwtAuthentication(JwtAuthenticationOptions options, RestOperations restOperations) {
|
||||
|
||||
Assert.notNull(options, "JwtAuthenticationOptions must not be null");
|
||||
Assert.notNull(restOperations, "RestOperations must not be null");
|
||||
|
||||
@@ -64,31 +67,22 @@ public class JwtAuthentication implements ClientAuthentication, AuthenticationSt
|
||||
this.restOperations = restOperations;
|
||||
}
|
||||
|
||||
private static Map<String, String> getJwtLogin(String role, String jwt) {
|
||||
Map<String, String> login = new HashMap<>();
|
||||
|
||||
login.put("jwt", jwt);
|
||||
if (StringUtils.hasText(role)) {
|
||||
login.put("role", role);
|
||||
}
|
||||
|
||||
return login;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationSteps getAuthenticationSteps() {
|
||||
return AuthenticationSteps.fromSupplier(options.getJwtSupplier())
|
||||
.map(token -> getJwtLogin(options.getRole(), token))
|
||||
.login(getLoginPath());
|
||||
.login(AuthenticationUtil.getLoginPath(this.options.getPath()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public VaultToken login() throws VaultException {
|
||||
|
||||
Map<String, String> login = getJwtLogin(this.options.getRole(), this.options.getJwtSupplier().get());
|
||||
|
||||
try {
|
||||
VaultResponse response = this.restOperations.postForObject(getLoginPath(), login, VaultResponse.class);
|
||||
|
||||
VaultResponse response = this.restOperations
|
||||
.postForObject(AuthenticationUtil.getLoginPath(this.options.getPath()), login, VaultResponse.class);
|
||||
Assert.state(response != null && response.getAuth() != null, "Auth field must not be null");
|
||||
|
||||
logger.debug("Login successful using JWT authentication");
|
||||
@@ -100,9 +94,17 @@ public class JwtAuthentication implements ClientAuthentication, AuthenticationSt
|
||||
}
|
||||
}
|
||||
|
||||
private String getLoginPath() {
|
||||
return AuthenticationUtil
|
||||
.getLoginPath(Optional.ofNullable(options.getPath()).orElse(DEFAULT_JWT_AUTHENTICATION_PATH));
|
||||
private static Map<String, String> getJwtLogin(@Nullable String role, String jwt) {
|
||||
|
||||
Map<String, String> login = new HashMap<>(2);
|
||||
|
||||
login.put("jwt", jwt);
|
||||
|
||||
if (StringUtils.hasText(role)) {
|
||||
login.put("role", role);
|
||||
}
|
||||
|
||||
return login;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.vault.authentication;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -25,20 +26,21 @@ import org.springframework.util.Assert;
|
||||
* Authentication options provide the role and the JWT. {@link JwtAuthenticationOptions}
|
||||
* can be constructed using {@link #builder()}. Instances of this class are immutable once
|
||||
* constructed.
|
||||
* <p>
|
||||
*
|
||||
* @author Nanne Baars
|
||||
* @since 3.0.4
|
||||
* @author Mark Paluch
|
||||
* @since 3.1
|
||||
* @see JwtAuthentication
|
||||
* @see #builder()
|
||||
*/
|
||||
public class JwtAuthenticationOptions {
|
||||
|
||||
public static final String DEFAULT_JWT_AUTHENTICATION_PATH = "jwt";
|
||||
|
||||
/**
|
||||
* Path of the JWT authentication backend mount. Optional and defaults to
|
||||
* {@literal jwt}.
|
||||
*/
|
||||
@Nullable
|
||||
private final String path;
|
||||
|
||||
/**
|
||||
@@ -55,7 +57,7 @@ public class JwtAuthenticationOptions {
|
||||
*/
|
||||
private final Supplier<String> jwtSupplier;
|
||||
|
||||
private JwtAuthenticationOptions(String role, Supplier<String> jwtSupplier, String path) {
|
||||
private JwtAuthenticationOptions(@Nullable String role, Supplier<String> jwtSupplier, String path) {
|
||||
|
||||
this.role = role;
|
||||
this.jwtSupplier = jwtSupplier;
|
||||
@@ -70,8 +72,10 @@ public class JwtAuthenticationOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return name of the role against which the login is being attempted.
|
||||
* @return name of the role against which the login is being attempted. Can be
|
||||
* {@literal null} if not configured.
|
||||
*/
|
||||
@Nullable
|
||||
public String getRole() {
|
||||
return this.role;
|
||||
}
|
||||
@@ -84,7 +88,7 @@ public class JwtAuthenticationOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the path of the kubernetes authentication backend mount.
|
||||
* @return the path of the JWT authentication backend mount.
|
||||
*/
|
||||
public String getPath() {
|
||||
return this.path;
|
||||
@@ -95,11 +99,26 @@ public class JwtAuthenticationOptions {
|
||||
*/
|
||||
public static class JwtAuthenticationOptionsBuilder {
|
||||
|
||||
private String path = DEFAULT_JWT_AUTHENTICATION_PATH;
|
||||
|
||||
@Nullable
|
||||
private String role;
|
||||
|
||||
@Nullable
|
||||
private Supplier<String> jwtSupplier;
|
||||
|
||||
private String path;
|
||||
/**
|
||||
* Configure the mount path.
|
||||
* @param path must not be {@literal null} or empty.
|
||||
* @return {@code this} {@link JwtAuthenticationOptionsBuilder}.
|
||||
*/
|
||||
public JwtAuthenticationOptionsBuilder path(String path) {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
|
||||
this.path = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the role.
|
||||
@@ -116,16 +135,17 @@ public class JwtAuthenticationOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the mount path.
|
||||
* @param path must not be {@literal null} or empty.
|
||||
* Configure the JWT authentication token. Vault authentication will use this
|
||||
* token as singleton. If you want to provide a dynamic token that can change over
|
||||
* time, see {@link #jwtSupplier(Supplier)}.
|
||||
* @param jwt must not be {@literal null}.
|
||||
* @return {@code this} {@link JwtAuthenticationOptionsBuilder}.
|
||||
*/
|
||||
public JwtAuthenticationOptionsBuilder path(String path) {
|
||||
public JwtAuthenticationOptionsBuilder jwt(String jwt) {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
Assert.hasText(jwt, "JWT must not be empty");
|
||||
|
||||
this.path = path;
|
||||
return this;
|
||||
return jwtSupplier(() -> jwt);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,9 +153,9 @@ public class JwtAuthenticationOptions {
|
||||
* @param jwtSupplier must not be {@literal null}.
|
||||
* @return {@code this} {@link JwtAuthenticationOptionsBuilder}.
|
||||
*/
|
||||
public JwtAuthenticationOptionsBuilder jwt(Supplier<String> jwtSupplier) {
|
||||
public JwtAuthenticationOptionsBuilder jwtSupplier(Supplier<String> jwtSupplier) {
|
||||
|
||||
Assert.notNull(jwtSupplier, "Jwt supplier must not be null");
|
||||
Assert.notNull(jwtSupplier, "JWT supplier must not be null");
|
||||
|
||||
this.jwtSupplier = jwtSupplier;
|
||||
return this;
|
||||
|
||||
@@ -15,9 +15,13 @@
|
||||
*/
|
||||
package org.springframework.vault.authentication;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.springframework.vault.authentication.JwtAuthentication.DEFAULT_JWT_AUTHENTICATION_PATH;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import com.nimbusds.jose.JOSEException;
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
@@ -25,23 +29,53 @@ import com.nimbusds.jose.JWSHeader;
|
||||
import com.nimbusds.jose.JWSObject;
|
||||
import com.nimbusds.jose.crypto.RSASSASigner;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.vault.support.VaultToken;
|
||||
import org.springframework.vault.util.IntegrationTestSupport;
|
||||
import org.springframework.vault.util.Settings;
|
||||
import org.springframework.vault.util.TestRestTemplateFactory;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
public class JwtAuthenticationIntegrationTest extends IntegrationTestSupport {
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.springframework.vault.authentication.JwtAuthentication.DEFAULT_JWT_AUTHENTICATION_PATH;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link KubernetesAuthentication} using
|
||||
* {@link AuthenticationStepsExecutor}.
|
||||
*
|
||||
* @author Nanne Baars
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class JwtAuthenticationIntegrationTest extends IntegrationTestSupport {
|
||||
|
||||
private KeyPair keyPair;
|
||||
|
||||
@BeforeEach
|
||||
void before() throws Exception {
|
||||
|
||||
keyPair = generateRsaKey();
|
||||
|
||||
if (!prepare().hasAuth("jwt")) {
|
||||
prepare().mountAuth("jwt");
|
||||
}
|
||||
|
||||
prepare().getVaultOperations().doWithSession(restOperations -> {
|
||||
|
||||
Map<String, String> jwtConfig = Map.of("jwt_validation_pubkeys", encodePublicKey(), "oidc_client_id", "",
|
||||
"oidc_client_secret", "");
|
||||
restOperations.postForEntity("auth/jwt/config", jwtConfig, Map.class);
|
||||
|
||||
Map<String, String> roleData = Map.of("role_type", DEFAULT_JWT_AUTHENTICATION_PATH, //
|
||||
"bound_audiences", "", "bound_subject", "admin", "user_claim", "user", "group_claims", "group");
|
||||
return restOperations.postForEntity("auth/jwt/role/my-role", roleData, Map.class);
|
||||
});
|
||||
}
|
||||
|
||||
private KeyPair generateRsaKey() throws Exception {
|
||||
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
||||
keyPairGenerator.initialize(2048);
|
||||
return keyPairGenerator.generateKeyPair();
|
||||
@@ -55,68 +89,16 @@ public class JwtAuthenticationIntegrationTest extends IntegrationTestSupport {
|
||||
""", Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()));
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void before() throws Exception {
|
||||
keyPair = generateRsaKey();
|
||||
if (!prepare().hasAuth("jwt")) {
|
||||
prepare().mountAuth("jwt");
|
||||
}
|
||||
|
||||
prepare().getVaultOperations().doWithSession(restOperations -> {
|
||||
var jwtConfig = Map.of( //
|
||||
"jwt_validation_pubkeys", encodePublicKey(), //
|
||||
"oidc_client_id", "", //
|
||||
"oidc_client_secret", "");
|
||||
restOperations.postForEntity("auth/jwt/config", jwtConfig, Map.class);
|
||||
|
||||
var roleData = Map.of("role_type", DEFAULT_JWT_AUTHENTICATION_PATH, //
|
||||
"bound_audiences", "", //
|
||||
"bound_subject", "admin", //
|
||||
"user_claim", "user", //
|
||||
"group_claims", "group");
|
||||
return restOperations.postForEntity("auth/jwt/role/my-role", roleData, Map.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLoginSuccessfully() throws Exception {
|
||||
var jwt = createToken("Administrator");
|
||||
var restTemplate = TestRestTemplateFactory.create(Settings.createSslConfiguration());
|
||||
|
||||
var loginToken = new JwtAuthentication(
|
||||
JwtAuthenticationOptions.builder().jwt(() -> jwt).role("my-role").build(), restTemplate)
|
||||
.login();
|
||||
|
||||
assertThat(loginToken.getToken()).startsWith("hvs.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void claimChangedInTokenShouldFailSignatureVerification() throws Exception {
|
||||
var token1 = createToken("Administrator");
|
||||
var token2 = createToken("Administrator2");
|
||||
// Different user claim with signature of token1 makes an invalid token
|
||||
var jwt = token2.substring(0, token2.lastIndexOf('.') + 1) + token1.substring(token1.lastIndexOf('.') + 1);
|
||||
|
||||
var restTemplate = TestRestTemplateFactory.create(Settings.createSslConfiguration());
|
||||
|
||||
assertThatThrownBy(
|
||||
() -> new JwtAuthentication(JwtAuthenticationOptions.builder().jwt(() -> jwt).role("my-role").build(),
|
||||
restTemplate)
|
||||
.login())
|
||||
.isInstanceOf(VaultLoginException.class)
|
||||
.hasMessage(
|
||||
"Cannot login using JWT: error validating token: error verifying token signature: no known key successfully validated the token signature");
|
||||
}
|
||||
|
||||
private String createToken(String user) throws JOSEException {
|
||||
var signer = new RSASSASigner(keyPair.getPrivate());
|
||||
var header = new JWSHeader(JWSAlgorithm.RS256);
|
||||
var body = new JWSObject(header,
|
||||
|
||||
RSASSASigner signer = new RSASSASigner(keyPair.getPrivate());
|
||||
JWSHeader header = new JWSHeader(JWSAlgorithm.RS256);
|
||||
JWSObject body = new JWSObject(header,
|
||||
new JWTClaimsSet.Builder().audience("local")
|
||||
.subject("admin")
|
||||
.claim("user", user)
|
||||
.issueTime(new Date())
|
||||
.expirationTime(java.sql.Timestamp.valueOf(LocalDateTime.now().plusDays(1)))
|
||||
.expirationTime(Date.from(Instant.now().plus(1, ChronoUnit.DAYS)))
|
||||
.issuer("http://localhost:8000")
|
||||
.build()
|
||||
.toPayload());
|
||||
@@ -124,4 +106,35 @@ public class JwtAuthenticationIntegrationTest extends IntegrationTestSupport {
|
||||
return body.serialize();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLoginSuccessfully() throws Exception {
|
||||
|
||||
String jwt = createToken("Administrator");
|
||||
RestTemplate restTemplate = TestRestTemplateFactory.create(Settings.createSslConfiguration());
|
||||
|
||||
JwtAuthentication authentication = new JwtAuthentication(
|
||||
JwtAuthenticationOptions.builder().jwtSupplier(() -> jwt).role("my-role").build(), restTemplate);
|
||||
VaultToken loginToken = authentication.login();
|
||||
|
||||
assertThat(loginToken.getToken()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void claimChangedInTokenShouldFailSignatureVerification() throws Exception {
|
||||
|
||||
String token1 = createToken("Administrator");
|
||||
String token2 = createToken("Administrator2");
|
||||
|
||||
// Different user claim with signature of token1 makes an invalid token
|
||||
String jwt = token2.substring(0, token2.lastIndexOf('.') + 1) + token1.substring(token1.lastIndexOf('.') + 1);
|
||||
|
||||
RestTemplate restTemplate = TestRestTemplateFactory.create(Settings.createSslConfiguration());
|
||||
JwtAuthentication authentication = new JwtAuthentication(
|
||||
JwtAuthenticationOptions.builder().jwtSupplier(() -> jwt).role("my-role").build(), restTemplate);
|
||||
|
||||
assertThatThrownBy(authentication::login).isInstanceOf(VaultLoginException.class)
|
||||
.hasMessageContaining("Cannot login using JWT", "error validating token",
|
||||
"error verifying token signature");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -743,6 +743,41 @@ See also:
|
||||
* https://www.vaultproject.io/docs/secrets/cubbyhole/index.html[Vault Documentation: Cubbyhole Secret Backend]
|
||||
* https://www.vaultproject.io/docs/concepts/response-wrapping.html[Vault Documentation: Response Wrapping]
|
||||
|
||||
[[vault.authentication.jwt]]
|
||||
== JWT authentication
|
||||
|
||||
Configuring JWT authentication requires the token or a JWT supplier.
|
||||
You can configure the authentication via `JwtAuthenticationOptions`.
|
||||
|
||||
On the Vault side you can configure the JWT backend by enabling the JWT auth backend and creating a role.
|
||||
You can either use `oidc_discovery_url`, `jwks_url` or `jwt_validation_pubkeys` to configure the JWT backend.
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
class AppConfig extends AbstractVaultConfiguration {
|
||||
|
||||
// …
|
||||
|
||||
@Override
|
||||
public ClientAuthentication clientAuthentication() {
|
||||
|
||||
JwtAuthenticationOptions options = JwtAuthenticationOptions.builder()
|
||||
.role(…).jwt(…).path(…).build();
|
||||
|
||||
return new JwtAuthentication(options, restOperations());
|
||||
}
|
||||
|
||||
// …
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
See also:
|
||||
|
||||
* https://developer.hashicorp.com/vault/docs/auth/jwt[Vault Documentation: Using the JWT auth backend]
|
||||
|
||||
[[vault.authentication.kubernetes]]
|
||||
== Kubernetes authentication
|
||||
|
||||
@@ -828,42 +863,6 @@ See also:
|
||||
* 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.jwt]]
|
||||
== JWT authentication
|
||||
|
||||
|
||||
Configuring JWT authentication requires at least the signed JWT to be provided:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
class AppConfig extends AbstractVaultConfiguration {
|
||||
|
||||
// …
|
||||
|
||||
@Override
|
||||
public ClientAuthentication clientAuthentication() {
|
||||
|
||||
JwtAuthenticationOptions options = JwtAuthenticationOptions.builder()
|
||||
.role(…).jwt(…).path(…).build();
|
||||
|
||||
return new JwtAuthentication(options, restOperations());
|
||||
}
|
||||
|
||||
// …
|
||||
}
|
||||
----
|
||||
====
|
||||
You can configure the authentication via `JwtAuthenticationOptions`.
|
||||
|
||||
On the Vault side you can configure the JWT backend by enabling the JWT auth backend and creating a role. You can either use `oidc_discovery_url`, `jwks_url` or `jwt_validation_pubkeys` to configure the JWT backend.
|
||||
|
||||
See also:
|
||||
|
||||
* https://developer.hashicorp.com/vault/docs/auth/jwt[Vault Documentation: Using the JWT auth backend]
|
||||
|
||||
|
||||
[[vault.authentication.steps]]
|
||||
== Authentication Steps
|
||||
|
||||
|
||||
Reference in New Issue
Block a user