Add support for PCF authentication.
We now support PCF authentication leveraging PCF's App and Container Identity Assurance. Instance certificate and key default to CF_INSTANCE_CERT respective CF_INSTANCE_KEY environment variables. PcfAuthenticationOptions options = PcfAuthenticationOptions.builder().role(…).build(); PcfAuthentication authentication = new PcfAuthentication(options, restOperations); VaultToken token = authentication.login(); PcfAuthentication requires BouncyCastle for RSA-PSS signing. Update KubernetesJwtSupplier and KubernetesServiceAccountTokenFile to inherit from generic Resource-based credential suppliers. Closes gh-440.
This commit is contained in:
@@ -191,6 +191,12 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk15on</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Testing -->
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2019 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.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Interface to obtain an arbitrary credential that is uses in
|
||||
* {@link ClientAuthentication} or {@link AuthenticationSteps} methods. Typically,
|
||||
* implementations obtain their credential from a file.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.2
|
||||
* @see ResourceCredentialSupplier
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CredentialSupplier extends Supplier<String> {
|
||||
|
||||
/**
|
||||
* Get a credential to be used with an authentication mechanism.
|
||||
*
|
||||
* @return the credential.
|
||||
*/
|
||||
@Override
|
||||
String get();
|
||||
|
||||
/**
|
||||
* Retrieve a cached {@link CredentialSupplier} that obtains the credential early and
|
||||
* reuses the token for each {@link #get()} call. This is useful to prevent I/O
|
||||
* operations in e.g. reactive usage.
|
||||
* <p>
|
||||
* Reusing a cached token can lead to authentication failures if the credential
|
||||
* expires.
|
||||
*
|
||||
* @return a caching {@link CredentialSupplier}.
|
||||
*/
|
||||
default CredentialSupplier cached() {
|
||||
|
||||
String credential = get();
|
||||
|
||||
return () -> credential;
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ public class KubernetesAuthentication
|
||||
public KubernetesAuthentication(KubernetesAuthenticationOptions options,
|
||||
RestOperations restOperations) {
|
||||
|
||||
Assert.notNull(options, "KubeAuthenticationOptions must not be null");
|
||||
Assert.notNull(options, "KubernetesAuthenticationOptions must not be null");
|
||||
Assert.notNull(restOperations, "RestOperations must not be null");
|
||||
|
||||
this.options = options;
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
*/
|
||||
package org.springframework.vault.authentication;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Interface to obtain a Kubernetes Service Account Token for Kubernetes authentication.
|
||||
* Implementations are used by {@link KubernetesAuthentication}.
|
||||
@@ -27,30 +25,6 @@ import java.util.function.Supplier;
|
||||
* @see KubernetesAuthentication
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface KubernetesJwtSupplier extends Supplier<String> {
|
||||
public interface KubernetesJwtSupplier extends CredentialSupplier {
|
||||
|
||||
/**
|
||||
* Get a JWT for Kubernetes authentication.
|
||||
*
|
||||
* @return the Kubernetes Service Account JWT.
|
||||
*/
|
||||
@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.
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,15 +16,9 @@
|
||||
package org.springframework.vault.authentication;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.vault.VaultException;
|
||||
|
||||
/**
|
||||
* Mechanism to retrieve a Kubernetes service account token.
|
||||
@@ -37,15 +31,14 @@ import org.springframework.vault.VaultException;
|
||||
* @since 2.0
|
||||
* @see KubernetesJwtSupplier
|
||||
*/
|
||||
public class KubernetesServiceAccountTokenFile implements KubernetesJwtSupplier {
|
||||
public class KubernetesServiceAccountTokenFile extends ResourceCredentialSupplier
|
||||
implements KubernetesJwtSupplier {
|
||||
|
||||
/**
|
||||
* Default path to the service account token file.
|
||||
*/
|
||||
public static final String DEFAULT_KUBERNETES_SERVICE_ACCOUNT_TOKEN_FILE = "/var/run/secrets/kubernetes.io/serviceaccount/token";
|
||||
|
||||
private final Resource resource;
|
||||
|
||||
/**
|
||||
* Create a new {@link KubernetesServiceAccountTokenFile} pointing to the
|
||||
* {@link #DEFAULT_KUBERNETES_SERVICE_ACCOUNT_TOKEN_FILE}. Construction fails with an
|
||||
@@ -88,38 +81,6 @@ public class KubernetesServiceAccountTokenFile implements KubernetesJwtSupplier
|
||||
* @throws IllegalArgumentException if the{@code path} does not exist.
|
||||
*/
|
||||
public KubernetesServiceAccountTokenFile(Resource resource) {
|
||||
|
||||
Assert.isTrue(resource.exists(),
|
||||
() -> String.format("Resource %s does not exist", resource));
|
||||
|
||||
this.resource = resource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get() {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the token from {@link Resource}.
|
||||
*
|
||||
* @param resource the resource to read from, must not be {@literal null}.
|
||||
* @return the new byte array that has been copied to (possibly empty).
|
||||
* @throws IOException in case of I/O errors.
|
||||
*/
|
||||
private static byte[] readToken(Resource resource) throws IOException {
|
||||
|
||||
Assert.notNull(resource, "Resource must not be null");
|
||||
|
||||
try (InputStream is = resource.getInputStream()) {
|
||||
return StreamUtils.copyToByteArray(is);
|
||||
}
|
||||
super(resource);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 2019 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.nio.charset.StandardCharsets;
|
||||
import java.security.spec.RSAPrivateKeySpec;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.bouncycastle.crypto.CryptoException;
|
||||
import org.bouncycastle.crypto.digests.SHA256Digest;
|
||||
import org.bouncycastle.crypto.engines.RSAEngine;
|
||||
import org.bouncycastle.crypto.params.RSAKeyParameters;
|
||||
import org.bouncycastle.crypto.signers.PSSSigner;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.Base64Utils;
|
||||
import org.springframework.vault.VaultException;
|
||||
import org.springframework.vault.support.PemObject;
|
||||
import org.springframework.vault.support.VaultResponse;
|
||||
import org.springframework.vault.support.VaultToken;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
|
||||
/**
|
||||
* PCF implementation of {@link ClientAuthentication}. {@link PcfAuthentication} uses a
|
||||
* PCF instance certificate and key to login into Vault.
|
||||
* <p>
|
||||
* Requires BouncyCastle to generate a RSA PSS signature.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.2
|
||||
* @see PcfAuthenticationOptions
|
||||
* @see RestOperations
|
||||
* @see <a href="https://www.vaultproject.io/docs/auth/pcf.html">Auth Backend: PCF</a>
|
||||
*/
|
||||
public class PcfAuthentication
|
||||
implements ClientAuthentication, AuthenticationStepsFactory {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(PcfAuthentication.class);
|
||||
|
||||
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter
|
||||
.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
|
||||
|
||||
// SHA256 hash and a salt length of 222
|
||||
private static final int SALT_LENGTH = 222;
|
||||
|
||||
private final PcfAuthenticationOptions options;
|
||||
|
||||
private final RestOperations restOperations;
|
||||
|
||||
/**
|
||||
* Create a {@link PcfAuthentication} using {@link PcfAuthenticationOptions} and
|
||||
* {@link RestOperations}.
|
||||
*
|
||||
* @param options must not be {@literal null}.
|
||||
* @param restOperations must not be {@literal null}.
|
||||
*/
|
||||
public PcfAuthentication(PcfAuthenticationOptions options,
|
||||
RestOperations restOperations) {
|
||||
|
||||
Assert.notNull(options, "PcfAuthenticationOptions must not be null");
|
||||
Assert.notNull(restOperations, "RestOperations must not be null");
|
||||
|
||||
this.options = options;
|
||||
this.restOperations = restOperations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link AuthenticationSteps} for pcf authentication given
|
||||
* {@link PcfAuthenticationOptions}.
|
||||
*
|
||||
* @param options must not be {@literal null}.
|
||||
* @return {@link AuthenticationSteps} for pcf authentication.
|
||||
*/
|
||||
public static AuthenticationSteps createAuthenticationSteps(
|
||||
PcfAuthenticationOptions options) {
|
||||
|
||||
Assert.notNull(options, "PcfAuthenticationOptions must not be null");
|
||||
|
||||
String instanceCert = options.getInstanceCertSupplier().get();
|
||||
String instanceKey = options.getInstanceKeySupplier().get();
|
||||
return AuthenticationSteps
|
||||
.fromSupplier(() -> getPcfLogin(options.getRole(), options.getClock(),
|
||||
instanceCert, instanceKey)) //
|
||||
.login("auth/{mount}/login", options.getPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public VaultToken login() throws VaultException {
|
||||
|
||||
Map<String, String> login = getPcfLogin(options.getRole(), options.getClock(),
|
||||
options.getInstanceCertSupplier().get(),
|
||||
options.getInstanceKeySupplier().get());
|
||||
|
||||
try {
|
||||
VaultResponse response = restOperations.postForObject("auth/{mount}/login",
|
||||
login, VaultResponse.class, options.getPath());
|
||||
|
||||
Assert.state(response != null && response.getAuth() != null,
|
||||
"Auth field must not be null");
|
||||
|
||||
logger.debug("Login successful using PCF authentication");
|
||||
|
||||
return LoginTokenUtil.from(response.getAuth());
|
||||
}
|
||||
catch (RestClientException e) {
|
||||
throw VaultLoginException.create("PCF", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationSteps getAuthenticationSteps() {
|
||||
return createAuthenticationSteps(this.options);
|
||||
}
|
||||
|
||||
private static Map<String, String> getPcfLogin(String role, Clock clock,
|
||||
String instanceCert, String instanceKey) {
|
||||
|
||||
Assert.hasText(role, "Role must not be empty");
|
||||
|
||||
String signingTime = TIME_FORMAT.format(LocalDateTime.now(clock));
|
||||
String message = getMessage(role, signingTime, instanceCert);
|
||||
String signature = sign(message, instanceKey);
|
||||
Map<String, String> login = new HashMap<>();
|
||||
|
||||
login.put("role", role);
|
||||
login.put("cf_instance_cert", instanceCert);
|
||||
login.put("signing_time", signingTime);
|
||||
login.put("signature", signature);
|
||||
|
||||
return login;
|
||||
}
|
||||
|
||||
private static String sign(String message, String privateKeyPem) {
|
||||
|
||||
try {
|
||||
return doSign(message.getBytes(StandardCharsets.US_ASCII), privateKeyPem);
|
||||
}
|
||||
catch (CryptoException e) {
|
||||
throw new VaultException("Cannot sign PCF login", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getMessage(String role, String signingTime,
|
||||
String instanceCertPem) {
|
||||
return signingTime + instanceCertPem + role;
|
||||
}
|
||||
|
||||
private static String doSign(byte[] message, String instanceKeyPem)
|
||||
throws CryptoException {
|
||||
|
||||
RSAPrivateKeySpec privateKey = PemObject.fromKey(instanceKeyPem).getRSAKeySpec();
|
||||
PSSSigner signer = new PSSSigner(new RSAEngine(), new SHA256Digest(),
|
||||
SALT_LENGTH);
|
||||
|
||||
signer.init(true, new RSAKeyParameters(true, privateKey.getModulus(),
|
||||
privateKey.getPrivateExponent()));
|
||||
signer.update(message, 0, message.length);
|
||||
|
||||
byte[] signature = signer.generateSignature();
|
||||
return Base64Utils.encodeToUrlSafeString(signature);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
* Copyright 2019 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.Clock;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Authentication options for {@link PcfAuthentication}.
|
||||
* <p>
|
||||
* Authentication options provide the path, {@link Clock} and instance key/instance
|
||||
* certificate {@link Supplier}s. {@link PcfAuthenticationOptions} can be constructed
|
||||
* using {@link #builder()}. Instances of this class are immutable once constructed.
|
||||
* <p>
|
||||
* Defaults to a cached instance certificate/key by resolving {@code CF_INSTANCE_CERT} and
|
||||
* {@code CF_INSTANCE_KEY} env variables.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @see CredentialSupplier
|
||||
* @see ResourceCredentialSupplier
|
||||
* @see #builder()
|
||||
*/
|
||||
public class PcfAuthenticationOptions {
|
||||
|
||||
public static final String DEFAULT_PCF_AUTHENTICATION_PATH = "pcf";
|
||||
|
||||
/**
|
||||
* Path of the pcf authentication backend mount.
|
||||
*/
|
||||
private final String path;
|
||||
|
||||
/**
|
||||
* Name of the role against which the login is being attempted.
|
||||
*/
|
||||
private final String role;
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
/**
|
||||
* Supplier instance to obtain the instance certificate.
|
||||
*/
|
||||
private final Supplier<String> instanceCertSupplier;
|
||||
|
||||
/**
|
||||
* Supplier instance to obtain the instance key.
|
||||
*/
|
||||
private final Supplier<String> instanceKeySupplier;
|
||||
|
||||
private PcfAuthenticationOptions(String path, String role, Clock clock,
|
||||
Supplier<String> instanceCertSupplier, Supplier<String> instanceKeySupplier) {
|
||||
this.path = path;
|
||||
this.role = role;
|
||||
this.clock = clock;
|
||||
this.instanceCertSupplier = instanceCertSupplier;
|
||||
this.instanceKeySupplier = instanceKeySupplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a new {@link PcfAuthenticationOptionsBuilder}.
|
||||
*/
|
||||
public static PcfAuthenticationOptionsBuilder builder() {
|
||||
return new PcfAuthenticationOptionsBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the path of the pcf authentication backend mount.
|
||||
*/
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return name of the role against which the login is being attempted.
|
||||
*/
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the {@link Clock}.
|
||||
*/
|
||||
public Clock getClock() {
|
||||
return clock;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the instance certificate {@link Supplier}.
|
||||
*/
|
||||
public Supplier<String> getInstanceCertSupplier() {
|
||||
return instanceCertSupplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the instance key {@link Supplier}.
|
||||
*/
|
||||
public Supplier<String> getInstanceKeySupplier() {
|
||||
return instanceKeySupplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for {@link PcfAuthenticationOptions}.
|
||||
*/
|
||||
public static class PcfAuthenticationOptionsBuilder {
|
||||
|
||||
private String path = DEFAULT_PCF_AUTHENTICATION_PATH;
|
||||
|
||||
private Clock clock = Clock.systemUTC();
|
||||
|
||||
@Nullable
|
||||
private String role;
|
||||
|
||||
@Nullable
|
||||
private Supplier<String> instanceCertSupplier;
|
||||
|
||||
@Nullable
|
||||
private Supplier<String> instanceKeySupplier;
|
||||
|
||||
PcfAuthenticationOptionsBuilder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the mount path.
|
||||
*
|
||||
* @param path must not be empty or {@literal null}.
|
||||
* @return {@code this} {@link PcfAuthenticationOptionsBuilder}.
|
||||
* @see #DEFAULT_PCF_AUTHENTICATION_PATH
|
||||
*/
|
||||
public PcfAuthenticationOptionsBuilder path(String path) {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
|
||||
this.path = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the role.
|
||||
*
|
||||
* @param role name of the role against which the login is being attempted, must
|
||||
* not be {@literal null} or empty.
|
||||
* @return {@code this} {@link PcfAuthenticationOptionsBuilder}.
|
||||
*/
|
||||
public PcfAuthenticationOptionsBuilder role(String role) {
|
||||
|
||||
Assert.hasText(role, "Role must not be empty");
|
||||
|
||||
this.role = role;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link Clock}.
|
||||
*
|
||||
* @param clock must not be {@literal null}.
|
||||
* @return {@code this} {@link PcfAuthenticationOptionsBuilder}.
|
||||
*/
|
||||
public PcfAuthenticationOptionsBuilder clock(Clock clock) {
|
||||
|
||||
Assert.notNull(clock, "Clock must not be null");
|
||||
|
||||
this.clock = clock;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link Supplier} to obtain the instance certificate.
|
||||
*
|
||||
* @param instanceCertSupplier the supplier, must not be {@literal null}.
|
||||
* @return {@code this} {@link PcfAuthenticationOptionsBuilder}.
|
||||
* @see ResourceCredentialSupplier
|
||||
*/
|
||||
public PcfAuthenticationOptionsBuilder instanceCertificate(
|
||||
Supplier<String> instanceCertSupplier) {
|
||||
|
||||
Assert.notNull(instanceCertSupplier,
|
||||
"Instance certificate supplier must not be null");
|
||||
|
||||
this.instanceCertSupplier = instanceCertSupplier;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link Supplier} to obtain the instance key.
|
||||
*
|
||||
* @param instanceKeySupplier the supplier, must not be {@literal null}.
|
||||
* @return {@code this} {@link PcfAuthenticationOptionsBuilder}.
|
||||
* @see ResourceCredentialSupplier
|
||||
*/
|
||||
public PcfAuthenticationOptionsBuilder instanceKey(
|
||||
Supplier<String> instanceKeySupplier) {
|
||||
|
||||
Assert.notNull(instanceKeySupplier,
|
||||
"Instance certificate supplier must not be null");
|
||||
|
||||
this.instanceKeySupplier = instanceKeySupplier;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new {@link PcfAuthenticationOptions} instance.
|
||||
* <p>
|
||||
* Falls back to the instance certificate at {@code CF_INSTANCE_CERT} if
|
||||
* {@link #instanceCertificate(Supplier)} is not configured respective
|
||||
* {@code CF_INSTANCE_KEY} if {@link #instanceKey(Supplier)} is not configured.
|
||||
*
|
||||
* @return a new {@link PcfAuthenticationOptions}.
|
||||
* @throws IllegalStateException if {@link #instanceCertificate(Supplier)} or
|
||||
* {@link #instanceKey(Supplier)} are not set and the corresponding
|
||||
* environment variable {@code CF_INSTANCE_CERT} respective
|
||||
* {@code CF_INSTANCE_KEY} is not set.
|
||||
*/
|
||||
public PcfAuthenticationOptions build() {
|
||||
|
||||
Assert.notNull(role, "Role must not be null");
|
||||
|
||||
Supplier<String> instanceCertSupplier = this.instanceCertSupplier;
|
||||
|
||||
if (instanceCertSupplier == null) {
|
||||
instanceCertSupplier = new ResourceCredentialSupplier(
|
||||
resolveEnvVariable("CF_INSTANCE_CERT")).cached();
|
||||
}
|
||||
|
||||
Supplier<String> instanceKeySupplier = this.instanceKeySupplier;
|
||||
if (instanceKeySupplier == null) {
|
||||
instanceKeySupplier = new ResourceCredentialSupplier(
|
||||
resolveEnvVariable("CF_INSTANCE_KEY")).cached();
|
||||
}
|
||||
|
||||
return new PcfAuthenticationOptions(path, role, clock, instanceCertSupplier,
|
||||
instanceKeySupplier);
|
||||
}
|
||||
|
||||
private static String resolveEnvVariable(String name) {
|
||||
|
||||
String value = System.getenv(name);
|
||||
|
||||
if (StringUtils.isEmpty(value)) {
|
||||
throw new IllegalStateException(
|
||||
String.format("Environment variable %s not set", name));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2019 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.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.vault.VaultException;
|
||||
|
||||
/**
|
||||
* Mechanism to retrieve a credential from a {@link Resource}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.2
|
||||
* @see CredentialSupplier
|
||||
*/
|
||||
public class ResourceCredentialSupplier implements CredentialSupplier {
|
||||
|
||||
private final Resource resource;
|
||||
|
||||
/**
|
||||
* Create a new {@link ResourceCredentialSupplier} {@link ResourceCredentialSupplier}
|
||||
* from a {@code path}.
|
||||
*
|
||||
* @param path path to the file holding the credential.
|
||||
* @throws IllegalArgumentException if the{@code path} does not exist.
|
||||
*/
|
||||
public ResourceCredentialSupplier(String path) {
|
||||
this(new FileSystemResource(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ResourceCredentialSupplier} {@link ResourceCredentialSupplier}
|
||||
* from a {@link File} handle.
|
||||
*
|
||||
* @param file path to the file holding the credential.
|
||||
* @throws IllegalArgumentException if the{@code path} does not exist.
|
||||
*/
|
||||
public ResourceCredentialSupplier(File file) {
|
||||
this(new FileSystemResource(file));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ResourceCredentialSupplier} {@link ResourceCredentialSupplier}
|
||||
* from a {@link Resource} handle.
|
||||
*
|
||||
* @param resource resource pointing to the resource holding the credential.
|
||||
* @throws IllegalArgumentException if the {@link Resource} does not exist.
|
||||
*/
|
||||
public ResourceCredentialSupplier(Resource resource) {
|
||||
|
||||
Assert.isTrue(resource.exists(),
|
||||
() -> String.format("Resource %s does not exist", resource));
|
||||
|
||||
this.resource = resource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get() {
|
||||
|
||||
try {
|
||||
return new String(readToken(this.resource), StandardCharsets.US_ASCII);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new VaultException(
|
||||
String.format("Credential retrieval from %s failed", this.resource),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the token from {@link Resource}.
|
||||
*
|
||||
* @param resource the resource to read from, must not be {@literal null}.
|
||||
* @return the new byte array that has been copied to (possibly empty).
|
||||
* @throws IOException in case of I/O errors.
|
||||
*/
|
||||
private static byte[] readToken(Resource resource) throws IOException {
|
||||
|
||||
Assert.notNull(resource, "Resource must not be null");
|
||||
|
||||
try (InputStream is = resource.getInputStream()) {
|
||||
return StreamUtils.copyToByteArray(is);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -264,7 +264,13 @@ public class RestTemplateBuilder {
|
||||
ClientHttpRequest request = requestFactory.createRequest(uri, httpMethod);
|
||||
HttpHeaders headers = request.getHeaders();
|
||||
|
||||
this.defaultHeaders.forEach(headers::addIfAbsent);
|
||||
this.defaultHeaders.forEach((key, value) -> {
|
||||
|
||||
if (!headers.containsKey(key)) {
|
||||
headers.add(key, value);
|
||||
}
|
||||
|
||||
});
|
||||
this.requestCustomizers.forEach(it -> it.customize(request));
|
||||
|
||||
return request;
|
||||
|
||||
@@ -106,7 +106,7 @@ public class Certificate {
|
||||
byte[] bytes = Base64Utils.decodeFromString(getCertificate());
|
||||
return KeystoreUtil.getCertificate(bytes);
|
||||
}
|
||||
catch (IOException | CertificateException e) {
|
||||
catch (CertificateException e) {
|
||||
throw new VaultException("Cannot create Certificate from certificate", e);
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,7 @@ public class Certificate {
|
||||
byte[] bytes = Base64Utils.decodeFromString(getIssuingCaCertificate());
|
||||
return KeystoreUtil.getCertificate(bytes);
|
||||
}
|
||||
catch (IOException | CertificateException e) {
|
||||
catch (CertificateException e) {
|
||||
throw new VaultException(
|
||||
"Cannot create Certificate from issuing CA certificate", e);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.math.BigInteger;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyStore;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
@@ -40,6 +41,27 @@ import java.util.List;
|
||||
*/
|
||||
class KeystoreUtil {
|
||||
|
||||
private static final CertificateFactory CERTIFICATE_FACTORY;
|
||||
|
||||
private static final KeyFactory KEY_FACTORY;
|
||||
|
||||
static {
|
||||
|
||||
try {
|
||||
CERTIFICATE_FACTORY = CertificateFactory.getInstance("X.509");
|
||||
}
|
||||
catch (CertificateException e) {
|
||||
throw new IllegalStateException("No X.509 Certificate available", e);
|
||||
}
|
||||
|
||||
try {
|
||||
KEY_FACTORY = KeyFactory.getInstance("RSA");
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("No RSA KeyFactory available", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link KeyStore} containing the {@link KeySpec} and {@link X509Certificate
|
||||
* certificates} using the given {@code keyAlias}.
|
||||
@@ -54,8 +76,7 @@ class KeystoreUtil {
|
||||
X509Certificate... certificates)
|
||||
throws GeneralSecurityException, IOException {
|
||||
|
||||
KeyFactory kf = KeyFactory.getInstance("RSA");
|
||||
PrivateKey privateKey = kf.generatePrivate(privateKeySpec);
|
||||
PrivateKey privateKey = KEY_FACTORY.generatePrivate(privateKeySpec);
|
||||
|
||||
KeyStore keyStore = createKeyStore();
|
||||
|
||||
@@ -93,11 +114,9 @@ class KeystoreUtil {
|
||||
}
|
||||
|
||||
static X509Certificate getCertificate(byte[] source)
|
||||
throws CertificateException, IOException {
|
||||
throws CertificateException {
|
||||
|
||||
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
|
||||
|
||||
List<X509Certificate> certificates = getCertificates(certificateFactory, source);
|
||||
List<X509Certificate> certificates = getCertificates(CERTIFICATE_FACTORY, source);
|
||||
|
||||
return certificates.stream().findFirst().orElseThrow(
|
||||
() -> new IllegalArgumentException("No X509Certificate found"));
|
||||
@@ -120,7 +139,7 @@ class KeystoreUtil {
|
||||
}
|
||||
|
||||
private static List<X509Certificate> getCertificates(CertificateFactory cf,
|
||||
byte[] source) throws CertificateException, IOException {
|
||||
byte[] source) throws CertificateException {
|
||||
|
||||
List<X509Certificate> x509Certificates = new ArrayList<>();
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2019 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.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.spec.RSAPrivateCrtKeySpec;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.util.Base64Utils;
|
||||
|
||||
/**
|
||||
* Represents a PEM object that is internally decoded to a DER object. Typically used to
|
||||
* obtain a {@link RSAPrivateCrtKeySpec}.
|
||||
* <p>
|
||||
* Mainly for use within the framework.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.2
|
||||
*/
|
||||
public class PemObject {
|
||||
|
||||
private static final Pattern KEY_PATTERN = Pattern
|
||||
.compile("-+BEGIN\\s+.*PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" + // Header
|
||||
"([a-z0-9+/=\\r\\n]+)" + // Base64 text
|
||||
"-+END\\s+.*PRIVATE\\s+KEY[^-]*-+", // Footer
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private final byte[] content;
|
||||
|
||||
private PemObject(String content) {
|
||||
|
||||
String sanitized = content.replaceAll("\r", "").replaceAll("\n", "");
|
||||
this.content = Base64Utils.decodeFromString(sanitized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a{@link PemObject} from PEM {@code content} that is enclosed with
|
||||
* {@code -BEGIN PRIVATE KEY-} and {@code -END PRIVATE KEY-}.
|
||||
*
|
||||
* @param content the PEM content.
|
||||
* @return the {@link PemObject} from PEM {@code content}.
|
||||
*/
|
||||
public static PemObject fromKey(String content) {
|
||||
|
||||
Matcher m = KEY_PATTERN.matcher(content);
|
||||
if (!m.find()) {
|
||||
throw new IllegalArgumentException("Could not find a PKCS #8 private key");
|
||||
}
|
||||
|
||||
return new PemObject(m.group(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a {@link RSAPrivateCrtKeySpec}.
|
||||
*
|
||||
* @return the {@link RSAPrivateCrtKeySpec}.
|
||||
*/
|
||||
public RSAPrivateCrtKeySpec getRSAKeySpec() {
|
||||
|
||||
try {
|
||||
return KeystoreUtil.getRSAKeySpec(this.content);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new IllegalArgumentException("Cannot obtain PrivateKey", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2019 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 static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link PcfAuthenticationOptions}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class PcfAuthenticationOptionsUnitTests {
|
||||
|
||||
@Test
|
||||
void emptyEnvVariableShouldFailWithMeaningfulMessage() {
|
||||
|
||||
assertThatThrownBy(
|
||||
() -> PcfAuthenticationOptions.builder().role("my-role").build())
|
||||
.isInstanceOf(IllegalStateException.class) //
|
||||
.hasMessageContaining(
|
||||
"Environment variable CF_INSTANCE_CERT not set");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 2019 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.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
|
||||
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.client.VaultClients.PrefixAwareUriTemplateHandler;
|
||||
import org.springframework.vault.support.VaultToken;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link PcfAuthentication}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class PcfAuthenticationUnitTests {
|
||||
|
||||
RestTemplate restTemplate;
|
||||
|
||||
MockRestServiceServer mockRest;
|
||||
|
||||
String instanceKey = "-----BEGIN RSA PRIVATE KEY-----\n"
|
||||
+ "MIIEpAIBAAKCAQEAzhDAw7m5EuCcQkT7cesJF9J/0FWeSICLg4F/3R8EpkjPfZSW\n"
|
||||
+ "BeIjSs6v+wUjPCdBomZpYnrphYvSdiDrHWIDKrFNcWMFms0t6A0jEyGG/k9xDf6u\n"
|
||||
+ "VjDw4Gi4LJ0o0sNK0y9ULgdrViwRkSKNdhZx+34l/aeWyrg9WG6KAdjbPQE2J4Vi\n"
|
||||
+ "3x7yJVEh/Ya0uR/UtN/8hB8Makuaz6SRcLvooUO0FXYpy20olA/nlmowCl1PxpVv\n"
|
||||
+ "smZZG7qDJGL/P3C3M9YRX4VPXLTiyvEFwIgWNz6QIhX/Enm0q+xyw+gd5hRQzF4Q\n"
|
||||
+ "Fs03VrzHsQX9H3GMd/HoqsM1vD+PPfpKI7re7wIDAQABAoIBAQCk83oq8wd4Wf4b\n"
|
||||
+ "ejbBWQB9Zk5UCcVbijKjwU0GR2ckaNJXV1LEQOI5ZrwuN02eQFpk0o/3eiZmdaey\n"
|
||||
+ "UeWDLssUKLuyUS7SXP4rbCCwlr0F47e/GSia7DBVot4TMHbWR+gkpxU+h0ffwgUJ\n"
|
||||
+ "5dvRNGRnifKFWtr1SYgpusqUcfAFovvcbLZ4X+VkV7uazHAbtor5ol2UuRN04Hnu\n"
|
||||
+ "i5gxKBvr/j4ULDnZDJZjJKPVyaS9x4ewVJx241mFbE3aOWxRl6QwouqXTqhIs/Lv\n"
|
||||
+ "85MR67nuVlEWMToR/sx0/5Xg/BJDHLUCRM5MNx+wD+Fyj6DlJn2A63Ly4HQZZuYG\n"
|
||||
+ "Z48BGku5AoGBAOFmlJQITfp5xtu2oy10bwtDElHV3HBAtcJ7OHgu9ZniNNhgZ7ud\n"
|
||||
+ "pw9VC/bL8qZ8iToooKA7AgecSmaDeX0qWPgE9uaheaT76x/6Kn3Ue/ZfV2pSrStu\n"
|
||||
+ "YlFc5UxG3Z3klAx/Y+DA7rLb2Y0U6olLx5A7Fg+V/feD8mYxnyZy2r/bAoGBAOoK\n"
|
||||
+ "NVIyzcv13S7OqiCo4nhF2CcDWekGTzmsFygwvNwiM1CipzFifQQw6Z+0rl1FwxN0\n"
|
||||
+ "uGs3x41iTmkVOyASO78Li9aPQHZWhRWQI82689kYcnChQnDZGLfIv/WVxGy3DgKd\n"
|
||||
+ "FdJIOSLagTwhUOnZ4kaR6Mj2Jc4RXYjwpoQUJON9AoGBALOqP75rjDSegvs5boJZ\n"
|
||||
+ "7/WLJfwjOw4jFn6KF638yHo7zCG5XpY3CSX4hYvYb3dzhzLblYWC45BLbSafn+Q8\n"
|
||||
+ "MCSqWF/n0H3I7FdV4i7gg1sUDirK8gvPdgEiygdt6VLlE3mOxX8ualYZViTVyklc\n"
|
||||
+ "JRt7bY9I4OI9w6bf4NsV6/XHAoGAISd5Dj/sL2yQ/MSCDUZfbrJWQJCU+BHQv1bF\n"
|
||||
+ "oQfmeTjPFCk2jiRpmWJkdh9eZBAx5luulGG+fyTh/rjnO0/Z7uJv2OFKPHldOQTG\n"
|
||||
+ "TaqiSKrR62qswte+TKq/psakoNH9xhkCsltQ3MMfc6k0kSwwhda9p1pXWK3VFkUh\n"
|
||||
+ "EazY3PECgYAoo8jvvQTKlXBmnVU1R//16fCklJXqYcEeOAO0CgyPCxHuAULK++M/\n"
|
||||
+ "HRHd+6FippoH4ppSACEqQO5TwBTYxgOCwOcYZaRDvYZqEbgPNlf3oZ73kRyoIeAK\n"
|
||||
+ "zvaXPNUuUEoW4E9Y2M+9SzF+975TTjqwjBgoCIF3xd+xQfgV9vkB6w==\n"
|
||||
+ "-----END RSA PRIVATE KEY-----";
|
||||
|
||||
Clock clock = Clock.fixed(Instant.parse("2007-12-03T10:15:30.00Z"), ZoneId.of("UTC"));
|
||||
|
||||
@BeforeEach
|
||||
void before() {
|
||||
|
||||
RestTemplate restTemplate = VaultClients.createRestTemplate();
|
||||
restTemplate.setUriTemplateHandler(new PrefixAwareUriTemplateHandler());
|
||||
|
||||
this.mockRest = MockRestServiceServer.createServer(restTemplate);
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginShouldObtainToken() {
|
||||
|
||||
PcfAuthenticationOptions options = PcfAuthenticationOptions.builder()
|
||||
.instanceCertificate(() -> "foo") //
|
||||
.instanceKey(() -> instanceKey) //
|
||||
.role("dev-role") //
|
||||
.clock(clock) //
|
||||
.build();
|
||||
|
||||
PcfAuthentication authentication = new PcfAuthentication(options, restTemplate);
|
||||
|
||||
expectLoginRequest();
|
||||
|
||||
VaultToken login = authentication.login();
|
||||
assertThat(login).isInstanceOf(LoginToken.class);
|
||||
assertThat(login.getToken()).isEqualTo("my-token");
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWithStepsShouldObtainToken() {
|
||||
|
||||
PcfAuthenticationOptions options = PcfAuthenticationOptions.builder()
|
||||
.instanceCertificate(() -> "foo") //
|
||||
.instanceKey(() -> instanceKey) //
|
||||
.role("dev-role") //
|
||||
.clock(clock) //
|
||||
.build();
|
||||
|
||||
expectLoginRequest();
|
||||
|
||||
AuthenticationStepsExecutor authentication = new AuthenticationStepsExecutor(
|
||||
PcfAuthentication.createAuthenticationSteps(options), restTemplate);
|
||||
|
||||
VaultToken login = authentication.login();
|
||||
assertThat(login).isInstanceOf(LoginToken.class);
|
||||
assertThat(login.getToken()).isEqualTo("my-token");
|
||||
}
|
||||
|
||||
private void expectLoginRequest() {
|
||||
|
||||
mockRest.expect(requestTo("/auth/pcf/login")).andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.role").value("dev-role"))
|
||||
.andExpect(jsonPath("$.signature").exists())
|
||||
.andExpect(jsonPath("$.cf_instance_cert").value("foo"))
|
||||
.andExpect(jsonPath("$.signing_time").value("2007-12-03T10:15:30Z"))
|
||||
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON)
|
||||
.body("{" + "\"auth\":{\"client_token\":\"my-token\"}" + "}"));
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import org.springframework.vault.client.VaultEndpoint;
|
||||
import org.springframework.vault.config.AbstractReactiveVaultConfiguration;
|
||||
import org.springframework.vault.support.SslConfiguration;
|
||||
import org.springframework.vault.util.Settings;
|
||||
import org.springframework.vault.util.TestRestTemplateFactory;
|
||||
|
||||
/**
|
||||
* Test configuration for Vault integration tests.
|
||||
@@ -34,7 +35,7 @@ public class VaultIntegrationTestConfiguration
|
||||
|
||||
@Override
|
||||
public VaultEndpoint vaultEndpoint() {
|
||||
return new VaultEndpoint();
|
||||
return TestRestTemplateFactory.TEST_VAULT_ENDPOINT;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,7 +18,6 @@ package org.springframework.vault.core;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -27,6 +26,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.util.Base64Utils;
|
||||
import org.springframework.vault.support.VaultMount;
|
||||
import org.springframework.vault.support.VaultResponse;
|
||||
import org.springframework.vault.support.VaultTransitKeyConfiguration;
|
||||
@@ -103,7 +103,7 @@ class VaultTemplateTransitIntegrationTests extends IntegrationTestSupport {
|
||||
|
||||
VaultResponse response = vaultOperations.write("transit/encrypt/mykey",
|
||||
Collections.singletonMap("plaintext",
|
||||
Base64.encodeBase64String("that message is secret".getBytes())));
|
||||
Base64Utils.encodeToString("that message is secret".getBytes())));
|
||||
|
||||
assertThat((String) response.getRequiredData().get("ciphertext")).isNotEmpty();
|
||||
}
|
||||
@@ -113,13 +113,13 @@ class VaultTemplateTransitIntegrationTests extends IntegrationTestSupport {
|
||||
|
||||
VaultResponse response = vaultOperations.write("transit/encrypt/mykey",
|
||||
Collections.singletonMap("plaintext",
|
||||
Base64.encodeBase64String("that message is secret".getBytes())));
|
||||
Base64Utils.encodeToString("that message is secret".getBytes())));
|
||||
|
||||
VaultResponse decrypted = vaultOperations.write("transit/decrypt/mykey",
|
||||
Collections.singletonMap("ciphertext",
|
||||
response.getRequiredData().get("ciphertext")));
|
||||
|
||||
assertThat((String) decrypted.getRequiredData().get("plaintext")).isEqualTo(
|
||||
Base64.encodeBase64String("that message is secret".getBytes()));
|
||||
Base64Utils.encodeToString("that message is secret".getBytes()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import org.springframework.web.reactive.function.client.WebClient;
|
||||
*/
|
||||
public class TestWebClientFactory {
|
||||
|
||||
private static final VaultEndpoint TEST_VAULT_ENDPOINT = new VaultEndpoint();
|
||||
private static final VaultEndpoint TEST_VAULT_ENDPOINT = TestRestTemplateFactory.TEST_VAULT_ENDPOINT;
|
||||
|
||||
/**
|
||||
* Create a new {@link WebClient} using the {@link SslConfiguration}. See
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
<aws-java-sdk.version>1.11.605</aws-java-sdk.version>
|
||||
<google-api-services-iam.version>v1-rev20190704-1.30.1</google-api-services-iam.version>
|
||||
<google-auth-library-oauth2-http.version>0.16.2</google-auth-library-oauth2-http.version>
|
||||
<bcpkix-jdk15on.version>1.62</bcpkix-jdk15on.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
@@ -150,6 +151,15 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- BouncyCastle -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk15on</artifactId>
|
||||
<version>${bcpkix-jdk15on.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
@@ -209,7 +219,8 @@
|
||||
<repository>
|
||||
<id>sonatype-nexus-staging</id>
|
||||
<name>Nexus Release Repository</name>
|
||||
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
|
||||
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/
|
||||
</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
* SpEL support in `@Secret`.
|
||||
* Add support for Jetty as reactive HttpClient.
|
||||
* `LifecycleAwareSessionManager` and `ReactiveLifecycleAwareSessionManager` emit now ``AuthenticationEvent``s.
|
||||
* <<vault.authentication.pcf>>.
|
||||
* Deprecation of `AppIdAuthentication`. Use `AppRoleAuthentication` instead as recommended by HashiCorp Vault.
|
||||
|
||||
[[new-features.2-1-0]]
|
||||
|
||||
@@ -531,6 +531,52 @@ See also:
|
||||
* https://www.vaultproject.io/docs/auth/gcp.html[Vault Documentation: Using the GCP auth backend]
|
||||
* https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signJwt[GCP Documentation: projects.serviceAccounts.signJwt][[vault.authentication.gcpiam]]
|
||||
|
||||
[[vault.authentication.pcf]]
|
||||
== PCF authentication
|
||||
|
||||
The https://www.vaultproject.io/docs/auth/pcf.html[pcf]
|
||||
auth backend allows Vault login for PCF instances.
|
||||
It leverages https://content.pivotal.io/blog/new-in-pcf-2-1-app-container-identity-assurance-via-automatic-cert-rotation[PCF's App and Container Identity Assurance].
|
||||
|
||||
PCF authentication uses the instance key and certificate to create a signature that is validated by Vault.
|
||||
If the signature matches, and potentially bound organization/space/application Id's match, Vault issues an appropriately-scoped token.
|
||||
|
||||
Instance credentials are available from files at `CF_INSTANCE_CERT` and
|
||||
`CF_INSTANCE_KEY` variables.
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
class AppConfig extends AbstractVaultConfiguration {
|
||||
|
||||
// …
|
||||
|
||||
@Override
|
||||
public ClientAuthentication clientAuthentication() {
|
||||
|
||||
PcfAuthenticationOptions options = PcfAuthenticationOptions.builder()
|
||||
.role(…).build();
|
||||
|
||||
PcfAuthentication authentication = new PcfAuthentication(options,
|
||||
restOperations());
|
||||
}
|
||||
|
||||
// …
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
`PcfAuthenticationOptions` requires the https://www.bouncycastle.org/latest_releases.html[BouncyCastle]
|
||||
library for creating RSA-PSS signatures.
|
||||
|
||||
You can configure the authentication via `PcfAuthenticationOptions`.
|
||||
|
||||
See also:
|
||||
|
||||
* https://www.vaultproject.io/docs/auth/pcf.html[Vault Documentation:
|
||||
Using the PCF auth backend]
|
||||
|
||||
[[vault.authentication.clientcert]]
|
||||
== TLS certificate authentication
|
||||
|
||||
|
||||
Reference in New Issue
Block a user