Add support for GCP authentication.

We now provide GCP IAM and GCP GCE authentication support for service accounts based on JSON Web Token.
GCP IAM requires Google Cloud Java SDK for credential and IAM interaction.

  GcpIamAuthenticationOptions options = GcpIamAuthenticationOptions.builder()
      .role(…).credential(GoogleCredentials.getApplicationDefault()).build();

  GcpIamAuthentication authentication = new GcpIamAuthentication(options, restOperations());

Closes gh-126.
This commit is contained in:
Mark Paluch
2018-03-01 15:23:00 +01:00
parent a188ff1234
commit 1711479dc0
13 changed files with 1288 additions and 5 deletions

View File

@@ -140,6 +140,28 @@
</exclusions>
</dependency>
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-iam</artifactId>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
<optional>true</optional>
</dependency>
<!-- Testing -->
<dependency>

View File

@@ -0,0 +1,172 @@
/*
* Copyright 2018 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
*
* http://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.LinkedHashMap;
import java.util.Map;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.vault.VaultException;
import org.springframework.vault.authentication.AuthenticationSteps.HttpRequest;
import org.springframework.vault.support.VaultToken;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestOperations;
import static org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder.get;
/**
* GCP GCE (Google Compute Engine)-based login implementation using GCE's metadata service
* to create signed JSON Web Token.
* <p/>
* This authentication method uses Googles GCE's metadata service in combination with the
* default/specified service account to obtain an identity document as JWT using a HTTP
* client. Credentials and authenticity are implied from the runtime itself and are not
* required to be configured.
*
* @author Mark Paluch
* @since 2.1
* @see GcpComputeAuthenticationOptions
* @see <a href="https://www.vaultproject.io/docs/auth/gcp.html">Auth Backend: gcp
* (IAM)</a>
* @see <a
* href="https://cloud.google.com/compute/docs/instances/verifying-instance-identity">Google
* Compute Engine: Verifying the Identity of Instances</a>
*/
public class GcpComputeAuthentication extends GcpJwtAuthenticationSupport implements
ClientAuthentication, AuthenticationStepsFactory {
public static final String COMPUTE_METADATA_URL_TEMPLATE = "http://metadata/computeMetadata/v1/instance/service-accounts/{serviceAccount}/identity"
+ "?audience={audience}&format={format}";
private final GcpComputeAuthenticationOptions options;
private final RestOperations googleMetadataRestOperations;
/**
* Create a new {@link GcpComputeAuthentication} instance given
* {@link GcpComputeAuthenticationOptions} and {@link RestOperations} for Vault and
* Google API use.
*
* @param options must not be {@literal null}.
* @param vaultRestOperations must not be {@literal null}.
*/
public GcpComputeAuthentication(GcpComputeAuthenticationOptions options,
RestOperations vaultRestOperations) {
this(options, vaultRestOperations, vaultRestOperations);
}
/**
* Create a new {@link GcpComputeAuthentication} instance given
* {@link GcpComputeAuthenticationOptions} and {@link RestOperations} for Vault and
* Google API use.
*
* @param options must not be {@literal null}.
* @param vaultRestOperations must not be {@literal null}.
* @param googleMetadataRestOperations must not be {@literal null}.
*/
public GcpComputeAuthentication(GcpComputeAuthenticationOptions options,
RestOperations vaultRestOperations,
RestOperations googleMetadataRestOperations) {
super(vaultRestOperations);
Assert.notNull(options, "GcpGceAuthenticationOptions must not be null");
Assert.notNull(googleMetadataRestOperations,
"Google Metadata RestOperations must not be null");
this.options = options;
this.googleMetadataRestOperations = googleMetadataRestOperations;
}
/**
* Creates a {@link AuthenticationSteps} for GCE authentication given
* {@link GcpComputeAuthenticationOptions}.
*
* @param options must not be {@literal null}.
* @return {@link AuthenticationSteps} for cubbyhole authentication.
*/
public static AuthenticationSteps createAuthenticationSteps(
GcpComputeAuthenticationOptions options) {
Assert.notNull(options, "CubbyholeAuthenticationOptions must not be null");
String serviceAccount = options.getServiceAccount();
String audience = getAudience(options.getRole());
HttpRequest<String> jwtRequest = get(COMPUTE_METADATA_URL_TEMPLATE,
serviceAccount, audience, "full") //
.with(getMetadataHttpHeaders()) //
.as(String.class);
return AuthenticationSteps.fromHttpRequest(jwtRequest)
//
.map(jwt -> createRequestBody(options.getRole(), jwt))
.login("auth/{mount}/login", options.getPath());
}
@Override
public VaultToken login() throws VaultException {
String signedJwt = signJwt();
return doLogin("GCP-GCE", signedJwt, this.options.getPath(),
this.options.getRole());
}
@Override
public AuthenticationSteps getAuthenticationSteps() {
return createAuthenticationSteps(options);
}
protected String signJwt() {
try {
Map<String, String> urlParameters = new LinkedHashMap<>();
urlParameters.put("serviceAccount", this.options.getServiceAccount());
urlParameters.put("audience", getAudience(this.options.getRole()));
urlParameters.put("format", "full");
HttpHeaders headers = getMetadataHttpHeaders();
HttpEntity<Object> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = googleMetadataRestOperations.exchange(
COMPUTE_METADATA_URL_TEMPLATE, HttpMethod.GET, entity, String.class,
urlParameters);
return response.getBody();
}
catch (HttpStatusCodeException e) {
throw new VaultException("Cannot obtain signed identity", e);
}
}
private static HttpHeaders getMetadataHttpHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.set("Metadata-Flavor", "Google");
return headers;
}
private static String getAudience(String role) {
return String.format("https://localhost:8200/vault/%s", role);
}
}

