Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Signer for impersonatied credentials #279

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -1,5 +1,5 @@
/*
* Copyright 2018, Google Inc. All rights reserved.
* Copyright 2019, Google Inc. All rights reserved.
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
Expand Down 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() {
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
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>
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
*/
@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