Skip to content

Commit

Permalink
Allows to use GCE service credentials to sign blobs (#150)
Browse files Browse the repository at this point in the history
Fixes #141 

* Allows to use GCE service credentials to sign blobs (#141)

* Fix failing test (#141)

* Improve error reporting (#141)

* Fix issues for code review (#141)

* Don't change ServiceAccountSigner method signatures
* Use complete list of imports in ComputeEngineCredentials

* Restore individual assert imports

* No need for IAM_API_ROOT_URL constant

* Add tests for failure cases
  • Loading branch information
dtretyakov authored and chingor13 committed Jul 12, 2018
1 parent 13c2418 commit 8964e7c
Show file tree
Hide file tree
Showing 5 changed files with 441 additions and 16 deletions.
141 changes: 126 additions & 15 deletions oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,22 @@
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpStatusCodes;
import com.google.api.client.http.json.JsonHttpContent;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.util.GenericData;
import com.google.auth.ServiceAccountSigner;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.base.MoreObjects;
import com.google.common.io.BaseEncoding;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
Expand All @@ -57,13 +63,15 @@
*
* <p>Fetches access tokens from the Google Compute Engine metadata server.
*/
public class ComputeEngineCredentials extends GoogleCredentials {
public class ComputeEngineCredentials extends GoogleCredentials implements ServiceAccountSigner {

private static final Logger LOGGER = Logger.getLogger(ComputeEngineCredentials.class.getName());

// Note: the explicit IP address is used to avoid name server resolution issues.
static final String DEFAULT_METADATA_SERVER_URL = "http://169.254.169.254";

static final String SIGN_BLOB_URL_FORMAT = "https://iam.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob?alt=json";

// Note: the explicit `timeout` and `tries` below is a workaround. The underlying
// issue is that resolving an unknown host on some networks will take
// 20-30 seconds; making this timeout short fixes the issue, but
Expand All @@ -76,11 +84,15 @@ public class ComputeEngineCredentials extends GoogleCredentials {
static final int COMPUTE_PING_CONNECTION_TIMEOUT_MS = 500;

private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. ";
private static final String PARSE_ERROR_ACCOUNT = "Error parsing service account response. ";
private static final String PARSE_ERROR_SIGNATURE = "Error parsing signature response. ";
private static final String PARSE_ERROR_MESSAGE = "Error parsing error message response. ";
private static final long serialVersionUID = -4113476462526554235L;

private final String transportFactoryClassName;

private transient HttpTransportFactory transportFactory;
private transient String serviceAccountEmail;

/**
* Returns a credentials instance from the given transport factory
Expand Down Expand Up @@ -133,20 +145,7 @@ public static ComputeEngineCredentials create() {
*/
@Override
public AccessToken refreshAccessToken() throws IOException {
GenericUrl tokenUrl = new GenericUrl(getTokenServerEncodedUrl());
HttpRequest request =
transportFactory.create().createRequestFactory().buildGetRequest(tokenUrl);
JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY);
request.setParser(parser);
request.getHeaders().set("Metadata-Flavor", "Google");
request.setThrowExceptionOnExecuteError(false);
HttpResponse response;
try {
response = request.execute();
} catch (UnknownHostException exception) {
throw new IOException("ComputeEngineCredentials cannot find the metadata server. This is"
+ " likely because code is not running on Google Compute Engine.", exception);
}
HttpResponse response = getMetadataResponse(getTokenServerEncodedUrl());
int statusCode = response.getStatusCode();
if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) {
throw new IOException(String.format("Error code %s trying to get security access token from"
Expand Down Expand Up @@ -174,6 +173,23 @@ public AccessToken refreshAccessToken() throws IOException {
return new AccessToken(accessToken, new Date(expiresAtMilliseconds));
}

private HttpResponse getMetadataResponse(String url) throws IOException {
GenericUrl genericUrl = new GenericUrl(url);
HttpRequest request = transportFactory.create().createRequestFactory().buildGetRequest(genericUrl);
JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY);
request.setParser(parser);
request.getHeaders().set("Metadata-Flavor", "Google");
request.setThrowExceptionOnExecuteError(false);
HttpResponse response;
try {
response = request.execute();
} catch (UnknownHostException exception) {
throw new IOException("ComputeEngineCredentials cannot find the metadata server. This is"
+ " likely because code is not running on Google Compute Engine.", exception);
}
return response;
}

/** Return whether code is running on Google Compute Engine. */
static boolean runningOnComputeEngine(
HttpTransportFactory transportFactory, DefaultCredentialsProvider provider) {
Expand Down Expand Up @@ -227,6 +243,11 @@ public static String getTokenServerEncodedUrl() {
return getTokenServerEncodedUrl(DefaultCredentialsProvider.DEFAULT);
}

public static String getServiceAccountsUrl() {
return getMetadataServerUrl(DefaultCredentialsProvider.DEFAULT)
+ "/computeMetadata/v1/instance/service-accounts/?recursive=true";
}

@Override
public int hashCode() {
return Objects.hash(transportFactoryClassName);
Expand Down Expand Up @@ -261,6 +282,96 @@ public static Builder newBuilder() {
return new Builder();
}

@Override
public String getAccount() {
if (serviceAccountEmail == null) {
try {
serviceAccountEmail = getDefaultServiceAccount();
} catch (IOException ex) {
throw new RuntimeException("Failed to to get service account", ex);
}
}
return serviceAccountEmail;
}

@Override
public byte[] sign(byte[] toSign) {
BaseEncoding base64 = BaseEncoding.base64();
String signature;
try {
signature = getSignature(base64.encode(toSign));
} catch (IOException ex) {
throw new SigningException("Failed to sign the provided bytes", ex);
}
return base64.decode(signature);
}

private String getSignature(String bytes) throws IOException {
String signBlobUrl = String.format(SIGN_BLOB_URL_FORMAT, getAccount());
GenericUrl genericUrl = new GenericUrl(signBlobUrl);

GenericData signRequest = new GenericData();
signRequest.set("bytesToSign", bytes);
JsonHttpContent signContent = new JsonHttpContent(OAuth2Utils.JSON_FACTORY, signRequest);
HttpRequest request = transportFactory.create().createRequestFactory().buildPostRequest(genericUrl, signContent);
Map<String, List<String>> headers = getRequestMetadata();
HttpHeaders requestHeaders = request.getHeaders();
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
requestHeaders.put(entry.getKey(), entry.getValue());
}
JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY);
request.setParser(parser);
request.setThrowExceptionOnExecuteError(false);

HttpResponse response = request.execute();
int statusCode = response.getStatusCode();
if (statusCode >= 400 && statusCode < HttpStatusCodes.STATUS_CODE_SERVER_ERROR) {
GenericData responseError = response.parseAs(GenericData.class);
Map<String, Object> error = OAuth2Utils.validateMap(responseError, "error", PARSE_ERROR_MESSAGE);
String errorMessage = OAuth2Utils.validateString(error, "message", PARSE_ERROR_MESSAGE);
throw new IOException(String.format("Error code %s trying to sign provided bytes: %s",
statusCode, errorMessage));
}
if (statusCode != HttpStatusCodes.STATUS_CODE_OK) {
throw new IOException(String.format("Unexpected Error code %s trying to sign provided bytes: %s", statusCode,
response.parseAsString()));
}
InputStream content = response.getContent();
if (content == null) {
// Throw explicitly here on empty content to avoid NullPointerException from parseAs call.
// Mock transports will have success code with empty content by default.
throw new IOException("Empty content from sign blob server request.");
}

GenericData responseData = response.parseAs(GenericData.class);
return OAuth2Utils.validateString(responseData, "signature", PARSE_ERROR_SIGNATURE);
}

private String getDefaultServiceAccount() throws IOException {
HttpResponse response = getMetadataResponse(getServiceAccountsUrl());
int statusCode = response.getStatusCode();
if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) {
throw new IOException(String.format("Error code %s trying to get service accounts from"
+ " Compute Engine metadata. This may be because the virtual machine instance"
+ " does not have permission scopes specified.",
statusCode));
}
if (statusCode != HttpStatusCodes.STATUS_CODE_OK) {
throw new IOException(String.format("Unexpected Error code %s trying to get service accounts"
+ " from Compute Engine metadata: %s", statusCode,
response.parseAsString()));
}
InputStream content = response.getContent();
if (content == null) {
// Throw explicitly here on empty content to avoid NullPointerException from parseAs call.
// Mock transports will have success code with empty content by default.
throw new IOException("Empty content from metadata token server request.");
}
GenericData responseData = response.parseAs(GenericData.class);
Map<String, Object> defaultAccount = OAuth2Utils.validateMap(responseData, "default", PARSE_ERROR_ACCOUNT);
return OAuth2Utils.validateString(defaultAccount, "email", PARSE_ERROR_ACCOUNT);
}

public static class Builder extends GoogleCredentials.Builder {
private HttpTransportFactory transportFactory;

Expand Down
17 changes: 17 additions & 0 deletions oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,23 @@ static long validateLong(Map<String, Object> map, String key, String errorPrefix
return (Long) value;
}

/**
* Return the specified map from JSON or throw a helpful error message.
*/
@SuppressWarnings("unchecked")
static Map<String, Object> validateMap(Map<String, Object> map, String key, String errorPrefix)
throws IOException {
Object value = map.get(key);
if (value == null) {
throw new IOException(String.format(VALUE_NOT_FOUND_MESSAGE, errorPrefix, key));
}
if (!(value instanceof Map)) {
throw new IOException(
String.format(VALUE_WRONG_TYPE_MESSAGE, errorPrefix, "Map", key));
}
return (Map) value;
}

private OAuth2Utils() {
}
}
9 changes: 9 additions & 0 deletions oauth2_http/javatests/com/google/auth/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,15 @@ public static Map<String, String> parseQuery(String query) throws IOException {
return map;
}

public static String errorJson(String message) throws IOException {
GenericJson errorResponse = new GenericJson();
errorResponse.setFactory(JSON_FACTORY);
GenericJson errorObject = new GenericJson();
errorObject.put("message", message);
errorResponse.put("error", errorObject);
return errorResponse.toPrettyString();
}

private TestUtils() {
}
}
Loading

0 comments on commit 8964e7c

Please sign in to comment.