View File

@@ -0,0 +1,159 @@
/*
* Copyright 2018 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
*
* http://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.lang.Nullable;
import org.springframework.util.Assert;
/**
* Authentication options for {@link GcpComputeAuthentication}.
* <p />
* Authentication options provide the path, role and an optional service account
* identifier. Instances of this class are immutable once constructed.
*
* @author Mark Paluch
* @see GcpComputeAuthentication
* @see #builder()
* @since 2.1
*/
public class GcpComputeAuthenticationOptions {
public static final String DEFAULT_GCP_AUTHENTICATION_PATH = "gcp";
/**
* Path of the gcp authentication backend mount.
*/
private final String path;
/**
* GCE service account identifier.
*/
private final String serviceAccount;
/**
* Name of the role against which the login is being attempted. If role is not
* specified, the friendly name (i.e., role name or username) of the IAM principal
* authenticated. If a matching role is not found, login fails.
*/
private final String role;
private GcpComputeAuthenticationOptions(String path, String serviceAccount,
String role) {
this.path = path;
this.serviceAccount = serviceAccount;
this.role = role;
}
/**
* @return a new {@link GcpComputeAuthenticationOptionsBuilder}.
*/
public static GcpComputeAuthenticationOptionsBuilder builder() {
return new GcpComputeAuthenticationOptionsBuilder();
}
/**
* @return the path of the gcp authentication backend mount.
*/
public String getPath() {
return path;
}
/**
* @return the GCE service account identifier.
*/
public String getServiceAccount() {
return serviceAccount;
}
/**
* @return name of the role against which the login is being attempted.
*/
public String getRole() {
return role;
}
/**
* Builder for {@link GcpComputeAuthenticationOptions}.
*/
public static class GcpComputeAuthenticationOptionsBuilder {
private String path = DEFAULT_GCP_AUTHENTICATION_PATH;
@Nullable
private String role;
private String serviceAccount = "default";
GcpComputeAuthenticationOptionsBuilder() {
}
/**
* Configure the mount path, defaults to {@literal aws}.
*
* @param path must not be empty or {@literal null}.
* @return {@code this} {@link GcpComputeAuthenticationOptionsBuilder}.
*/
public GcpComputeAuthenticationOptionsBuilder path(String path) {
Assert.hasText(path, "Path must not be empty");
this.path = path;
return this;
}
/**
* Configure the service account identifier. Uses the {@code default} service
* account if left unconfigured.
*
* @param serviceAccount must not be empty or {@literal null}.
* @return {@code this} {@link GcpComputeAuthenticationOptionsBuilder}.
*/
public GcpComputeAuthenticationOptionsBuilder serviceAccount(String serviceAccount) {
Assert.hasText(serviceAccount, "Service account must not be null");
this.serviceAccount = serviceAccount;
return this;
}
/**
* Configure the name of the role against which the login is being attempted.
*
* @param role must not be empty or {@literal null}.
* @return {@code this} {@link GcpComputeAuthenticationOptionsBuilder}.
*/
public GcpComputeAuthenticationOptionsBuilder role(String role) {
Assert.hasText(role, "Role must not be null or empty");
this.role = role;
return this;
}
/**
* Build a new {@link GcpComputeAuthenticationOptions} instance.
*
* @return a new {@link GcpComputeAuthenticationOptions}.
*/
public GcpComputeAuthenticationOptions build() {
Assert.notNull(role, "Role must not be null");
return new GcpComputeAuthenticationOptions(path, serviceAccount, role);
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2018 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
*
* http://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.IOException;
import java.util.function.Supplier;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
/**
* Interface to obtain a {@link GoogleCredential} for GCP IAM authentication.
* Implementations are used by {@link GcpIamAuthentication}.
*
* @author Mark Paluch
* @since 2.1
* @see GcpIamAuthentication
*/
@FunctionalInterface
public interface GcpCredentialSupplier extends Supplier<GoogleCredential> {
/**
* Exception-safe helper to get {@link GoogleCredential} from {@link #getCredential}.
*
* @return the GoogleCredential for JWT signing.
*/
@Override
default GoogleCredential get() {
try {
return getCredential();
}
catch (IOException e) {
throw new IllegalStateException("Cannot obtain GoogleCredential", e);
}
}
/**
* Get a {@link GoogleCredential} for GCP IAM authentication via JWT signing.
*
* @return the {@link GoogleCredential}.
* @throws IOException if the credential lookup fails.
*/
GoogleCredential getCredential() throws IOException;
}

View File

@@ -0,0 +1,170 @@
/*
* Copyright 2018 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
*
* http://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.IOException;
import java.security.GeneralSecurityException;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
import com.google.api.client.googleapis.apache.GoogleApacheHttpTransport;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.iam.v1.Iam;
import com.google.api.services.iam.v1.Iam.Builder;
import com.google.api.services.iam.v1.Iam.Projects.ServiceAccounts.SignJwt;
import com.google.api.services.iam.v1.model.SignJwtRequest;
import com.google.api.services.iam.v1.model.SignJwtResponse;
import com.google.auth.oauth2.GoogleCredentials;
import org.springframework.util.Assert;
import org.springframework.vault.VaultException;
import org.springframework.vault.support.VaultToken;
import org.springframework.web.client.RestOperations;
/**
* GCP IAM login implementation using GCP IAM service accounts to legitimate its
* authenticity via JSON Web Token.
* <p/>
* This authentication method uses Googles IAM API to obtain a signed token for a specific
* {@link com.google.api.client.auth.oauth2.Credential}. Project and service account
* details are obtained from a {@link GoogleCredential} that can be retrieved either from
* a JSON file or the runtime environment (GAE, GCE).
* <p/>
* {@link GcpIamAuthentication} uses Google Java API that uses synchronous API.
*
* @author Mark Paluch
* @since 2.1
* @see GcpIamAuthenticationOptions
* @see HttpTransport
* @see GoogleCredential
* @see GoogleCredentials#getApplicationDefault()
* @see RestOperations
* @see <a href="https://www.vaultproject.io/docs/auth/gcp.html">Auth Backend: gcp
* (IAM)</a>
* @see <a
* href="https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signJwt">GCP:
* projects.serviceAccounts.signJwt</a>
*/
public class GcpIamAuthentication extends GcpJwtAuthenticationSupport implements
ClientAuthentication {
private static final JsonFactory JSON_FACTORY = new JacksonFactory();
private final GcpIamAuthenticationOptions options;
private final HttpTransport httpTransport;
private final GoogleCredential credential;
/**
* Create a new instance of {@link GcpIamAuthentication} given
* {@link GcpIamAuthenticationOptions} and {@link RestOperations}. This constructor
* initializes {@link GoogleApacheHttpTransport} for Google API usage.
*
* @param options must not be {@literal null}.
* @param restOperations HTTP client for for Vault login, must not be {@literal null}.
* @throws GeneralSecurityException thrown by
* {@link GoogleApacheHttpTransport#newTrustedTransport()}.
* @throws IOException thrown by
* {@link GoogleApacheHttpTransport#newTrustedTransport()}.
*/
public GcpIamAuthentication(GcpIamAuthenticationOptions options,
RestOperations restOperations) throws GeneralSecurityException, IOException {
this(options, restOperations, GoogleApacheHttpTransport.newTrustedTransport());
}
/**
* Create a new instance of {@link GcpIamAuthentication} given
* {@link GcpIamAuthenticationOptions}, {@link RestOperations} and
* {@link HttpTransport}.
*
* @param options must not be {@literal null}.
* @param restOperations HTTP client for for Vault login, must not be {@literal null}.
* @param httpTransport HTTP client for Google API use, must not be {@literal null}.
*/
public GcpIamAuthentication(GcpIamAuthenticationOptions options,
RestOperations restOperations, HttpTransport httpTransport) {
super(restOperations);
Assert.notNull(options, "GcpIamAuthenticationOptions must not be null!");
Assert.notNull(restOperations, "RestOperations must not be null!");
Assert.notNull(httpTransport, "HttpTransport must not be null!");
this.options = options;
this.httpTransport = httpTransport;
this.credential = options.getCredentialSupplier().get();
}
@SuppressWarnings("unchecked")
@Override
public VaultToken login() throws VaultException {
String signedJwt = signJwt();
return doLogin("GCP-IAM", signedJwt, this.options.getPath(),
this.options.getRole());
}
protected String signJwt() {
String projectId = credential.getServiceAccountProjectId();
String serviceAccount = credential.getServiceAccountId();
Map<String, Object> jwtPayload = getJwtPayload(options, serviceAccount);
Iam iam = new Builder(httpTransport, JSON_FACTORY, credential)
.setApplicationName("Spring Vault/" + getClass().getName()).build();
try {
String payload = JSON_FACTORY.toString(jwtPayload);
SignJwtRequest request = new SignJwtRequest();
request.setPayload(payload);
SignJwt signJwt = iam
.projects()
.serviceAccounts()
.signJwt(
String.format("projects/%s/serviceAccounts/%s", projectId,
serviceAccount), request);
SignJwtResponse response = signJwt.execute();
return response.getSignedJwt();
}
catch (IOException e) {
throw new VaultException("Cannot sign JWT", e);
}
}
private static Map<String, Object> getJwtPayload(GcpIamAuthenticationOptions options,
String serviceAccount) {
Instant validUntil = options.getClock().instant().plus(options.getJwtValidity());
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("sub", serviceAccount);
payload.put("aud", "vault/" + options.getRole());
payload.put("exp", validUntil.getEpochSecond());
return payload;
}
}

View File

@@ -0,0 +1,247 @@
/*
* Copyright 2018 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
*
* http://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.Duration;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Authentication options for {@link GcpIamAuthentication}.
* <p/>
* Authentication options provide the path, a {@link GcpCredentialSupplier}, role and JWT
* expiry for GCP IAM authentication. Instances of this class are immutable once
* constructed.
*
* @author Mark Paluch
* @see GcpIamAuthentication
* @see #builder()
* @since 2.1
*/
public class GcpIamAuthenticationOptions {
public static final String DEFAULT_GCP_AUTHENTICATION_PATH = "gcp";
/**
* Path of the gcp authentication backend mount.
*/
private final String path;
private final GcpCredentialSupplier credentialSupplier;
/**
* Name of the role against which the login is being attempted. If role is not
* specified, the friendly name (i.e., role name or username) of the IAM principal
* authenticated. If a matching role is not found, login fails.
*/
private final String role;
/**
* JWT validity/expiration.
*/
private final Duration jwtValidity;
/**
* {@link Clock} to calculate JWT expiration.
*/
private final Clock clock;
private GcpIamAuthenticationOptions(String path,
GcpCredentialSupplier credentialSupplier, String role, Duration jwtValidity,
Clock clock) {
this.path = path;
this.credentialSupplier = credentialSupplier;
this.role = role;
this.jwtValidity = jwtValidity;
this.clock = clock;
}
/**
* @return a new {@link GcpIamAuthenticationOptionsBuilder}.
*/
public static GcpIamAuthenticationOptionsBuilder builder() {
return new GcpIamAuthenticationOptionsBuilder();
}
/**
* @return the path of the gcp authentication backend mount.
*/
public String getPath() {
return path;
}
/**
* @return the gcp {@link Credential} supplier.
*/
public GcpCredentialSupplier getCredentialSupplier() {
return credentialSupplier;
}
/**
* @return name of the role against which the login is being attempted.
*/
public String getRole() {
return role;
}
/**
* @return {@link Duration} of the JWT to generate.
*/
public Duration getJwtValidity() {
return jwtValidity;
}
/**
* @return {@link Clock} used to calculate epoch seconds until the JWT expires.
*/
public Clock getClock() {
return clock;
}
/**
* Builder for {@link GcpIamAuthenticationOptions}.
*/
public static class GcpIamAuthenticationOptionsBuilder {
private String path = DEFAULT_GCP_AUTHENTICATION_PATH;
@Nullable
private String role;
@Nullable
private GcpCredentialSupplier credentialSupplier;
private Duration jwtValidity = Duration.ofMinutes(15);
private Clock clock = Clock.systemDefaultZone();
GcpIamAuthenticationOptionsBuilder() {
}
/**
* Configure the mount path, defaults to {@literal aws}.
*
* @param path must not be empty or {@literal null}.
* @return {@code this} {@link GcpIamAuthenticationOptionsBuilder}.
*/
public GcpIamAuthenticationOptionsBuilder path(String path) {
Assert.hasText(path, "Path must not be empty");
this.path = path;
return this;
}
/**
* Configure static Google credentials, required to create a signed JWT. Either
* use static credentials or provide a
* {@link #credentialSupplier(GcpCredentialSupplier) credentials provider}.
*
* @param credential must not be {@literal null}.
* @return {@code this} {@link GcpIamAuthenticationOptionsBuilder}.
* @see #credentialSupplier(GcpCredentialSupplier)
*/
public GcpIamAuthenticationOptionsBuilder credential(GoogleCredential credential) {
Assert.notNull(credential, "Credential must not be null");
return credentialSupplier(() -> credential);
}
/**
* Configure an {@link AWSCredentialsProvider}, required to create a signed JWT.
* Alternatively, configure static {@link #credential(GoogleCredential)
* credentials}.
*
* @param credentialSupplier must not be {@literal null}.
* @return {@code this} {@link GcpIamAuthenticationOptionsBuilder}.
* @see #credential(GoogleCredential)
*/
public GcpIamAuthenticationOptionsBuilder credentialSupplier(
GcpCredentialSupplier credentialSupplier) {
Assert.notNull(credentialSupplier, "GcpCredentialSupplier must not be null");
this.credentialSupplier = credentialSupplier;
return this;
}
/**
* Configure the name of the role against which the login is being attempted.
*
* @param role must not be empty or {@literal null}.
* @return {@code this} {@link GcpIamAuthenticationOptionsBuilder}.
*/
public GcpIamAuthenticationOptionsBuilder role(String role) {
Assert.hasText(role, "Role must not be null or empty");
this.role = role;
return this;
}
/**
* Configure the {@link Duration} for the JWT expiration. This defaults to 15
* minutes and cannot be more than a hour.
*
* @param jwtValidity must not be {@literal null}.
* @return {@code this} {@link GcpIamAuthenticationOptionsBuilder}.
*/
public GcpIamAuthenticationOptionsBuilder jwtValidity(Duration jwtValidity) {
Assert.hasText(role, "JWT validity duration must not be null");
this.jwtValidity = jwtValidity;
return this;
}
/**
* Configure the {@link Clock} used to calculate epoch seconds until the JWT
* expiration.
*
* @param clock must not be {@literal null}.
* @return {@code this} {@link GcpIamAuthenticationOptionsBuilder}.
*/
public GcpIamAuthenticationOptionsBuilder clock(Clock clock) {
Assert.hasText(role, "Clock must not be null");
this.clock = clock;
return this;
}
/**
* Build a new {@link GcpIamAuthenticationOptions} instance.
*
* @return a new {@link GcpIamAuthenticationOptions}.
*/
public GcpIamAuthenticationOptions build() {
Assert.notNull(credentialSupplier, "GcpCredentialSupplier must not be null");
Assert.notNull(role, "Role must not be null");
return new GcpIamAuthenticationOptions(path, credentialSupplier, role,
jwtValidity, clock);
}
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2018 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
*
* http://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.HashMap;
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;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultToken;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestOperations;
/**
* Base class for GCP JWT-based authentication. Used by framework components.
*
* @author Mark Paluch
* @since 2.1
*/
public abstract class GcpJwtAuthenticationSupport {
private static final Log logger = LogFactory
.getLog(GcpJwtAuthenticationSupport.class);
private final RestOperations restOperations;
GcpJwtAuthenticationSupport(RestOperations restOperations) {
Assert.notNull(restOperations, "Vault RestOperations must not be null");
this.restOperations = restOperations;
}
/**
* Perform the actual Vault login given {@code signedJwt}.
*
* @param authenticationName authentication name for logging.
* @param signedJwt the JSON web token.
* @param path GCP authentication mount path.
* @param role Vault role.
* @return the {@link VaultToken}.
*/
VaultToken doLogin(String authenticationName, String signedJwt, String path,
String role) {
Map<String, String> login = createRequestBody(role, signedJwt);
try {
VaultResponse response = this.restOperations.postForObject(
"auth/{mount}/login", login, VaultResponse.class, path);
Assert.state(response != null && response.getAuth() != null,
"Auth field must not be null");
if (logger.isDebugEnabled()) {
if (response.getAuth().get("metadata") instanceof Map) {
Map<Object, Object> metadata = (Map<Object, Object>) response
.getAuth().get("metadata");
logger.debug(String.format(
"Login successful using %s authentication for user id %s",
authenticationName, metadata.get("service_account_email")));
}
else {
logger.debug("Login successful using " + authenticationName
+ " authentication");
}
}
return LoginTokenUtil.from(response.getAuth());
}
catch (HttpStatusCodeException e) {
throw new VaultException(String.format("Cannot login using %s: %s",
authenticationName,
VaultResponses.getError(e.getResponseBodyAsString())));
}
}
static Map<String, String> createRequestBody(String role, String signedJwt) {
Map<String, String> login = new HashMap<>();
login.put("role", role);
login.put("jwt", signedJwt);
return login;
}
}

View File

@@ -50,7 +50,7 @@ public class CubbyholeAuthenticationUnitTests {
private MockRestServiceServer mockRest;
@Before
public void before() throws Exception {
public void before() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(new PrefixAwareUriTemplateHandler());
@@ -132,7 +132,7 @@ public class CubbyholeAuthenticationUnitTests {
}
@Test
public void shouldLoginUsingStoredLogin() throws Exception {
public void shouldLoginUsingStoredLogin() {
mockRest.expect(requestTo("/cubbyhole/token")).andExpect(method(HttpMethod.GET))
.andExpect(header(VaultHttpHeaders.VAULT_TOKEN, "hello"))
@@ -153,7 +153,7 @@ public class CubbyholeAuthenticationUnitTests {
}
@Test
public void shouldRetrieveRenewabulityUsingStoredLogin() throws Exception {
public void shouldRetrieveRenewabilityUsingStoredLogin() {
mockRest.expect(requestTo("/cubbyhole/token")).andExpect(method(HttpMethod.GET))
.andExpect(header(VaultHttpHeaders.VAULT_TOKEN, "hello"))
@@ -185,7 +185,7 @@ public class CubbyholeAuthenticationUnitTests {
}
@Test
public void shouldFailUsingStoredLoginNoData() throws Exception {
public void shouldFailUsingStoredLoginNoData() {
mockRest.expect(requestTo("/cubbyhole/token")).andExpect(method(HttpMethod.GET))
.andExpect(header(VaultHttpHeaders.VAULT_TOKEN, "hello"))
@@ -208,7 +208,7 @@ public class CubbyholeAuthenticationUnitTests {
}
@Test
public void shouldFailUsingStoredMultipleEntries() throws Exception {
public void shouldFailUsingStoredMultipleEntries() {
mockRest.expect(requestTo("/cubbyhole/token")).andExpect(method(HttpMethod.GET))
.andExpect(header(VaultHttpHeaders.VAULT_TOKEN, "hello"))

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2018 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
*
* http://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.Before;
import org.junit.Test;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
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 GcpComputeAuthentication}.
*
* @author Mark Paluch
*/
public class GcpComputeAuthenticationUnitTests {
private RestTemplate restTemplate;
private MockRestServiceServer mockRest;
@Before
public void before() {
RestTemplate restTemplate = new RestTemplate();
this.mockRest = MockRestServiceServer.createServer(restTemplate);
this.restTemplate = restTemplate;
}
private void setupMocks() {
mockRest.expect(
requestTo("http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://localhost:8200/vault/dev-role&format=full"))
.andExpect(method(HttpMethod.GET))
.andRespond(
withSuccess().contentType(MediaType.TEXT_PLAIN).body("my-jwt"));
mockRest.expect(requestTo("/auth/gcp/login"))
.andExpect(method(HttpMethod.POST))
.andExpect(jsonPath("$.role").value("dev-role"))
.andExpect(jsonPath("$.jwt").value("my-jwt"))
.andRespond(
withSuccess()
.contentType(MediaType.APPLICATION_JSON)
.body("{"
+ "\"auth\":{\"client_token\":\"my-token\", \"renewable\": true, \"lease_duration\": 10}"
+ "}"));
}
@Test
public void shouldLogin() {
setupMocks();
GcpComputeAuthenticationOptions options = GcpComputeAuthenticationOptions
.builder().role("dev-role").build();
GcpComputeAuthentication authentication = new GcpComputeAuthentication(options,
restTemplate);
VaultToken login = authentication.login();
assertThat(login).isInstanceOf(LoginToken.class);
assertThat(login.getToken()).isEqualTo("my-token");
LoginToken loginToken = (LoginToken) login;
assertThat(loginToken.isRenewable()).isTrue();
assertThat(loginToken.getLeaseDuration()).isEqualTo(Duration.ofSeconds(10));
}
@Test
public void shouldLoginWithAuthenticationSteps() {
setupMocks();
GcpComputeAuthenticationOptions options = GcpComputeAuthenticationOptions
.builder().role("dev-role").build();
GcpComputeAuthentication authentication = new GcpComputeAuthentication(options,
restTemplate);
AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor(
authentication.getAuthenticationSteps(), restTemplate);
VaultToken login = executor.login();
assertThat(login).isInstanceOf(LoginToken.class);
assertThat(login.getToken()).isEqualTo("my-token");
LoginToken loginToken = (LoginToken) login;
assertThat(loginToken.isRenewable()).isTrue();
assertThat(loginToken.getLeaseDuration()).isEqualTo(Duration.ofSeconds(10));
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2018 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
*
* http://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.security.PrivateKey;
import java.time.Duration;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential.Builder;
import com.google.api.client.testing.http.MockHttpTransport;
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
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.mockito.Mockito.mock;
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 GcpIamAuthentication}.
*
* @author Mark Paluch
*/
public class GcpIamAuthenticationUnitTests {
private RestTemplate restTemplate;
private MockRestServiceServer mockRest;
private MockHttpTransport mockHttpTransport;
@Before
public void before() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(new PrefixAwareUriTemplateHandler());
this.mockRest = MockRestServiceServer.createServer(restTemplate);
this.restTemplate = restTemplate;
}
@Test
public void shouldLogin() {
MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
response.setStatusCode(200);
response.setContent("{\"keyId\":\"keyid\", \"signedJwt\":\"my-jwt\"}");
mockHttpTransport = new MockHttpTransport.Builder().setLowLevelHttpResponse(
response).build();
mockRest.expect(requestTo("/auth/gcp/login"))
.andExpect(method(HttpMethod.POST))
.andExpect(jsonPath("$.role").value("dev-role"))
.andExpect(jsonPath("$.jwt").value("my-jwt"))
.andRespond(
withSuccess()
.contentType(MediaType.APPLICATION_JSON)
.body("{"
+ "\"auth\":{\"client_token\":\"my-token\", \"renewable\": true, \"lease_duration\": 10}"
+ "}"));
PrivateKey privateKeyMock = mock(PrivateKey.class);
GoogleCredential credential = new Builder().setServiceAccountId("hello@world")
.setServiceAccountProjectId("foobar")
.setServiceAccountPrivateKey(privateKeyMock)
.setServiceAccountPrivateKeyId("key-id").build();
credential.setAccessToken("foobar");
GcpIamAuthenticationOptions options = GcpIamAuthenticationOptions.builder()
.role("dev-role").credential(credential).build();
GcpIamAuthentication authentication = new GcpIamAuthentication(options,
restTemplate, mockHttpTransport);
VaultToken login = authentication.login();
assertThat(login).isInstanceOf(LoginToken.class);
assertThat(login.getToken()).isEqualTo("my-token");
LoginToken loginToken = (LoginToken) login;
assertThat(loginToken.isRenewable()).isTrue();
assertThat(loginToken.getLeaseDuration()).isEqualTo(Duration.ofSeconds(10));
}
}

View File

@@ -124,6 +124,21 @@
<optional>true</optional>
</dependency>
<!-- GCP SDK -->
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-iam</artifactId>
<version>v1-rev233-1.23.0</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
<version>0.9.0</version>
<optional>true</optional>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -1,6 +1,10 @@
[[new-features]]
== New & Noteworthy
[[new-features.2-1-0]]
=== What's new in Spring Vault 2.1
* <<vault.authentication.gcpgce,GCP Compute>> and <<vault.authentication.gcpiam,GCP IAM>> authentication.
[[new-features.2-0-0]]
=== What's new in Spring Vault 2.0

View File

@@ -375,6 +375,112 @@ See also:
* https://www.vaultproject.io/docs/auth/aws.html[Vault Documentation: Using the AWS auth backend]
* http://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html[AWS Documentation: STS GetCallerIdentity]
[[vault.authentication.gcpgce]]
== GCP-GCE authentication
The https://www.vaultproject.io/docs/auth/gcp.html[gcp]
auth backend allows Vault login by using existing GCP (Google Cloud Platform) IAM and GCE credentials.
GCP GCE (Google Compute Engine) authentication creates a signature in the form of a
JSON Web Token (JWT) for a service account. A JWT for a Compute Engine instance
is obtained from the GCE metadata service using https://cloud.google.com/compute/docs/instances/verifying-instance-identity[Instance identification].
This API creates a JSON Web Token that can be used to confirm the instance identity.
Unlike most Vault authentication backends, this backend
does not require first-deploying, or provisioning security-sensitive
credentials (tokens, username/password, client certificates, etc.).
Instead, it treats GCP as a Trusted Third Party and uses the
cryptographically signed dynamic metadata information that uniquely
represents each GCP service account.
====
[source,java]
----
@Configuration
class AppConfig extends AbstractVaultConfiguration {
// …
@Override
public ClientAuthentication clientAuthentication() {
GcpComputeAuthenticationOptions options = GcpComputeAuthenticationOptions.builder()
.role(…).build();
GcpComputeAuthentication authentication = new GcpComputeAuthentication(options,
restOperations());
}
// …
}
----
====
`GcpIamAuthenticationOptions` requires the Google Cloud Java SDK dependency
(`com.google.apis:google-api-services-iam` and `com.google.auth:google-auth-library-oauth2-http`)
as the authentication implementation uses Google APIs for credentials and JWT signing.
You can configure the authentication via `GcpIamAuthenticationOptions`.
See also:
* https://www.vaultproject.io/docs/auth/gcp.html[Vault Documentation: Using the GCP auth backend]
* https://cloud.google.com/compute/docs/instances/verifying-instance-identity[GCP Documentation: Verifying the Identity of Instances]
[[vault.authentication.gcpiam]]
== GCP-IAM authentication
The https://www.vaultproject.io/docs/auth/gcp.html[gcp]
auth backend allows Vault login by using existing GCP (Google Cloud Platform) IAM and GCE credentials.
GCP IAM authentication creates a signature in the form of a JSON Web Token (JWT)
for a service account. A JWT for a service account is obtained by
calling GCP IAM's https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signJwt[`projects.serviceAccounts.signJwt`] API. The caller authenticates against GCP IAM
and proves thereby its identity. This Vault backend treats GCP as a Trusted Third Party.
IAM credentials can be obtained from either the runtime environment
or supplied externally as e.g. JSON. JSON is the preferred form as it
carries the project id and service account identifier required for calling
``projects.serviceAccounts.signJwt``.
====
[source,java]
----
@Configuration
class AppConfig extends AbstractVaultConfiguration {
// …
@Override
public ClientAuthentication clientAuthentication() {
GcpIamAuthenticationOptions options = GcpIamAuthenticationOptions.builder()
.role(…).credential(GoogleCredentials.getApplicationDefault()).build();
GcpIamAuthentication authentication = new GcpIamAuthentication(options,
restOperations());
}
// …
}
----
====
`GcpIamAuthenticationOptions` requires the Google Cloud Java SDK dependency
(`com.google.apis:google-api-services-iam` and `com.google.auth:google-auth-library-oauth2-http`)
as the authentication implementation uses Google APIs for credentials and JWT signing.
You can configure the authentication via `GcpIamAuthenticationOptions`.
NOTE: Google credentials require an OAuth 2 token maintaining the token lifecycle. All API
is synchronous therefore, `GcpIamAuthentication` does not support `AuthenticationSteps` which is
required for reactive usage.
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.clientcert]]
== TLS certificate authentication