Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/modules/ROOT/pages/authentication.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,33 @@ See also:
* https://developer.hashicorp.com/vault/api-docs/auth/github[Vault Documentation: GitGub]
* https://cli.github.com/[GitHub CLI]

[[vault.config.authentication.jwt]]
== JWT authentication

JWT authentication allows to authenticate with Vault using a JSON Web Token issued by an OIDC-compatible platform (e.g. GitHub Actions, EKS IRSA, GitLab CI/CD).
The JWT can be provided inline or read from a file.

.application.yml with all JWT authentication properties
[source,yaml]
----
spring.cloud.vault:
authentication: JWT
jwt:
role: my-role
jwt-path: jwt
token: ${VAULT_JWT_TOKEN:}
token-file: ${AWS_WEB_IDENTITY_TOKEN_FILE:}
----

* `role` sets the Role. Optional; if empty, the JWT auth method's `default_role` is used.
* `jwt-path` sets the path of the JWT auth method mount.
* `token` sets a static JWT to use.
* `token-file` points to a file containing the JWT. The file is re-read on each login to support token rotation.

NOTE: `token` and `token-file` are mutually exclusive — exactly one must be set.

See also: https://developer.hashicorp.com/vault/docs/auth/jwt[Vault Documentation: JWT/OIDC]

