diff --git a/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java
index 895feb0ad..2276467c3 100644
--- a/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java
@@ -34,6 +34,7 @@
import static com.google.common.base.MoreObjects.firstNonNull;
import java.io.IOException;
+import java.io.InputStream;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
@@ -46,9 +47,11 @@
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpContent;
+import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.HttpStatusCodes;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.json.JsonHttpContent;
import com.google.api.client.json.JsonObjectParser;
@@ -57,6 +60,9 @@
import com.google.auth.http.HttpTransportFactory;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableMap;
+import com.google.common.io.BaseEncoding;
+
+import com.google.auth.ServiceAccountSigner;
/**
* ImpersonatedCredentials allowing credentials issued to a user or service account to impersonate
@@ -81,16 +87,20 @@
* System.out.println(b);
*
*/
-public class ImpersonatedCredentials extends GoogleCredentials {
+public class ImpersonatedCredentials extends GoogleCredentials implements ServiceAccountSigner {
private static final long serialVersionUID = -2133257318957488431L;
private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ss'Z'";
private static final int ONE_HOUR_IN_SECONDS = 3600;
private static final String CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
- private static final String IAM_ENDPOINT = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken";
+ private static final String IAM_ACCESS_TOKEN_ENDPOINT = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken";
+ private static final String IAM_ID_TOKEN_ENDPOINT = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateIdToken";
+ private static final String IAM_SIGN_ENDPOINT = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob";
private static final String SCOPE_EMPTY_ERROR = "Scopes cannot be null";
private static final String LIFETIME_EXCEEDED_ERROR = "lifetime must be less than or equal to 3600";
+ private static final String PARSE_ERROR_SIGNATURE = "Error parsing signature response. ";
+ private static final String PARSE_ERROR_MESSAGE = "Error parsing error message response. ";
private GoogleCredentials sourceCredentials;
private String targetPrincipal;
@@ -153,6 +163,85 @@ public static ImpersonatedCredentials create(GoogleCredentials sourceCredentials
.build();
}
+ /**
+ * Returns the email field of the serviceAccount that is being impersonated.
+ *
+ * @return email address of the impesonated service account.
+ */
+ @Override
+ public String getAccount() {
+ return this.targetPrincipal;
+ }
+
+ /**
+ * Signs the provided bytes using the private key associated with the impersonated
+ * service account
+ *
+ * @param toSign bytes to sign
+ * @return signed bytes
+ * @throws SigningException if the attempt to sign the provided bytes failed
+ * @see Blob Signing
+ */
+ @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(IAM_SIGN_ENDPOINT, getAccount());
+ GenericUrl genericUrl = new GenericUrl(signBlobUrl);
+
+ GenericData signRequest = new GenericData();
+ signRequest.set("delegates", this.delegates);
+ signRequest.set("payload", bytes);
+ JsonHttpContent signContent = new JsonHttpContent(OAuth2Utils.JSON_FACTORY, signRequest);
+ HttpTransport httpTransport = this.transportFactory.create();
+ HttpCredentialsAdapter adapter = new HttpCredentialsAdapter(sourceCredentials);
+ HttpRequestFactory requestFactory = httpTransport.createRequestFactory();
+
+ HttpRequest request = requestFactory.buildPostRequest(genericUrl, signContent);
+ Map> headers = getRequestMetadata();
+ HttpHeaders requestHeaders = request.getHeaders();
+ for (Map.Entry> entry : headers.entrySet()) {
+ requestHeaders.put(entry.getKey(), entry.getValue());
+ }
+ JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY);
+ adapter.initialize(request);
+ 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 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, "signedBlob", PARSE_ERROR_SIGNATURE);
+ }
+
+
private ImpersonatedCredentials(Builder builder) {
this.sourceCredentials = builder.getSourceCredentials();
this.targetPrincipal = builder.getTargetPrincipal();
@@ -192,7 +281,7 @@ public AccessToken refreshAccessToken() throws IOException {
HttpCredentialsAdapter adapter = new HttpCredentialsAdapter(sourceCredentials);
HttpRequestFactory requestFactory = httpTransport.createRequestFactory();
- String endpointUrl = String.format(IAM_ENDPOINT, this.targetPrincipal);
+ String endpointUrl = String.format(IAM_ACCESS_TOKEN_ENDPOINT, this.targetPrincipal);
GenericUrl url = new GenericUrl(endpointUrl);
Map body = ImmutableMap.of("delegates", this.delegates, "scope",
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java
index 16bb93fed..eb1aa0ddf 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java
@@ -31,9 +31,11 @@
package com.google.auth.oauth2;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import java.io.ByteArrayOutputStream;
@@ -51,6 +53,7 @@
import com.google.api.client.util.Clock;
import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.GoogleCredentialsTest.MockTokenServerTransportFactory;
+import com.google.auth.ServiceAccountSigner.SigningException;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -247,6 +250,96 @@ public void refreshAccessToken_invalidDate() throws IOException, IllegalStateExc
}
}
+ @Test
+ public void getAccount_sameAs() throws IOException {
+ GoogleCredentials sourceCredentials = getSourceCredentials();
+ MockIAMCredentialsServiceTransportFactory mtransportFactory =
+ new MockIAMCredentialsServiceTransportFactory();
+ mtransportFactory.transport.setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
+ mtransportFactory.transport.setAccessToken(ACCESS_TOKEN);
+ mtransportFactory.transport.setexpireTime(getDefaultExpireTime());
+ ImpersonatedCredentials targetCredentials = ImpersonatedCredentials.create(sourceCredentials,
+ IMPERSONATED_CLIENT_EMAIL, null, SCOPES, VALID_LIFETIME, mtransportFactory);
+
+ assertEquals(IMPERSONATED_CLIENT_EMAIL, targetCredentials.getAccount());
+ }
+
+
+ @Test
+ public void sign_sameAs() throws IOException {
+ GoogleCredentials sourceCredentials = getSourceCredentials();
+ MockIAMCredentialsServiceTransportFactory mtransportFactory =
+ new MockIAMCredentialsServiceTransportFactory();
+ mtransportFactory.transport.setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
+ mtransportFactory.transport.setAccessToken(ACCESS_TOKEN);
+ mtransportFactory.transport.setexpireTime(getDefaultExpireTime());
+ ImpersonatedCredentials targetCredentials = ImpersonatedCredentials.create(sourceCredentials,
+ IMPERSONATED_CLIENT_EMAIL, null, SCOPES, VALID_LIFETIME, mtransportFactory);
+
+ byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD};
+
+ mtransportFactory.transport.setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
+ mtransportFactory.transport.setSignedBlob(expectedSignature);
+
+ assertArrayEquals(expectedSignature, targetCredentials.sign(expectedSignature));
+ }
+
+ @Test
+ public void sign_accessDenied_throws() throws IOException {
+ GoogleCredentials sourceCredentials = getSourceCredentials();
+ MockIAMCredentialsServiceTransportFactory mtransportFactory =
+ new MockIAMCredentialsServiceTransportFactory();
+ mtransportFactory.transport.setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
+ mtransportFactory.transport.setAccessToken(ACCESS_TOKEN);
+ mtransportFactory.transport.setexpireTime(getDefaultExpireTime());
+ ImpersonatedCredentials targetCredentials = ImpersonatedCredentials.create(sourceCredentials,
+ IMPERSONATED_CLIENT_EMAIL, null, SCOPES, VALID_LIFETIME, mtransportFactory);
+
+ byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD};
+
+ mtransportFactory.transport.setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
+ mtransportFactory.transport.setSignedBlob(expectedSignature);
+ mtransportFactory.transport.setSigningErrorResponseCodeAndMessage(HttpStatusCodes.STATUS_CODE_FORBIDDEN, "Sign Error");
+
+ try {
+ byte[] bytes = {0xD, 0xE, 0xA, 0xD};
+ targetCredentials.sign(bytes);
+ fail("Signing should have failed");
+ } catch (SigningException e) {
+ assertEquals("Failed to sign the provided bytes", e.getMessage());
+ assertNotNull(e.getCause());
+ assertTrue(e.getCause().getMessage().contains("403"));
+ }
+ }
+
+ @Test
+ public void sign_serverError_throws() throws IOException {
+ GoogleCredentials sourceCredentials = getSourceCredentials();
+ MockIAMCredentialsServiceTransportFactory mtransportFactory =
+ new MockIAMCredentialsServiceTransportFactory();
+ mtransportFactory.transport.setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
+ mtransportFactory.transport.setAccessToken(ACCESS_TOKEN);
+ mtransportFactory.transport.setexpireTime(getDefaultExpireTime());
+ ImpersonatedCredentials targetCredentials = ImpersonatedCredentials.create(sourceCredentials,
+ IMPERSONATED_CLIENT_EMAIL, null, SCOPES, VALID_LIFETIME, mtransportFactory);
+
+ byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD};
+
+ mtransportFactory.transport.setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
+ mtransportFactory.transport.setSignedBlob(expectedSignature);
+ mtransportFactory.transport.setSigningErrorResponseCodeAndMessage(HttpStatusCodes.STATUS_CODE_SERVER_ERROR, "Sign Error");
+
+ try {
+ byte[] bytes = {0xD, 0xE, 0xA, 0xD};
+ targetCredentials.sign(bytes);
+ fail("Signing should have failed");
+ } catch (SigningException e) {
+ assertEquals("Failed to sign the provided bytes", e.getMessage());
+ assertNotNull(e.getCause());
+ assertTrue(e.getCause().getMessage().contains("500"));
+ }
+ }
+
@Test
public void hashCode_equals() throws IOException {
GoogleCredentials sourceCredentials = getSourceCredentials();
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockIAMCredentialsServiceTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockIAMCredentialsServiceTransport.java
index 71a0ecb6d..75dddaa14 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/MockIAMCredentialsServiceTransport.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/MockIAMCredentialsServiceTransport.java
@@ -32,25 +32,33 @@
package com.google.auth.oauth2;
import java.io.IOException;
+import java.util.List;
import com.google.api.client.http.LowLevelHttpRequest;
import com.google.api.client.http.LowLevelHttpResponse;
+import com.google.api.client.http.HttpStatusCodes;
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.Json;
import com.google.api.client.testing.http.MockHttpTransport;
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.common.io.BaseEncoding;
+import com.google.auth.TestUtils;
/**
* Transport that simulates the IAMCredentials server for access tokens.
*/
public class MockIAMCredentialsServiceTransport extends MockHttpTransport {
- private static final String IAM_ENDPOINT = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken";
-
+ private static final String IAM_ACCESS_TOKEN_ENDPOINT = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken";
+ private static final String IAM_ID_TOKEN_ENDPOINT = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateIdToken";
+ private static final String IAM_SIGN_ENDPOINT = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob";
private Integer tokenResponseErrorCode;
private String tokenResponseErrorContent;
private String targetPrincipal;
+ private byte[] signedBlob;
+ private int responseCode = HttpStatusCodes.STATUS_CODE_OK;
+ private String errorMessage;
private String accessToken;
private String expireTime;
@@ -78,11 +86,21 @@ public void setexpireTime(String expireTime) {
this.expireTime = expireTime;
}
+ public void setSignedBlob(byte[] signedBlob) {
+ this.signedBlob = signedBlob;
+ }
+
+ public void setSigningErrorResponseCodeAndMessage(int responseCode, String errorMessage) {
+ this.responseCode = responseCode;
+ this.errorMessage = errorMessage;
+ }
+
@Override
public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
- String formattedUrl = String.format(IAM_ENDPOINT, this.targetPrincipal);
- if (url.equals(formattedUrl)) {
+ String iamAccesssTokenformattedUrl = String.format(IAM_ACCESS_TOKEN_ENDPOINT, this.targetPrincipal);
+ String iamSignBlobformattedUrl = String.format(IAM_SIGN_ENDPOINT, this.targetPrincipal);
+ if (url.equals(iamAccesssTokenformattedUrl)) {
return new MockLowLevelHttpRequest(url) {
@Override
public LowLevelHttpResponse execute() throws IOException {
@@ -105,7 +123,52 @@ public LowLevelHttpResponse execute() throws IOException {
.setContent(refreshText);
}
};
+ } else if (url.equals(iamSignBlobformattedUrl) && responseCode != HttpStatusCodes.STATUS_CODE_OK) {
+ return new MockLowLevelHttpRequest(url) {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+
+ if (tokenResponseErrorCode != null) {
+ return new MockLowLevelHttpResponse()
+ .setStatusCode(tokenResponseErrorCode)
+ .setContentType(Json.MEDIA_TYPE)
+ .setContent(tokenResponseErrorContent);
+ }
+
+ BaseEncoding base64 = BaseEncoding.base64();
+ GenericJson refreshContents = new GenericJson();
+ refreshContents.setFactory(OAuth2Utils.JSON_FACTORY);
+ refreshContents.put("signedBlob", base64.encode(signedBlob));
+ String refreshText = refreshContents.toPrettyString();
+ return new MockLowLevelHttpResponse()
+ .setStatusCode(responseCode)
+ .setContent(TestUtils.errorJson(errorMessage));
+ }
+ };
+ } else if (url.equals(iamSignBlobformattedUrl)) {
+ return new MockLowLevelHttpRequest(url) {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+
+ if (tokenResponseErrorCode != null) {
+ return new MockLowLevelHttpResponse()
+ .setStatusCode(tokenResponseErrorCode)
+ .setContentType(Json.MEDIA_TYPE)
+ .setContent(tokenResponseErrorContent);
+ }
+
+ BaseEncoding base64 = BaseEncoding.base64();
+ GenericJson refreshContents = new GenericJson();
+ refreshContents.setFactory(OAuth2Utils.JSON_FACTORY);
+ refreshContents.put("signedBlob", base64.encode(signedBlob));
+ String refreshText = refreshContents.toPrettyString();
+ return new MockLowLevelHttpResponse()
+ .setContentType(Json.MEDIA_TYPE)
+ .setContent(refreshText);
+ }
+ };
}
+
return super.buildRequest(method, url);
}