diff --git a/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java index 34e7f11ff..75edf184c 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java @@ -95,9 +95,6 @@ public class ComputeEngineCredentials extends GoogleCredentials static final String DEFAULT_METADATA_SERVER_URL = "http://metadata.google.internal"; - static final String SIGN_BLOB_URL_FORMAT = - "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob"; - // Note: the explicit `timeout` and `tries` below is a workaround. The underlying // issue is that resolving an unknown host on some networks will take // 20-30 seconds; making this timeout short fixes the issue, but @@ -675,11 +672,18 @@ public byte[] sign(byte[] toSign) { try { String account = getAccount(); return IamUtils.sign( - account, this, transportFactory.create(), toSign, Collections.emptyMap()); + account, + this, + this.getUniverseDomain(), + transportFactory.create(), + toSign, + Collections.emptyMap()); } catch (SigningException ex) { throw ex; } catch (RuntimeException ex) { throw new SigningException("Signing failed", ex); + } catch (IOException ex) { + throw new SigningException("Failed to sign: Error obtaining universe domain", ex); } } diff --git a/oauth2_http/java/com/google/auth/oauth2/IamUtils.java b/oauth2_http/java/com/google/auth/oauth2/IamUtils.java index 4a2a00870..571d7f668 100644 --- a/oauth2_http/java/com/google/auth/oauth2/IamUtils.java +++ b/oauth2_http/java/com/google/auth/oauth2/IamUtils.java @@ -62,10 +62,14 @@ * features like signing. */ class IamUtils { - private static final String SIGN_BLOB_URL_FORMAT = - "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob"; - private static final String ID_TOKEN_URL_FORMAT = - "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateIdToken"; + + // IAM credentials endpoints are to be formatted with universe domain and client email. + static final String IAM_ID_TOKEN_ENDPOINT_FORMAT = + "https://iamcredentials.%s/v1/projects/-/serviceAccounts/%s:generateIdToken"; + static final String IAM_ACCESS_TOKEN_ENDPOINT_FORMAT = + "https://iamcredentials.%s/v1/projects/-/serviceAccounts/%s:generateAccessToken"; + static final String IAM_SIGN_BLOB_ENDPOINT_FORMAT = + "https://iamcredentials.%s/v1/projects/-/serviceAccounts/%s:signBlob"; private static final String PARSE_ERROR_MESSAGE = "Error parsing error message response. "; private static final String PARSE_ERROR_SIGNATURE = "Error parsing signature response. "; @@ -88,6 +92,7 @@ class IamUtils { static byte[] sign( String serviceAccountEmail, Credentials credentials, + String universeDomain, HttpTransport transport, byte[] toSign, Map additionalFields) { @@ -97,7 +102,12 @@ static byte[] sign( String signature; try { signature = - getSignature(serviceAccountEmail, base64.encode(toSign), additionalFields, factory); + getSignature( + serviceAccountEmail, + universeDomain, + base64.encode(toSign), + additionalFields, + factory); } catch (IOException ex) { throw new ServiceAccountSigner.SigningException("Failed to sign the provided bytes", ex); } @@ -106,11 +116,13 @@ static byte[] sign( private static String getSignature( String serviceAccountEmail, + String universeDomain, String bytes, Map additionalFields, HttpRequestFactory factory) throws IOException { - String signBlobUrl = String.format(SIGN_BLOB_URL_FORMAT, serviceAccountEmail); + String signBlobUrl = + String.format(IAM_SIGN_BLOB_ENDPOINT_FORMAT, universeDomain, serviceAccountEmail); GenericUrl genericUrl = new GenericUrl(signBlobUrl); GenericData signRequest = new GenericData(); @@ -193,10 +205,12 @@ static IdToken getIdToken( String targetAudience, boolean includeEmail, Map additionalFields, - CredentialTypeForMetrics credentialTypeForMetrics) + CredentialTypeForMetrics credentialTypeForMetrics, + String universeDomain) throws IOException { - String idTokenUrl = String.format(ID_TOKEN_URL_FORMAT, serviceAccountEmail); + String idTokenUrl = + String.format(IAM_ID_TOKEN_ENDPOINT_FORMAT, universeDomain, serviceAccountEmail); GenericUrl genericUrl = new GenericUrl(idTokenUrl); GenericData idTokenRequest = new GenericData(); diff --git a/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java index 8ce922538..e5ea4d923 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java @@ -345,12 +345,19 @@ public void setTransportFactory(HttpTransportFactory httpTransportFactory) { */ @Override public byte[] sign(byte[] toSign) { - return IamUtils.sign( - getAccount(), - sourceCredentials, - transportFactory.create(), - toSign, - ImmutableMap.of("delegates", this.delegates)); + try { + return IamUtils.sign( + getAccount(), + sourceCredentials, + getUniverseDomain(), + transportFactory.create(), + toSign, + ImmutableMap.of("delegates", this.delegates)); + } catch (IOException ex) { + // Throwing an IOException would be a breaking change, so wrap it here. + // This should not happen for this credential type. + throw new SigningException("Failed to sign: Error obtaining universe domain", ex); + } } /** @@ -525,7 +532,7 @@ public AccessToken refreshAccessToken() throws IOException { this.iamEndpointOverride != null ? this.iamEndpointOverride : String.format( - OAuth2Utils.IAM_ACCESS_TOKEN_ENDPOINT_FORMAT, + IamUtils.IAM_ACCESS_TOKEN_ENDPOINT_FORMAT, getUniverseDomain(), this.targetPrincipal); @@ -593,7 +600,8 @@ public IdToken idTokenWithAudience(String targetAudience, List credentials.sign(expectedSignature)); + assertEquals("Failed to sign: Error obtaining universe domain", signingException.getMessage()); + } + + @Test + public void sign_getAccountFails() { MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD}; @@ -667,13 +686,10 @@ public void sign_getAccountFails() throws IOException { ComputeEngineCredentials credentials = ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - try { - credentials.sign(expectedSignature); - fail("Should not be able to use credential without exception."); - } catch (SigningException ex) { - assertNotNull(ex.getMessage()); - assertNotNull(ex.getCause()); - } + SigningException exception = + Assert.assertThrows(SigningException.class, () -> credentials.sign(expectedSignature)); + assertNotNull(exception.getMessage()); + assertNotNull(exception.getCause()); } @Test @@ -705,15 +721,13 @@ public LowLevelHttpResponse execute() throws IOException { ComputeEngineCredentials credentials = ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - try { - byte[] bytes = {0xD, 0xE, 0xA, 0xD}; - credentials.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")); - } + byte[] bytes = {0xD, 0xE, 0xA, 0xD}; + + SigningException exception = + Assert.assertThrows(SigningException.class, () -> credentials.sign(bytes)); + assertEquals("Failed to sign the provided bytes", exception.getMessage()); + assertNotNull(exception.getCause()); + assertTrue(exception.getCause().getMessage().contains("403")); } @Test @@ -745,15 +759,13 @@ public LowLevelHttpResponse execute() throws IOException { ComputeEngineCredentials credentials = ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - try { - byte[] bytes = {0xD, 0xE, 0xA, 0xD}; - credentials.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")); - } + byte[] bytes = {0xD, 0xE, 0xA, 0xD}; + + SigningException exception = + Assert.assertThrows(SigningException.class, () -> credentials.sign(bytes)); + assertEquals("Failed to sign the provided bytes", exception.getMessage()); + assertNotNull(exception.getCause()); + assertTrue(exception.getCause().getMessage().contains("500")); } @Test @@ -778,14 +790,11 @@ public LowLevelHttpResponse execute() throws IOException { ComputeEngineCredentials credentials = ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - try { - credentials.refreshAccessToken(); - fail("Should have failed"); - } catch (IOException e) { - assertTrue(e.getCause().getMessage().contains("503")); - assertTrue(e instanceof GoogleAuthException); - assertTrue(((GoogleAuthException) e).isRetryable()); - } + IOException exception = + Assert.assertThrows(IOException.class, () -> credentials.refreshAccessToken()); + assertTrue(exception.getCause().getMessage().contains("503")); + assertTrue(exception instanceof GoogleAuthException); + assertTrue(((GoogleAuthException) exception).isRetryable()); } @Test @@ -818,12 +827,9 @@ public LowLevelHttpResponse execute() throws IOException { ComputeEngineCredentials credentials = ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - try { - credentials.refreshAccessToken(); - fail("Should have failed"); - } catch (IOException e) { - assertFalse(e instanceof GoogleAuthException); - } + IOException exception = + Assert.assertThrows(IOException.class, () -> credentials.refreshAccessToken()); + assertFalse(exception instanceof GoogleAuthException); } } @@ -993,15 +999,13 @@ public LowLevelHttpResponse execute() throws IOException { ComputeEngineCredentials credentials = ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - try { - byte[] bytes = {0xD, 0xE, 0xA, 0xD}; - credentials.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("Empty content")); - } + byte[] bytes = {0xD, 0xE, 0xA, 0xD}; + + SigningException exception = + Assert.assertThrows(SigningException.class, () -> credentials.sign(bytes)); + assertEquals("Failed to sign the provided bytes", exception.getMessage()); + assertNotNull(exception.getCause()); + assertTrue(exception.getCause().getMessage().contains("Empty content")); } @Test diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IamUtilsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IamUtilsTest.java index fd5061cc4..54a3753e6 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/IamUtilsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/IamUtilsTest.java @@ -36,6 +36,7 @@ import static org.junit.Assert.assertTrue; import com.google.api.client.http.HttpStatusCodes; +import com.google.auth.Credentials; import com.google.auth.ServiceAccountSigner; import com.google.common.collect.ImmutableMap; import java.io.IOException; @@ -60,6 +61,7 @@ public void setup() throws IOException { // token credentials = Mockito.mock(ServiceAccountCredentials.class); Mockito.when(credentials.getRequestMetadata(Mockito.any())).thenReturn(ImmutableMap.of()); + Mockito.when(credentials.getUniverseDomain()).thenReturn("googleapis.com"); } @Test @@ -76,6 +78,7 @@ public void sign_success_noRetry() { IamUtils.sign( CLIENT_EMAIL, credentials, + Credentials.GOOGLE_DEFAULT_UNIVERSE, transportFactory.getTransport(), expectedSignature, ImmutableMap.of()); @@ -107,6 +110,7 @@ public void sign_retryTwoTimes_success() { IamUtils.sign( CLIENT_EMAIL, credentials, + Credentials.GOOGLE_DEFAULT_UNIVERSE, transportFactory.getTransport(), expectedSignature, ImmutableMap.of()); @@ -143,6 +147,7 @@ public void sign_retryThreeTimes_success() { IamUtils.sign( CLIENT_EMAIL, credentials, + Credentials.GOOGLE_DEFAULT_UNIVERSE, transportFactory.getTransport(), expectedSignature, ImmutableMap.of()); @@ -185,6 +190,7 @@ public void sign_retryThreeTimes_exception() { IamUtils.sign( CLIENT_EMAIL, credentials, + Credentials.GOOGLE_DEFAULT_UNIVERSE, transportFactory.getTransport(), expectedSignature, ImmutableMap.of())); @@ -220,6 +226,7 @@ public void sign_4xxError_noRetry_exception() { IamUtils.sign( CLIENT_EMAIL, credentials, + Credentials.GOOGLE_DEFAULT_UNIVERSE, transportFactory.getTransport(), expectedSignature, ImmutableMap.of())); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java index dda81837b..2eca3c5be 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java @@ -40,6 +40,8 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import com.google.api.client.http.HttpStatusCodes; import com.google.api.client.json.GenericJson; @@ -133,12 +135,12 @@ public class ImpersonatedCredentialsTest extends BaseSerializationTest { + ":generateAccessToken"; public static final String DEFAULT_IMPERSONATION_URL = String.format( - OAuth2Utils.IAM_ACCESS_TOKEN_ENDPOINT_FORMAT, + IamUtils.IAM_ACCESS_TOKEN_ENDPOINT_FORMAT, DEFAULT_UNIVERSE_DOMAIN, IMPERSONATED_CLIENT_EMAIL); private static final String NONGDU_IMPERSONATION_URL = String.format( - OAuth2Utils.IAM_ACCESS_TOKEN_ENDPOINT_FORMAT, + IamUtils.IAM_ACCESS_TOKEN_ENDPOINT_FORMAT, TEST_UNIVERSE_DOMAIN, IMPERSONATED_CLIENT_EMAIL); public static final String IMPERSONATION_OVERRIDE_URL = @@ -908,6 +910,64 @@ public void sign_serverError_throws() { } } + @Test + public void sign_sameAs_nonGDU() { + + MockIAMCredentialsServiceTransportFactory transportFactory = + new MockIAMCredentialsServiceTransportFactory("test.com"); + transportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); + transportFactory.getTransport().setAccessToken(ACCESS_TOKEN); + transportFactory.getTransport().setExpireTime(getDefaultExpireTime()); + transportFactory.getTransport().addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, ""); + ServiceAccountCredentials sourceCredentialsNonGDU = + ((ServiceAccountCredentials) sourceCredentials) + .toBuilder() + .setUniverseDomain("test.com") + .setHttpTransportFactory(transportFactory) + .build(); + ImpersonatedCredentials targetCredentials = + ImpersonatedCredentials.create( + sourceCredentialsNonGDU, + IMPERSONATED_CLIENT_EMAIL, + null, + IMMUTABLE_SCOPES_LIST, + VALID_LIFETIME, + transportFactory); + + byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD}; + + transportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); + transportFactory.getTransport().setSignedBlob(expectedSignature); + + assertArrayEquals(expectedSignature, targetCredentials.sign(expectedSignature)); + } + + @Test + public void sign_universeDomainException() throws IOException { + // Currently, no credentials allowed as source credentials throws exception for + // getUniverseDomain(), mock this behavior for test only. ServiceAccountCredentials + // should not throw for getUniverseDomain() calls. + ServiceAccountCredentials sourceCredentialsMock = mock(ServiceAccountCredentials.class); + when(sourceCredentialsMock.getUniverseDomain()).thenThrow(IOException.class); + + MockIAMCredentialsServiceTransportFactory transportFactory = + new MockIAMCredentialsServiceTransportFactory(); + ImpersonatedCredentials targetCredentials = + ImpersonatedCredentials.create( + sourceCredentialsMock, + IMPERSONATED_CLIENT_EMAIL, + null, + IMMUTABLE_SCOPES_LIST, + VALID_LIFETIME, + transportFactory); + + byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD}; + + SigningException exception = + assertThrows(SigningException.class, () -> targetCredentials.sign(expectedSignature)); + assertEquals("Failed to sign: Error obtaining universe domain", exception.getMessage()); + } + @Test public void idTokenWithAudience_sameAs() throws IOException { mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); @@ -977,6 +1037,45 @@ public void idTokenWithAudience_withEmail() throws IOException { assertTrue(requestHeader.containsKey("authorization")); } + @Test + public void idTokenWithAudience_sameAs_nonGDU() throws IOException { + MockIAMCredentialsServiceTransportFactory transportFactory = + new MockIAMCredentialsServiceTransportFactory("test.com"); + transportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); + transportFactory.getTransport().setAccessToken(ACCESS_TOKEN); + transportFactory.getTransport().setExpireTime(getDefaultExpireTime()); + transportFactory.getTransport().addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, ""); + ServiceAccountCredentials sourceCredentialsNonGDU = + ((ServiceAccountCredentials) sourceCredentials) + .toBuilder() + .setUniverseDomain("test.com") + .setHttpTransportFactory(transportFactory) + .build(); + ImpersonatedCredentials targetCredentials = + ImpersonatedCredentials.create( + sourceCredentialsNonGDU, + IMPERSONATED_CLIENT_EMAIL, + null, + IMMUTABLE_SCOPES_LIST, + VALID_LIFETIME, + transportFactory); + + transportFactory.getTransport().setIdToken(STANDARD_ID_TOKEN); + + String targetAudience = "https://foo.bar"; + IdTokenCredentials tokenCredential = + IdTokenCredentials.newBuilder() + .setIdTokenProvider(targetCredentials) + .setTargetAudience(targetAudience) + .build(); + tokenCredential.refresh(); + assertEquals(STANDARD_ID_TOKEN, tokenCredential.getAccessToken().getTokenValue()); + assertEquals(STANDARD_ID_TOKEN, tokenCredential.getIdToken().getTokenValue()); + assertEquals( + targetAudience, + (String) tokenCredential.getIdToken().getJsonWebSignature().getPayload().getAudience()); + } + @Test public void idToken_withServerError() { mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockIAMCredentialsServiceTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockIAMCredentialsServiceTransport.java index bc969f1ad..cbd57d115 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/MockIAMCredentialsServiceTransport.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/MockIAMCredentialsServiceTransport.java @@ -31,7 +31,7 @@ package com.google.auth.oauth2; -import static com.google.auth.oauth2.OAuth2Utils.IAM_ID_TOKEN_ENDPOINT_FORMAT; +import static com.google.auth.oauth2.IamUtils.IAM_ID_TOKEN_ENDPOINT_FORMAT; import com.google.api.client.http.HttpStatusCodes; import com.google.api.client.http.LowLevelHttpRequest; diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java index 9f68220a9..d2c3259b9 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java @@ -325,7 +325,8 @@ protected boolean isGetServiceAccountsUrl(String url) { protected boolean isSignRequestUrl(String url) { return serviceAccountEmail != null && url.equals( - String.format(ComputeEngineCredentials.SIGN_BLOB_URL_FORMAT, serviceAccountEmail)); + String.format( + IamUtils.IAM_SIGN_BLOB_ENDPOINT_FORMAT, "googleapis.com", serviceAccountEmail)); } protected boolean isIdentityDocumentUrl(String url) {