[[vault.authentication.gcpiam]]
[[vault.config.authentication.kubernetes]]
== Kubernetes authentication
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
Expand Down Expand Up @@ -58,6 +59,8 @@
import org.springframework.vault.authentication.GcpComputeAuthenticationOptions.GcpComputeAuthenticationOptionsBuilder;
import org.springframework.vault.authentication.GitHubAuthentication;
import org.springframework.vault.authentication.GitHubAuthenticationOptions;
import org.springframework.vault.authentication.JwtAuthentication;
import org.springframework.vault.authentication.JwtAuthenticationOptions;
import org.springframework.vault.authentication.KubernetesAuthentication;
import org.springframework.vault.authentication.KubernetesAuthenticationOptions;
import org.springframework.vault.authentication.KubernetesServiceAccountTokenFile;
Expand All @@ -78,6 +81,7 @@
* @author Michal Budzyn
* @author Quincy Conduff
* @author Issam El-atif
* @author Toshiaki Maki
* @since 1.1
*/
class ClientAuthenticationFactory {
Expand Down Expand Up @@ -113,6 +117,7 @@ ClientAuthentication createClientAuthentication() {
case GCP_GCE -> gcpGceAuthentication(this.vaultProperties);
case GCP_IAM -> gcpIamAuthentication(this.vaultProperties);
case GITHUB -> gitHubAuthentication(this.vaultProperties);
case JWT -> jwtAuthentication(this.vaultProperties);
case KUBERNETES -> kubernetesAuthentication(this.vaultProperties);
case PCF -> pcfAuthentication(this.vaultProperties);
case TOKEN -> tokenAuthentication(this.vaultProperties);
Expand Down Expand Up @@ -337,6 +342,32 @@ private static String invokeGitHubCli(String command, String subcommand) {
}
}

private ClientAuthentication jwtAuthentication(VaultProperties vaultProperties) {

VaultProperties.JwtProperties jwt = vaultProperties.getJwt();

Assert.hasText(jwt.getJwtPath(), "Mount path (spring.cloud.vault.jwt.jwt-path) must not be empty");

boolean hasToken = StringUtils.hasText(jwt.getToken());
boolean hasTokenFile = StringUtils.hasText(jwt.getTokenFile());
Assert.isTrue(hasToken ^ hasTokenFile,
"Exactly one of spring.cloud.vault.jwt.token or spring.cloud.vault.jwt.token-file must be set");

// ResourceCredentialSupplier re-reads the file on each get() call (invoked on
// every JwtAuthentication.login()), so re-logins pick up rotated tokens.
Supplier<String> jwtSupplier = hasToken ? jwt::getToken : new ResourceCredentialSupplier(jwt.getTokenFile());

JwtAuthenticationOptions.JwtAuthenticationOptionsBuilder builder = JwtAuthenticationOptions.builder()
.path(jwt.getJwtPath())
.jwtSupplier(jwtSupplier);

if (StringUtils.hasText(jwt.getRole())) {
builder.role(jwt.getRole());
}

return new JwtAuthentication(builder.build(), this.restOperations);
}

private ClientAuthentication kubernetesAuthentication(VaultProperties vaultProperties) {

VaultProperties.KubernetesProperties kubernetes = vaultProperties.getKubernetes();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
* @author Grenville Wilson
* @author Mårten Svantesson
* @author Issam El-atif
* @author Toshiaki Maki
*/
@ConfigurationProperties(VaultProperties.PREFIX)
public class VaultProperties implements EnvironmentAware {
Expand Down Expand Up @@ -131,6 +132,8 @@ public class VaultProperties implements EnvironmentAware {

private GithubProperties github = new GithubProperties();

private JwtProperties jwt = new JwtProperties();

private KubernetesProperties kubernetes = new KubernetesProperties();

private PcfProperties pcf = new PcfProperties();
Expand Down Expand Up @@ -321,6 +324,14 @@ public void setGithub(GithubProperties github) {
this.github = github;
}

public JwtProperties getJwt() {
return this.jwt;
}

public void setJwt(JwtProperties jwt) {
this.jwt = jwt;
}

public KubernetesProperties getKubernetes() {
return this.kubernetes;
}
Expand Down Expand Up @@ -382,7 +393,8 @@ public void setAuthentication(AuthenticationMethod authentication) {
*/
public enum AuthenticationMethod {

APPROLE, AWS_EC2, AWS_IAM, AZURE_MSI, CERT, CUBBYHOLE, GCP_GCE, GCP_IAM, GITHUB, KUBERNETES, NONE, PCF, TOKEN;
APPROLE, AWS_EC2, AWS_IAM, AZURE_MSI, CERT, CUBBYHOLE, GCP_GCE, GCP_IAM, GITHUB, JWT, KUBERNETES, NONE, PCF,
TOKEN;

}

Expand Down Expand Up @@ -1038,6 +1050,80 @@ public void setServiceAccountTokenFile(String serviceAccountTokenFile) {

}

/**
* JWT authentication properties.
*
* @author Toshiaki Maki
* @since 5.0.2
*/
public static class JwtProperties {

/**
* Mount path of the JWT authentication backend.
*/
private String jwtPath = "jwt";

/**
* Name of the role against which the login is being attempted. Optional; when not
* set, the JWT auth method's {@code default_role} configured on the Vault server
* is used.
*/
@Nullable
private String role;

/**
* Static JWT used for authentication. Mutually exclusive with {@link #tokenFile}.
* Typically resolved from an environment variable via a property placeholder.
*/
@Nullable
private String token;

/**
* Path to a file containing the JWT used for authentication. Mutually exclusive
* with {@link #token}. Useful for platforms that mount the JWT as a file (e.g.
* EKS IRSA via {@code AWS_WEB_IDENTITY_TOKEN_FILE}, Kubernetes projected service
* account tokens). The file is re-read on each login to support token rotation.
*/
@Nullable
private String tokenFile;

public String getJwtPath() {
return this.jwtPath;
}

public void setJwtPath(String jwtPath) {
this.jwtPath = jwtPath;
}

@Nullable
public String getRole() {
return this.role;
}

public void setRole(@Nullable String role) {
this.role = role;
}

@Nullable
public String getToken() {
return this.token;
}

public void setToken(@Nullable String token) {
this.token = token;
}

@Nullable
public String getTokenFile() {
return this.tokenFile;
}

public void setTokenFile(@Nullable String tokenFile) {
this.tokenFile = tokenFile;
}

}

/**
* PCF properties.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.nio.file.StandardOpenOption;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import software.amazon.awssdk.core.SdkSystemSetting;
import software.amazon.awssdk.regions.Region;

Expand All @@ -38,12 +39,15 @@
import org.springframework.vault.authentication.ClientAuthentication;
import org.springframework.vault.authentication.ClientCertificateAuthentication;
import org.springframework.vault.authentication.GitHubAuthentication;
import org.springframework.vault.authentication.JwtAuthentication;
import org.springframework.vault.authentication.JwtAuthenticationOptions;
import org.springframework.vault.authentication.PcfAuthentication;
import org.springframework.vault.authentication.TokenAuthentication;
import org.springframework.vault.support.VaultToken;
import org.springframework.web.client.RestTemplate;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

Expand All @@ -53,6 +57,7 @@
* @author Mark Paluch
* @author Quincy Conduff
* @author Issam El-atif
* @author Toshiaki Maki
*/
public class ClientAuthenticationFactoryUnitTests {

Expand Down Expand Up @@ -274,4 +279,82 @@ public void tokenAuthShouldFailIfTokenFileNotExistsAndTokenEmpty() throws IOExce
assertThatIllegalStateException().isThrownBy(factory::createClientAuthentication);
}

@Test
public void shouldSupportJwtWithStaticToken() {

VaultProperties properties = new VaultProperties();
properties.setAuthentication(VaultProperties.AuthenticationMethod.JWT);
properties.getJwt().setRole("my-role");
properties.getJwt().setToken("eyJ.static-jwt");

ClientAuthentication clientAuthentication = new ClientAuthenticationFactory(properties, new RestTemplate(),
new RestTemplate())
.createClientAuthentication();

assertThat(clientAuthentication).isInstanceOf(JwtAuthentication.class);

JwtAuthenticationOptions options = (JwtAuthenticationOptions) ReflectionTestUtils.getField(clientAuthentication,
"options");
assertThat(options.getPath()).isEqualTo("jwt");
assertThat(options.getRole()).isEqualTo("my-role");
assertThat(options.getJwtSupplier().get()).isEqualTo("eyJ.static-jwt");
}

@Test
public void shouldSupportJwtWithTokenFile(@TempDir Path tmp) throws IOException {

Path tokenFile = tmp.resolve("oidc-token");
Files.writeString(tokenFile, "eyJ.file-jwt");

VaultProperties properties = new VaultProperties();
properties.setAuthentication(VaultProperties.AuthenticationMethod.JWT);
properties.getJwt().setJwtPath("custom-jwt");
properties.getJwt().setTokenFile(tokenFile.toString());

ClientAuthentication clientAuthentication = new ClientAuthenticationFactory(properties, new RestTemplate(),
new RestTemplate())
.createClientAuthentication();

assertThat(clientAuthentication).isInstanceOf(JwtAuthentication.class);

JwtAuthenticationOptions options = (JwtAuthenticationOptions) ReflectionTestUtils.getField(clientAuthentication,
"options");
assertThat(options.getPath()).isEqualTo("custom-jwt");
assertThat(options.getRole()).isNull();
assertThat(options.getJwtSupplier().get()).isEqualTo("eyJ.file-jwt");
}

@Test
public void shouldFailJwtWhenBothTokenAndTokenFileSet(@TempDir Path tmp) throws IOException {

Path tokenFile = tmp.resolve("oidc-token");
Files.writeString(tokenFile, "eyJ.file-jwt");

VaultProperties properties = new VaultProperties();
properties.setAuthentication(VaultProperties.AuthenticationMethod.JWT);
properties.getJwt().setToken("eyJ.static-jwt");
properties.getJwt().setTokenFile(tokenFile.toString());

ClientAuthenticationFactory factory = new ClientAuthenticationFactory(properties, new RestTemplate(),
new RestTemplate());

assertThatIllegalArgumentException().isThrownBy(factory::createClientAuthentication)
.withMessage(
"Exactly one of spring.cloud.vault.jwt.token or spring.cloud.vault.jwt.token-file must be set");
}

@Test
public void shouldFailJwtWhenNoTokenSourceSet() {

VaultProperties properties = new VaultProperties();
properties.setAuthentication(VaultProperties.AuthenticationMethod.JWT);

ClientAuthenticationFactory factory = new ClientAuthenticationFactory(properties, new RestTemplate(),
new RestTemplate());

assertThatIllegalArgumentException().isThrownBy(factory::createClientAuthentication)
.withMessage(
"Exactly one of spring.cloud.vault.jwt.token or spring.cloud.vault.jwt.token-file must be set");
}

}