Skip to content

Commit

Permalink
feat: Add GDCH support (#1087)
Browse files Browse the repository at this point in the history
* feat: add GDCH support

* feat: add GDCH support

* fix the format

* change the grant_type to token exchange type

* add more unit tests

* remove implementing JwtProvider from GdchCredentials class

* address the comments

* add custom httpTransport for ca cert

* add more unit test

* add more unit test

* add an overload method for fromJSON in GdchCredentials

* add more unit test

* address the comments

* fix test cert path

* remove `privateKeyFromPkcs8` from ServiceAccountCredentials and refer to the helper method in OAuth2Utils

* add a javadoc for `TransportFactoryForGdch`

* change `ca_cert_path` to optional

* revert ca cert path to required

* fix the format

* revert back to ca cert path optional

Co-authored-by: Timur Sadykov <stim@google.com>
  • Loading branch information
poanc and TimurSadykov committed Dec 6, 2022
1 parent 6f32ac3 commit cfafb2d
Show file tree
Hide file tree
Showing 15 changed files with 1,834 additions and 100 deletions.
528 changes: 528 additions & 0 deletions oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java

Large diffs are not rendered by default.

Expand Up @@ -56,6 +56,7 @@ public class GoogleCredentials extends OAuth2Credentials implements QuotaProject
static final String QUOTA_PROJECT_ID_HEADER_KEY = "x-goog-user-project";
static final String USER_FILE_TYPE = "authorized_user";
static final String SERVICE_ACCOUNT_FILE_TYPE = "service_account";
static final String GDCH_SERVICE_ACCOUNT_FILE_TYPE = "gdch_service_account";

protected final String quotaProjectId;

Expand Down Expand Up @@ -174,6 +175,9 @@ public static GoogleCredentials fromStream(
if (SERVICE_ACCOUNT_FILE_TYPE.equals(fileType)) {
return ServiceAccountCredentials.fromJson(fileContents, transportFactory);
}
if (GDCH_SERVICE_ACCOUNT_FILE_TYPE.equals(fileType)) {
return GdchCredentials.fromJson(fileContents);
}
if (ExternalAccountCredentials.EXTERNAL_ACCOUNT_FILE_TYPE.equals(fileType)) {
return ExternalAccountCredentials.fromJson(fileContents, transportFactory);
}
Expand Down
31 changes: 31 additions & 0 deletions oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java
Expand Up @@ -38,6 +38,9 @@
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.client.util.PemReader;
import com.google.api.client.util.PemReader.Section;
import com.google.api.client.util.SecurityUtils;
import com.google.auth.http.AuthHttpConstants;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.io.ByteStreams;
Expand All @@ -47,9 +50,16 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.math.BigDecimal;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
Expand All @@ -61,6 +71,8 @@ class OAuth2Utils {
static final String SIGNATURE_ALGORITHM = "SHA256withRSA";

static final String TOKEN_TYPE_ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token";
static final String TOKEN_TYPE_TOKEN_EXCHANGE = "urn:ietf:params:oauth:token-type:token-exchange";
static final String GRANT_TYPE_JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer";

static final URI TOKEN_SERVER_URI = URI.create("https://oauth2.googleapis.com/token");
static final URI TOKEN_REVOKE_URI = URI.create("https://oauth2.googleapis.com/revoke");
Expand Down Expand Up @@ -202,5 +214,24 @@ static Map<String, Object> validateMap(Map<String, Object> map, String key, Stri
return (Map) value;
}

/** Helper to convert from a PKCS#8 String to an RSA private key */
static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8) throws IOException {
Reader reader = new StringReader(privateKeyPkcs8);
Section section = PemReader.readFirstSectionAndClose(reader, "PRIVATE KEY");
if (section == null) {
throw new IOException("Invalid PKCS#8 data.");
}
byte[] bytes = section.getBase64DecodedBytes();
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
Exception unexpectedException;
try {
KeyFactory keyFactory = SecurityUtils.getRsaKeyFactory();
return keyFactory.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException exception) {
unexpectedException = exception;
}
throw new IOException("Unexpected exception reading PKCS#8 data", unexpectedException);
}

private OAuth2Utils() {}
}
Expand Up @@ -49,10 +49,7 @@
import com.google.api.client.util.ExponentialBackOff;
import com.google.api.client.util.GenericData;
import com.google.api.client.util.Joiner;
import com.google.api.client.util.PemReader;
import com.google.api.client.util.PemReader.Section;
import com.google.api.client.util.Preconditions;
import com.google.api.client.util.SecurityUtils;
import com.google.auth.RequestMetadataCallback;
import com.google.auth.ServiceAccountSigner;
import com.google.auth.http.HttpTransportFactory;
Expand All @@ -62,20 +59,15 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.Reader;
import java.io.StringReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
Expand Down Expand Up @@ -437,31 +429,12 @@ public static ServiceAccountCredentials fromPkcs8(
*/
static ServiceAccountCredentials fromPkcs8(
String privateKeyPkcs8, ServiceAccountCredentials.Builder builder) throws IOException {
PrivateKey privateKey = privateKeyFromPkcs8(privateKeyPkcs8);
PrivateKey privateKey = OAuth2Utils.privateKeyFromPkcs8(privateKeyPkcs8);
builder.setPrivateKey(privateKey);

return new ServiceAccountCredentials(builder);
}

/** Helper to convert from a PKCS#8 String to an RSA private key */
static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8) throws IOException {
Reader reader = new StringReader(privateKeyPkcs8);
Section section = PemReader.readFirstSectionAndClose(reader, "PRIVATE KEY");
if (section == null) {
throw new IOException("Invalid PKCS#8 data.");
}
byte[] bytes = section.getBase64DecodedBytes();
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
Exception unexpectedException;
try {
KeyFactory keyFactory = SecurityUtils.getRsaKeyFactory();
return keyFactory.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException exception) {
unexpectedException = exception;
}
throw new IOException("Unexpected exception reading PKCS#8 data", unexpectedException);
}

/**
* Returns credentials defined by a Service Account key file in JSON format from the Google
* Developers Console.
Expand Down Expand Up @@ -1038,7 +1011,7 @@ public Builder setPrivateKey(PrivateKey privateKey) {
}

public Builder setPrivateKeyString(String privateKeyPkcs8) throws IOException {
this.privateKey = privateKeyFromPkcs8(privateKeyPkcs8);
this.privateKey = OAuth2Utils.privateKeyFromPkcs8(privateKeyPkcs8);
return this;
}

Expand Down
Expand Up @@ -217,7 +217,7 @@ static ServiceAccountJwtAccessCredentials fromPkcs8(
URI defaultAudience,
String quotaProjectId)
throws IOException {
PrivateKey privateKey = ServiceAccountCredentials.privateKeyFromPkcs8(privateKeyPkcs8);
PrivateKey privateKey = OAuth2Utils.privateKeyFromPkcs8(privateKeyPkcs8);
return new ServiceAccountJwtAccessCredentials(
clientId, clientEmail, privateKey, privateKeyId, defaultAudience, quotaProjectId);
}
Expand Down
Expand Up @@ -89,6 +89,18 @@ public class DefaultCredentialsProviderTest {
private static final String SA_PRIVATE_KEY_ID = "d84a4fefcf50791d4a90f2d7af17469d6282df9d";
private static final String SA_PRIVATE_KEY_PKCS8 =
ServiceAccountCredentialsTest.PRIVATE_KEY_PKCS8;
private static final String GDCH_SA_FORMAT_VERSION = GdchCredentials.SUPPORTED_FORMAT_VERSION;
private static final String GDCH_SA_PROJECT_ID = "gdch-service-account-project-id";
private static final String GDCH_SA_PRIVATE_KEY_ID = "d84a4fefcf50791d4a90f2d7af17469d6282df9d";
private static final String GDCH_SA_PRIVATE_KEY_PKC8 = GdchCredentialsTest.PRIVATE_KEY_PKCS8;
private static final String GDCH_SA_SERVICE_IDENTITY_NAME =
"gdch-service-account-service-identity-name";
private static final URI GDCH_SA_TOKEN_SERVER_URI =
URI.create("https://service-identity.domain/authenticate");
private static final String GDCH_SA_CA_CERT_FILE_NAME = "cert.pem";
private static final String GDCH_SA_CA_CERT_PATH =
GdchCredentialsTest.class.getClassLoader().getResource(GDCH_SA_CA_CERT_FILE_NAME).getPath();
private static final URI GDCH_SA_API_AUDIENCE = URI.create("https://gdch-api-audience");
private static final Collection<String> SCOPES = Collections.singletonList("dummy.scope");
private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo");
private static final String QUOTA_PROJECT = "sample-quota-project-id";
Expand Down Expand Up @@ -392,6 +404,49 @@ public void getDefaultCredentials_envUser_providesToken() throws IOException {
}

@Test
public void getDefaultCredentials_GdchServiceAccount() throws IOException {
MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory();
InputStream gdchServiceAccountStream =
GdchCredentialsTest.writeGdchServiceAccountStream(
GDCH_SA_FORMAT_VERSION,
GDCH_SA_PROJECT_ID,
GDCH_SA_PRIVATE_KEY_ID,
GDCH_SA_PRIVATE_KEY_PKC8,
GDCH_SA_SERVICE_IDENTITY_NAME,
GDCH_SA_CA_CERT_PATH,
GDCH_SA_TOKEN_SERVER_URI);
TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider();
String gdchServiceAccountPath = tempFilePath("gdch_service_account.json");
testProvider.addFile(gdchServiceAccountPath, gdchServiceAccountStream);
testProvider.setEnv(DefaultCredentialsProvider.CREDENTIAL_ENV_VAR, gdchServiceAccountPath);

GoogleCredentials defaultCredentials = testProvider.getDefaultCredentials(transportFactory);

assertNotNull(defaultCredentials);
assertTrue(defaultCredentials instanceof GdchCredentials);
assertEquals(GDCH_SA_PROJECT_ID, ((GdchCredentials) defaultCredentials).getProjectId());
assertEquals(
GDCH_SA_SERVICE_IDENTITY_NAME,
((GdchCredentials) defaultCredentials).getServiceIdentityName());
assertEquals(
GDCH_SA_TOKEN_SERVER_URI, ((GdchCredentials) defaultCredentials).getTokenServerUri());
assertEquals(GDCH_SA_CA_CERT_PATH, ((GdchCredentials) defaultCredentials).getCaCertPath());
assertNull(((GdchCredentials) defaultCredentials).getApiAudience());

defaultCredentials =
((GdchCredentials) defaultCredentials).createWithGdchAudience(GDCH_SA_API_AUDIENCE);
assertNotNull(defaultCredentials);
assertTrue(defaultCredentials instanceof GdchCredentials);
assertEquals(GDCH_SA_PROJECT_ID, ((GdchCredentials) defaultCredentials).getProjectId());
assertEquals(
GDCH_SA_SERVICE_IDENTITY_NAME,
((GdchCredentials) defaultCredentials).getServiceIdentityName());
assertEquals(
GDCH_SA_TOKEN_SERVER_URI, ((GdchCredentials) defaultCredentials).getTokenServerUri());
assertEquals(GDCH_SA_CA_CERT_PATH, ((GdchCredentials) defaultCredentials).getCaCertPath());
assertNotNull(((GdchCredentials) defaultCredentials).getApiAudience());
}

public void getDefaultCredentials_quota_project() throws IOException {
InputStream userStream =
UserCredentialsTest.writeUserStream(
Expand Down
Expand Up @@ -212,7 +212,7 @@ private static GoogleCredentials getServiceAccountSourceCredentials(boolean canR
ServiceAccountCredentials sourceCredentials =
ServiceAccountCredentials.newBuilder()
.setClientEmail(email)
.setPrivateKey(ServiceAccountCredentials.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8))
.setPrivateKey(OAuth2Utils.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8))
.setPrivateKeyId("privateKeyId")
.setProjectId("projectId")
.setHttpTransportFactory(transportFactory)
Expand Down

0 comments on commit cfafb2d

Please sign in to comment.