Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Implement ServiceAccountSigner for ImpersonatedCredentials (#279)
* Add Signer for impersonatied credentials

* add lint fixes

* back to the future

* Update oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java

Co-Authored-By: Jeff Ching <chingor@google.com>
  • Loading branch information
salrashid123 and chingor13 committed Jul 9, 2019
1 parent fcbc426 commit 70767e3
Show file tree
Hide file tree
Showing 3 changed files with 252 additions and 7 deletions.
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -81,16 +87,20 @@
* System.out.println(b);
* </pre>
*/
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;
Expand Down Expand Up @@ -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 <a href="https://cloud.google.com/iam/credentials/reference/rest/v1/projects.serviceAccounts/signBlob">Blob Signing</a>
*/
@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<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);
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<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, "signedBlob", PARSE_ERROR_SIGNATURE);
}


private ImpersonatedCredentials(Builder builder) {
this.sourceCredentials = builder.getSourceCredentials();
this.targetPrincipal = builder.getTargetPrincipal();
Expand Down Expand Up @@ -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<String, Object> body = ImmutableMap.<String, Object>of("delegates", this.delegates, "scope",
Expand Down
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
}

Expand Down

0 comments on commit 70767e3

Please sign in to comment.