Skip to content

Commit adc2ff3

Browse files
authored
feat: ImpersonatedCredentials to support universe domain for idtoken and signblob (#1566)
follow up to #1528. idtoken and sign flow are tested E2E according to TPC test guide for sa-to-sa impersonation.
1 parent fc20d9c commit adc2ff3

File tree

10 files changed

+212
-85
lines changed

10 files changed

+212
-85
lines changed

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,6 @@ public class ComputeEngineCredentials extends GoogleCredentials
9595

9696
static final String DEFAULT_METADATA_SERVER_URL = "http://metadata.google.internal";
9797

98-
static final String SIGN_BLOB_URL_FORMAT =
99-
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob";
100-
10198
// Note: the explicit `timeout` and `tries` below is a workaround. The underlying
10299
// issue is that resolving an unknown host on some networks will take
103100
// 20-30 seconds; making this timeout short fixes the issue, but
@@ -675,11 +672,18 @@ public byte[] sign(byte[] toSign) {
675672
try {
676673
String account = getAccount();
677674
return IamUtils.sign(
678-
account, this, transportFactory.create(), toSign, Collections.<String, Object>emptyMap());
675+
account,
676+
this,
677+
this.getUniverseDomain(),
678+
transportFactory.create(),
679+
toSign,
680+
Collections.<String, Object>emptyMap());
679681
} catch (SigningException ex) {
680682
throw ex;
681683
} catch (RuntimeException ex) {
682684
throw new SigningException("Signing failed", ex);
685+
} catch (IOException ex) {
686+
throw new SigningException("Failed to sign: Error obtaining universe domain", ex);
683687
}
684688
}
685689

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

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,14 @@
6262
* features like signing.
6363
*/
6464
class IamUtils {
65-
private static final String SIGN_BLOB_URL_FORMAT =
66-
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob";
67-
private static final String ID_TOKEN_URL_FORMAT =
68-
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateIdToken";
65+
66+
// IAM credentials endpoints are to be formatted with universe domain and client email.
67+
static final String IAM_ID_TOKEN_ENDPOINT_FORMAT =
68+
"https://iamcredentials.%s/v1/projects/-/serviceAccounts/%s:generateIdToken";
69+
static final String IAM_ACCESS_TOKEN_ENDPOINT_FORMAT =
70+
"https://iamcredentials.%s/v1/projects/-/serviceAccounts/%s:generateAccessToken";
71+
static final String IAM_SIGN_BLOB_ENDPOINT_FORMAT =
72+
"https://iamcredentials.%s/v1/projects/-/serviceAccounts/%s:signBlob";
6973
private static final String PARSE_ERROR_MESSAGE = "Error parsing error message response. ";
7074
private static final String PARSE_ERROR_SIGNATURE = "Error parsing signature response. ";
7175

@@ -88,6 +92,7 @@ class IamUtils {
8892
static byte[] sign(
8993
String serviceAccountEmail,
9094
Credentials credentials,
95+
String universeDomain,
9196
HttpTransport transport,
9297
byte[] toSign,
9398
Map<String, ?> additionalFields) {
@@ -97,7 +102,12 @@ static byte[] sign(
97102
String signature;
98103
try {
99104
signature =
100-
getSignature(serviceAccountEmail, base64.encode(toSign), additionalFields, factory);
105+
getSignature(
106+
serviceAccountEmail,
107+
universeDomain,
108+
base64.encode(toSign),
109+
additionalFields,
110+
factory);
101111
} catch (IOException ex) {
102112
throw new ServiceAccountSigner.SigningException("Failed to sign the provided bytes", ex);
103113
}
@@ -106,11 +116,13 @@ static byte[] sign(
106116

107117
private static String getSignature(
108118
String serviceAccountEmail,
119+
String universeDomain,
109120
String bytes,
110121
Map<String, ?> additionalFields,
111122
HttpRequestFactory factory)
112123
throws IOException {
113-
String signBlobUrl = String.format(SIGN_BLOB_URL_FORMAT, serviceAccountEmail);
124+
String signBlobUrl =
125+
String.format(IAM_SIGN_BLOB_ENDPOINT_FORMAT, universeDomain, serviceAccountEmail);
114126
GenericUrl genericUrl = new GenericUrl(signBlobUrl);
115127

116128
GenericData signRequest = new GenericData();
@@ -193,10 +205,12 @@ static IdToken getIdToken(
193205
String targetAudience,
194206
boolean includeEmail,
195207
Map<String, ?> additionalFields,
196-
CredentialTypeForMetrics credentialTypeForMetrics)
208+
CredentialTypeForMetrics credentialTypeForMetrics,
209+
String universeDomain)
197210
throws IOException {
198211

199-
String idTokenUrl = String.format(ID_TOKEN_URL_FORMAT, serviceAccountEmail);
212+
String idTokenUrl =
213+
String.format(IAM_ID_TOKEN_ENDPOINT_FORMAT, universeDomain, serviceAccountEmail);
200214
GenericUrl genericUrl = new GenericUrl(idTokenUrl);
201215

202216
GenericData idTokenRequest = new GenericData();

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -345,12 +345,19 @@ public void setTransportFactory(HttpTransportFactory httpTransportFactory) {
345345
*/
346346
@Override
347347
public byte[] sign(byte[] toSign) {
348-
return IamUtils.sign(
349-
getAccount(),
350-
sourceCredentials,
351-
transportFactory.create(),
352-
toSign,
353-
ImmutableMap.of("delegates", this.delegates));
348+
try {
349+
return IamUtils.sign(
350+
getAccount(),
351+
sourceCredentials,
352+
getUniverseDomain(),
353+
transportFactory.create(),
354+
toSign,
355+
ImmutableMap.of("delegates", this.delegates));
356+
} catch (IOException ex) {
357+
// Throwing an IOException would be a breaking change, so wrap it here.
358+
// This should not happen for this credential type.
359+
throw new SigningException("Failed to sign: Error obtaining universe domain", ex);
360+
}
354361
}
355362

356363
/**
@@ -525,7 +532,7 @@ public AccessToken refreshAccessToken() throws IOException {
525532
this.iamEndpointOverride != null
526533
? this.iamEndpointOverride
527534
: String.format(
528-
OAuth2Utils.IAM_ACCESS_TOKEN_ENDPOINT_FORMAT,
535+
IamUtils.IAM_ACCESS_TOKEN_ENDPOINT_FORMAT,
529536
getUniverseDomain(),
530537
this.targetPrincipal);
531538

@@ -593,7 +600,8 @@ public IdToken idTokenWithAudience(String targetAudience, List<IdTokenProvider.O
593600
targetAudience,
594601
includeEmail,
595602
ImmutableMap.of("delegates", this.delegates),
596-
getMetricsCredentialType());
603+
getMetricsCredentialType(),
604+
getUniverseDomain());
597605
}
598606

599607
@Override

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

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,6 @@ class OAuth2Utils {
7777
static final String TOKEN_TYPE_TOKEN_EXCHANGE = "urn:ietf:params:oauth:token-type:token-exchange";
7878
static final String GRANT_TYPE_JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer";
7979

80-
// generateIdToken endpoint is to be formatted with universe domain and client email
81-
static final String IAM_ID_TOKEN_ENDPOINT_FORMAT =
82-
"https://iamcredentials.%s/v1/projects/-/serviceAccounts/%s:generateIdToken";
83-
84-
static final String IAM_ACCESS_TOKEN_ENDPOINT_FORMAT =
85-
"https://iamcredentials.%s/v1/projects/-/serviceAccounts/%s:generateAccessToken";
86-
static final String SIGN_BLOB_ENDPOINT_FORMAT =
87-
"https://iamcredentials.%s/v1/projects/-/serviceAccounts/%s:signBlob";
88-
8980
static final URI TOKEN_SERVER_URI = URI.create("https://oauth2.googleapis.com/token");
9081

9182
static final URI TOKEN_REVOKE_URI = URI.create("https://oauth2.googleapis.com/revoke");

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -636,8 +636,7 @@ private IdToken getIdTokenIamEndpoint(String targetAudience) throws IOException
636636
// `getUniverseDomain()` throws an IOException that would need to be caught
637637
URI iamIdTokenUri =
638638
URI.create(
639-
String.format(
640-
OAuth2Utils.IAM_ID_TOKEN_ENDPOINT_FORMAT, getUniverseDomain(), clientEmail));
639+
String.format(IamUtils.IAM_ID_TOKEN_ENDPOINT_FORMAT, getUniverseDomain(), clientEmail));
641640
HttpRequest request = buildIdTokenRequest(iamIdTokenUri, transportFactory, content);
642641
// Use the Access Token from the SSJWT to request the ID Token from IAM Endpoint
643642
request.setHeaders(new HttpHeaders().set(AuthHttpConstants.AUTHORIZATION, accessToken));

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

Lines changed: 54 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
import java.util.stream.Collectors;
6767
import java.util.stream.IntStream;
6868
import java.util.stream.Stream;
69+
import org.junit.Assert;
6970
import org.junit.Test;
7071
import org.junit.runner.RunWith;
7172
import org.junit.runners.JUnit4;
@@ -645,7 +646,7 @@ public LowLevelHttpResponse execute() throws IOException {
645646
}
646647

647648
@Test
648-
public void sign_sameAs() throws IOException {
649+
public void sign_sameAs() {
649650
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
650651
String defaultAccountEmail = "[email protected]";
651652
byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD};
@@ -659,21 +660,36 @@ public void sign_sameAs() throws IOException {
659660
}
660661

661662
@Test
662-
public void sign_getAccountFails() throws IOException {
663+
public void sign_getUniverseException() {
664+
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
665+
666+
String defaultAccountEmail = "[email protected]";
667+
transportFactory.transport.setServiceAccountEmail(defaultAccountEmail);
668+
ComputeEngineCredentials credentials =
669+
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
670+
671+
transportFactory.transport.setRequestStatusCode(501);
672+
Assert.assertThrows(IOException.class, credentials::getUniverseDomain);
673+
674+
byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD};
675+
SigningException signingException =
676+
Assert.assertThrows(SigningException.class, () -> credentials.sign(expectedSignature));
677+
assertEquals("Failed to sign: Error obtaining universe domain", signingException.getMessage());
678+
}
679+
680+
@Test
681+
public void sign_getAccountFails() {
663682
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
664683
byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD};
665684

666685
transportFactory.transport.setSignature(expectedSignature);
667686
ComputeEngineCredentials credentials =
668687
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
669688

670-
try {
671-
credentials.sign(expectedSignature);
672-
fail("Should not be able to use credential without exception.");
673-
} catch (SigningException ex) {
674-
assertNotNull(ex.getMessage());
675-
assertNotNull(ex.getCause());
676-
}
689+
SigningException exception =
690+
Assert.assertThrows(SigningException.class, () -> credentials.sign(expectedSignature));
691+
assertNotNull(exception.getMessage());
692+
assertNotNull(exception.getCause());
677693
}
678694

679695
@Test
@@ -705,15 +721,13 @@ public LowLevelHttpResponse execute() throws IOException {
705721
ComputeEngineCredentials credentials =
706722
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
707723

708-
try {
709-
byte[] bytes = {0xD, 0xE, 0xA, 0xD};
710-
credentials.sign(bytes);
711-
fail("Signing should have failed");
712-
} catch (SigningException e) {
713-
assertEquals("Failed to sign the provided bytes", e.getMessage());
714-
assertNotNull(e.getCause());
715-
assertTrue(e.getCause().getMessage().contains("403"));
716-
}
724+
byte[] bytes = {0xD, 0xE, 0xA, 0xD};
725+
726+
SigningException exception =
727+
Assert.assertThrows(SigningException.class, () -> credentials.sign(bytes));
728+
assertEquals("Failed to sign the provided bytes", exception.getMessage());
729+
assertNotNull(exception.getCause());
730+
assertTrue(exception.getCause().getMessage().contains("403"));
717731
}
718732

719733
@Test
@@ -745,15 +759,13 @@ public LowLevelHttpResponse execute() throws IOException {
745759
ComputeEngineCredentials credentials =
746760
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
747761

748-
try {
749-
byte[] bytes = {0xD, 0xE, 0xA, 0xD};
750-
credentials.sign(bytes);
751-
fail("Signing should have failed");
752-
} catch (SigningException e) {
753-
assertEquals("Failed to sign the provided bytes", e.getMessage());
754-
assertNotNull(e.getCause());
755-
assertTrue(e.getCause().getMessage().contains("500"));
756-
}
762+
byte[] bytes = {0xD, 0xE, 0xA, 0xD};
763+
764+
SigningException exception =
765+
Assert.assertThrows(SigningException.class, () -> credentials.sign(bytes));
766+
assertEquals("Failed to sign the provided bytes", exception.getMessage());
767+
assertNotNull(exception.getCause());
768+
assertTrue(exception.getCause().getMessage().contains("500"));
757769
}
758770

759771
@Test
@@ -778,14 +790,11 @@ public LowLevelHttpResponse execute() throws IOException {
778790
ComputeEngineCredentials credentials =
779791
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
780792

781-
try {
782-
credentials.refreshAccessToken();
783-
fail("Should have failed");
784-
} catch (IOException e) {
785-
assertTrue(e.getCause().getMessage().contains("503"));
786-
assertTrue(e instanceof GoogleAuthException);
787-
assertTrue(((GoogleAuthException) e).isRetryable());
788-
}
793+
IOException exception =
794+
Assert.assertThrows(IOException.class, () -> credentials.refreshAccessToken());
795+
assertTrue(exception.getCause().getMessage().contains("503"));
796+
assertTrue(exception instanceof GoogleAuthException);
797+
assertTrue(((GoogleAuthException) exception).isRetryable());
789798
}
790799

791800
@Test
@@ -818,12 +827,9 @@ public LowLevelHttpResponse execute() throws IOException {
818827
ComputeEngineCredentials credentials =
819828
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
820829

821-
try {
822-
credentials.refreshAccessToken();
823-
fail("Should have failed");
824-
} catch (IOException e) {
825-
assertFalse(e instanceof GoogleAuthException);
826-
}
830+
IOException exception =
831+
Assert.assertThrows(IOException.class, () -> credentials.refreshAccessToken());
832+
assertFalse(exception instanceof GoogleAuthException);
827833
}
828834
}
829835

@@ -993,15 +999,13 @@ public LowLevelHttpResponse execute() throws IOException {
993999
ComputeEngineCredentials credentials =
9941000
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
9951001

996-
try {
997-
byte[] bytes = {0xD, 0xE, 0xA, 0xD};
998-
credentials.sign(bytes);
999-
fail("Signing should have failed");
1000-
} catch (SigningException e) {
1001-
assertEquals("Failed to sign the provided bytes", e.getMessage());
1002-
assertNotNull(e.getCause());
1003-
assertTrue(e.getCause().getMessage().contains("Empty content"));
1004-
}
1002+
byte[] bytes = {0xD, 0xE, 0xA, 0xD};
1003+
1004+
SigningException exception =
1005+
Assert.assertThrows(SigningException.class, () -> credentials.sign(bytes));
1006+
assertEquals("Failed to sign the provided bytes", exception.getMessage());
1007+
assertNotNull(exception.getCause());
1008+
assertTrue(exception.getCause().getMessage().contains("Empty content"));
10051009
}
10061010

10071011
@Test

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import static org.junit.Assert.assertTrue;
3737

3838
import com.google.api.client.http.HttpStatusCodes;
39+
import com.google.auth.Credentials;
3940
import com.google.auth.ServiceAccountSigner;
4041
import com.google.common.collect.ImmutableMap;
4142
import java.io.IOException;
@@ -60,6 +61,7 @@ public void setup() throws IOException {
6061
// token
6162
credentials = Mockito.mock(ServiceAccountCredentials.class);
6263
Mockito.when(credentials.getRequestMetadata(Mockito.any())).thenReturn(ImmutableMap.of());
64+
Mockito.when(credentials.getUniverseDomain()).thenReturn("googleapis.com");
6365
}
6466

6567
@Test
@@ -76,6 +78,7 @@ public void sign_success_noRetry() {
7678
IamUtils.sign(
7779
CLIENT_EMAIL,
7880
credentials,
81+
Credentials.GOOGLE_DEFAULT_UNIVERSE,
7982
transportFactory.getTransport(),
8083
expectedSignature,
8184
ImmutableMap.of());
@@ -107,6 +110,7 @@ public void sign_retryTwoTimes_success() {
107110
IamUtils.sign(
108111
CLIENT_EMAIL,
109112
credentials,
113+
Credentials.GOOGLE_DEFAULT_UNIVERSE,
110114
transportFactory.getTransport(),
111115
expectedSignature,
112116
ImmutableMap.of());
@@ -143,6 +147,7 @@ public void sign_retryThreeTimes_success() {
143147
IamUtils.sign(
144148
CLIENT_EMAIL,
145149
credentials,
150+
Credentials.GOOGLE_DEFAULT_UNIVERSE,
146151
transportFactory.getTransport(),
147152
expectedSignature,
148153
ImmutableMap.of());
@@ -185,6 +190,7 @@ public void sign_retryThreeTimes_exception() {
185190
IamUtils.sign(
186191
CLIENT_EMAIL,
187192
credentials,
193+
Credentials.GOOGLE_DEFAULT_UNIVERSE,
188194
transportFactory.getTransport(),
189195
expectedSignature,
190196
ImmutableMap.of()));
@@ -220,6 +226,7 @@ public void sign_4xxError_noRetry_exception() {
220226
IamUtils.sign(
221227
CLIENT_EMAIL,
222228
credentials,
229+
Credentials.GOOGLE_DEFAULT_UNIVERSE,
223230
transportFactory.getTransport(),
224231
expectedSignature,
225232
ImmutableMap.of()));

0 commit comments

Comments
 (0)