Skip to content

Commit cfafb2d

Browse files
poancTimur Sadykov
andauthored
feat: Add GDCH support (#1087)
* 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>
1 parent 6f32ac3 commit cfafb2d

15 files changed

+1834
-100
lines changed

oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java

Lines changed: 528 additions & 0 deletions
Large diffs are not rendered by default.

oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public class GoogleCredentials extends OAuth2Credentials implements QuotaProject
5656
static final String QUOTA_PROJECT_ID_HEADER_KEY = "x-goog-user-project";
5757
static final String USER_FILE_TYPE = "authorized_user";
5858
static final String SERVICE_ACCOUNT_FILE_TYPE = "service_account";
59+
static final String GDCH_SERVICE_ACCOUNT_FILE_TYPE = "gdch_service_account";
5960

6061
protected final String quotaProjectId;
6162

@@ -174,6 +175,9 @@ public static GoogleCredentials fromStream(
174175
if (SERVICE_ACCOUNT_FILE_TYPE.equals(fileType)) {
175176
return ServiceAccountCredentials.fromJson(fileContents, transportFactory);
176177
}
178+
if (GDCH_SERVICE_ACCOUNT_FILE_TYPE.equals(fileType)) {
179+
return GdchCredentials.fromJson(fileContents);
180+
}
177181
if (ExternalAccountCredentials.EXTERNAL_ACCOUNT_FILE_TYPE.equals(fileType)) {
178182
return ExternalAccountCredentials.fromJson(fileContents, transportFactory);
179183
}

oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
import com.google.api.client.json.JsonFactory;
3939
import com.google.api.client.json.JsonObjectParser;
4040
import com.google.api.client.json.gson.GsonFactory;
41+
import com.google.api.client.util.PemReader;
42+
import com.google.api.client.util.PemReader.Section;
43+
import com.google.api.client.util.SecurityUtils;
4144
import com.google.auth.http.AuthHttpConstants;
4245
import com.google.auth.http.HttpTransportFactory;
4346
import com.google.common.io.ByteStreams;
@@ -47,9 +50,16 @@
4750
import java.io.IOException;
4851
import java.io.InputStream;
4952
import java.io.OutputStream;
53+
import java.io.Reader;
54+
import java.io.StringReader;
5055
import java.math.BigDecimal;
5156
import java.net.URI;
5257
import java.nio.charset.StandardCharsets;
58+
import java.security.KeyFactory;
59+
import java.security.NoSuchAlgorithmException;
60+
import java.security.PrivateKey;
61+
import java.security.spec.InvalidKeySpecException;
62+
import java.security.spec.PKCS8EncodedKeySpec;
5363
import java.util.Arrays;
5464
import java.util.Collection;
5565
import java.util.HashSet;
@@ -61,6 +71,8 @@ class OAuth2Utils {
6171
static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
6272

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

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

217+
/** Helper to convert from a PKCS#8 String to an RSA private key */
218+
static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8) throws IOException {
219+
Reader reader = new StringReader(privateKeyPkcs8);
220+
Section section = PemReader.readFirstSectionAndClose(reader, "PRIVATE KEY");
221+
if (section == null) {
222+
throw new IOException("Invalid PKCS#8 data.");
223+
}
224+
byte[] bytes = section.getBase64DecodedBytes();
225+
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
226+
Exception unexpectedException;
227+
try {
228+
KeyFactory keyFactory = SecurityUtils.getRsaKeyFactory();
229+
return keyFactory.generatePrivate(keySpec);
230+
} catch (NoSuchAlgorithmException | InvalidKeySpecException exception) {
231+
unexpectedException = exception;
232+
}
233+
throw new IOException("Unexpected exception reading PKCS#8 data", unexpectedException);
234+
}
235+
205236
private OAuth2Utils() {}
206237
}

oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,7 @@
4949
import com.google.api.client.util.ExponentialBackOff;
5050
import com.google.api.client.util.GenericData;
5151
import com.google.api.client.util.Joiner;
52-
import com.google.api.client.util.PemReader;
53-
import com.google.api.client.util.PemReader.Section;
5452
import com.google.api.client.util.Preconditions;
55-
import com.google.api.client.util.SecurityUtils;
5653
import com.google.auth.RequestMetadataCallback;
5754
import com.google.auth.ServiceAccountSigner;
5855
import com.google.auth.http.HttpTransportFactory;
@@ -62,20 +59,15 @@
6259
import java.io.IOException;
6360
import java.io.InputStream;
6461
import java.io.ObjectInputStream;
65-
import java.io.Reader;
66-
import java.io.StringReader;
6762
import java.net.URI;
6863
import java.net.URISyntaxException;
6964
import java.nio.charset.StandardCharsets;
7065
import java.security.GeneralSecurityException;
7166
import java.security.InvalidKeyException;
72-
import java.security.KeyFactory;
7367
import java.security.NoSuchAlgorithmException;
7468
import java.security.PrivateKey;
7569
import java.security.Signature;
7670
import java.security.SignatureException;
77-
import java.security.spec.InvalidKeySpecException;
78-
import java.security.spec.PKCS8EncodedKeySpec;
7971
import java.util.Collection;
8072
import java.util.Collections;
8173
import java.util.Date;
@@ -437,31 +429,12 @@ public static ServiceAccountCredentials fromPkcs8(
437429
*/
438430
static ServiceAccountCredentials fromPkcs8(
439431
String privateKeyPkcs8, ServiceAccountCredentials.Builder builder) throws IOException {
440-
PrivateKey privateKey = privateKeyFromPkcs8(privateKeyPkcs8);
432+
PrivateKey privateKey = OAuth2Utils.privateKeyFromPkcs8(privateKeyPkcs8);
441433
builder.setPrivateKey(privateKey);
442434

443435
return new ServiceAccountCredentials(builder);
444436
}
445437

446-
/** Helper to convert from a PKCS#8 String to an RSA private key */
447-
static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8) throws IOException {
448-
Reader reader = new StringReader(privateKeyPkcs8);
449-
Section section = PemReader.readFirstSectionAndClose(reader, "PRIVATE KEY");
450-
if (section == null) {
451-
throw new IOException("Invalid PKCS#8 data.");
452-
}
453-
byte[] bytes = section.getBase64DecodedBytes();
454-
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
455-
Exception unexpectedException;
456-
try {
457-
KeyFactory keyFactory = SecurityUtils.getRsaKeyFactory();
458-
return keyFactory.generatePrivate(keySpec);
459-
} catch (NoSuchAlgorithmException | InvalidKeySpecException exception) {
460-
unexpectedException = exception;
461-
}
462-
throw new IOException("Unexpected exception reading PKCS#8 data", unexpectedException);
463-
}
464-
465438
/**
466439
* Returns credentials defined by a Service Account key file in JSON format from the Google
467440
* Developers Console.
@@ -1038,7 +1011,7 @@ public Builder setPrivateKey(PrivateKey privateKey) {
10381011
}
10391012

10401013
public Builder setPrivateKeyString(String privateKeyPkcs8) throws IOException {
1041-
this.privateKey = privateKeyFromPkcs8(privateKeyPkcs8);
1014+
this.privateKey = OAuth2Utils.privateKeyFromPkcs8(privateKeyPkcs8);
10421015
return this;
10431016
}
10441017

oauth2_http/java/com/google/auth/oauth2/ServiceAccountJwtAccessCredentials.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ static ServiceAccountJwtAccessCredentials fromPkcs8(
217217
URI defaultAudience,
218218
String quotaProjectId)
219219
throws IOException {
220-
PrivateKey privateKey = ServiceAccountCredentials.privateKeyFromPkcs8(privateKeyPkcs8);
220+
PrivateKey privateKey = OAuth2Utils.privateKeyFromPkcs8(privateKeyPkcs8);
221221
return new ServiceAccountJwtAccessCredentials(
222222
clientId, clientEmail, privateKey, privateKeyId, defaultAudience, quotaProjectId);
223223
}

oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,18 @@ public class DefaultCredentialsProviderTest {
8989
private static final String SA_PRIVATE_KEY_ID = "d84a4fefcf50791d4a90f2d7af17469d6282df9d";
9090
private static final String SA_PRIVATE_KEY_PKCS8 =
9191
ServiceAccountCredentialsTest.PRIVATE_KEY_PKCS8;
92+
private static final String GDCH_SA_FORMAT_VERSION = GdchCredentials.SUPPORTED_FORMAT_VERSION;
93+
private static final String GDCH_SA_PROJECT_ID = "gdch-service-account-project-id";
94+
private static final String GDCH_SA_PRIVATE_KEY_ID = "d84a4fefcf50791d4a90f2d7af17469d6282df9d";
95+
private static final String GDCH_SA_PRIVATE_KEY_PKC8 = GdchCredentialsTest.PRIVATE_KEY_PKCS8;
96+
private static final String GDCH_SA_SERVICE_IDENTITY_NAME =
97+
"gdch-service-account-service-identity-name";
98+
private static final URI GDCH_SA_TOKEN_SERVER_URI =
99+
URI.create("https://service-identity.domain/authenticate");
100+
private static final String GDCH_SA_CA_CERT_FILE_NAME = "cert.pem";
101+
private static final String GDCH_SA_CA_CERT_PATH =
102+
GdchCredentialsTest.class.getClassLoader().getResource(GDCH_SA_CA_CERT_FILE_NAME).getPath();
103+
private static final URI GDCH_SA_API_AUDIENCE = URI.create("https://gdch-api-audience");
92104
private static final Collection<String> SCOPES = Collections.singletonList("dummy.scope");
93105
private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo");
94106
private static final String QUOTA_PROJECT = "sample-quota-project-id";
@@ -392,6 +404,49 @@ public void getDefaultCredentials_envUser_providesToken() throws IOException {
392404
}
393405

394406
@Test
407+
public void getDefaultCredentials_GdchServiceAccount() throws IOException {
408+
MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory();
409+
InputStream gdchServiceAccountStream =
410+
GdchCredentialsTest.writeGdchServiceAccountStream(
411+
GDCH_SA_FORMAT_VERSION,
412+
GDCH_SA_PROJECT_ID,
413+
GDCH_SA_PRIVATE_KEY_ID,
414+
GDCH_SA_PRIVATE_KEY_PKC8,
415+
GDCH_SA_SERVICE_IDENTITY_NAME,
416+
GDCH_SA_CA_CERT_PATH,
417+
GDCH_SA_TOKEN_SERVER_URI);
418+
TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider();
419+
String gdchServiceAccountPath = tempFilePath("gdch_service_account.json");
420+
testProvider.addFile(gdchServiceAccountPath, gdchServiceAccountStream);
421+
testProvider.setEnv(DefaultCredentialsProvider.CREDENTIAL_ENV_VAR, gdchServiceAccountPath);
422+
423+
GoogleCredentials defaultCredentials = testProvider.getDefaultCredentials(transportFactory);
424+
425+
assertNotNull(defaultCredentials);
426+
assertTrue(defaultCredentials instanceof GdchCredentials);
427+
assertEquals(GDCH_SA_PROJECT_ID, ((GdchCredentials) defaultCredentials).getProjectId());
428+
assertEquals(
429+
GDCH_SA_SERVICE_IDENTITY_NAME,
430+
((GdchCredentials) defaultCredentials).getServiceIdentityName());
431+
assertEquals(
432+
GDCH_SA_TOKEN_SERVER_URI, ((GdchCredentials) defaultCredentials).getTokenServerUri());
433+
assertEquals(GDCH_SA_CA_CERT_PATH, ((GdchCredentials) defaultCredentials).getCaCertPath());
434+
assertNull(((GdchCredentials) defaultCredentials).getApiAudience());
435+
436+
defaultCredentials =
437+
((GdchCredentials) defaultCredentials).createWithGdchAudience(GDCH_SA_API_AUDIENCE);
438+
assertNotNull(defaultCredentials);
439+
assertTrue(defaultCredentials instanceof GdchCredentials);
440+
assertEquals(GDCH_SA_PROJECT_ID, ((GdchCredentials) defaultCredentials).getProjectId());
441+
assertEquals(
442+
GDCH_SA_SERVICE_IDENTITY_NAME,
443+
((GdchCredentials) defaultCredentials).getServiceIdentityName());
444+
assertEquals(
445+
GDCH_SA_TOKEN_SERVER_URI, ((GdchCredentials) defaultCredentials).getTokenServerUri());
446+
assertEquals(GDCH_SA_CA_CERT_PATH, ((GdchCredentials) defaultCredentials).getCaCertPath());
447+
assertNotNull(((GdchCredentials) defaultCredentials).getApiAudience());
448+
}
449+
395450
public void getDefaultCredentials_quota_project() throws IOException {
396451
InputStream userStream =
397452
UserCredentialsTest.writeUserStream(

oauth2_http/javatests/com/google/auth/oauth2/DownscopedCredentialsTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ private static GoogleCredentials getServiceAccountSourceCredentials(boolean canR
212212
ServiceAccountCredentials sourceCredentials =
213213
ServiceAccountCredentials.newBuilder()
214214
.setClientEmail(email)
215-
.setPrivateKey(ServiceAccountCredentials.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8))
215+
.setPrivateKey(OAuth2Utils.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8))
216216
.setPrivateKeyId("privateKeyId")
217217
.setProjectId("projectId")
218218
.setHttpTransportFactory(transportFactory)

0 commit comments

Comments
 (0)