From 7f0c1d31176f9e634fac3b2c6b06f880a51b5fa6 Mon Sep 17 00:00:00 2001 From: Min Zhu Date: Wed, 2 Oct 2024 11:04:34 -0400 Subject: [PATCH] feat: add metric headers (#1503) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit context: b/339259830 and [go/send-auth-metrics-java](http://goto.google.com/send-auth-metrics-java) Changes include: - expose `Credentials` type via `getMetricsCredentialType()`. Override this method for UserCredentials, ServiceAccountCredentials, ImpersonatedCredentials, and ComputeEngineCredentials. This is used in both token request and token usage flows. - add metric headers for each of the in-scope token requests. Below are examples of each request flow with added metrics: - User credentials request (at/id): “gl-java/19.0.1 auth/1.24.3 cred-type/u” - SA credentials, VM credentials or Impersonated credentials requests (at/id): “gl-java/19.0.1 auth/1.24.3 auth-request-type/at cred-type/sa” - MDS ping (This is used in ADC during the credential detection): “gl-java/19.0.1 auth/1.24.3 auth-request-type/mds” - What is not tracked: ComputeEngineCredentials getUniverseDomain and getAccount does not send metrics header; TPC flows does not send metrics headers. Related pr: adding for cred_type for token usage requests https://github.com/googleapis/sdk-platform-java/pull/3186 --- .../google/auth/CredentialTypeForMetrics.java | 64 +++++++++++++++++ .../java/com/google/auth/Credentials.java | 12 ++++ .../auth/oauth2/ComputeEngineCredentials.java | 35 +++++++-- .../java/com/google/auth/oauth2/IamUtils.java | 11 ++- .../auth/oauth2/ImpersonatedCredentials.java | 16 ++++- .../com/google/auth/oauth2/MetricsUtils.java | 49 +++++++++++++ .../oauth2/ServiceAccountCredentials.java | 36 +++++++--- .../google/auth/oauth2/UserCredentials.java | 12 ++++ .../oauth2/ComputeEngineCredentialsTest.java | 13 ++++ .../DefaultCredentialsProviderTest.java | 5 ++ .../oauth2/ImpersonatedCredentialsTest.java | 12 ++++ .../google/auth/oauth2/MetricsUtilsTest.java | 47 ++++++++++-- .../oauth2/MockMetadataServerTransport.java | 48 ++++++++----- .../oauth2/ServiceAccountCredentialsTest.java | 22 ++++++ .../com/google/auth/oauth2/TestUtils.java | 72 +++++++++++++++++++ .../auth/oauth2/UserCredentialsTest.java | 11 +++ 16 files changed, 422 insertions(+), 43 deletions(-) create mode 100644 credentials/java/com/google/auth/CredentialTypeForMetrics.java create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/TestUtils.java diff --git a/credentials/java/com/google/auth/CredentialTypeForMetrics.java b/credentials/java/com/google/auth/CredentialTypeForMetrics.java new file mode 100644 index 000000000..ccea4ea7b --- /dev/null +++ b/credentials/java/com/google/auth/CredentialTypeForMetrics.java @@ -0,0 +1,64 @@ +/* + * Copyright 2024 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.auth; + +/** + * Defines the different types of credentials that can be used for metrics. + * + *

Each credential type is associated with a label that is used for reporting purposes. Add new + * enum constant only when corresponding configs established. + * + *

Credentials with type {@code CredentialTypeForMetrics.DO_NOT_SEND} is default value for + * credential implementations that do not set type specifically. It is not expected to send metrics. + * + *

+ * + * @see #getLabel() + */ +public enum CredentialTypeForMetrics { + USER_CREDENTIALS("u"), + SERVICE_ACCOUNT_CREDENTIALS_AT("sa"), + SERVICE_ACCOUNT_CREDENTIALS_JWT("jwt"), + VM_CREDENTIALS("mds"), + IMPERSONATED_CREDENTIALS("imp"), + DO_NOT_SEND("dns"); + + private String label; + + private CredentialTypeForMetrics(String label) { + this.label = label; + } + + public String getLabel() { + return label; + } +} diff --git a/credentials/java/com/google/auth/Credentials.java b/credentials/java/com/google/auth/Credentials.java index 493b970ad..b1579db61 100644 --- a/credentials/java/com/google/auth/Credentials.java +++ b/credentials/java/com/google/auth/Credentials.java @@ -70,6 +70,18 @@ public String getUniverseDomain() throws IOException { return GOOGLE_DEFAULT_UNIVERSE; } + /** + * Gets the credential type used for internal metrics header. + * + *

The default is {@code CredentialTypeForMetrics.DO_NOT_SEND}. For a credential that is + * established to track for metrics, this default should be overridden. + * + * @return a enum value for credential type + */ + public CredentialTypeForMetrics getMetricsCredentialType() { + return CredentialTypeForMetrics.DO_NOT_SEND; + } + /** * Get the current request metadata, refreshing tokens if required. * diff --git a/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java index 9996c045d..c1aeb28c9 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java @@ -41,10 +41,12 @@ import com.google.api.client.http.HttpStatusCodes; import com.google.api.client.json.JsonObjectParser; import com.google.api.client.util.GenericData; +import com.google.auth.CredentialTypeForMetrics; import com.google.auth.Credentials; import com.google.auth.Retryable; import com.google.auth.ServiceAccountSigner; import com.google.auth.http.HttpTransportFactory; +import com.google.auth.oauth2.MetricsUtils.RequestType; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.base.MoreObjects.ToStringHelper; @@ -133,7 +135,6 @@ public class ComputeEngineCredentials extends GoogleCredentials */ private ComputeEngineCredentials(ComputeEngineCredentials.Builder builder) { super(builder); - this.transportFactory = firstNonNull( builder.getHttpTransportFactory(), @@ -153,6 +154,11 @@ private ComputeEngineCredentials(ComputeEngineCredentials.Builder builder) { } } + @Override + public CredentialTypeForMetrics getMetricsCredentialType() { + return CredentialTypeForMetrics.VM_CREDENTIALS; + } + /** Clones the compute engine account with the specified scopes. */ @Override public GoogleCredentials createScoped(Collection newScopes) { @@ -234,7 +240,8 @@ public String getUniverseDomain() throws IOException { } private String getUniverseDomainFromMetadata() throws IOException { - HttpResponse response = getMetadataResponse(getUniverseDomainUrl()); + HttpResponse response = + getMetadataResponse(getUniverseDomainUrl(), RequestType.UNTRACKED, false); int statusCode = response.getStatusCode(); if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) { return Credentials.GOOGLE_DEFAULT_UNIVERSE; @@ -260,7 +267,8 @@ private String getUniverseDomainFromMetadata() throws IOException { /** Refresh the access token by getting it from the GCE metadata server */ @Override public AccessToken refreshAccessToken() throws IOException { - HttpResponse response = getMetadataResponse(createTokenUrlWithScopes()); + HttpResponse response = + getMetadataResponse(createTokenUrlWithScopes(), RequestType.ACCESS_TOKEN_REQUEST, true); int statusCode = response.getStatusCode(); if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) { throw new IOException( @@ -325,7 +333,8 @@ public IdToken idTokenWithAudience(String targetAudience, List additionalFields) + Map additionalFields, + CredentialTypeForMetrics credentialTypeForMetrics) throws IOException { String idTokenUrl = String.format(ID_TOKEN_URL_FORMAT, serviceAccountEmail); @@ -211,6 +215,11 @@ static IdToken getIdToken( request.setParser(parser); request.setThrowExceptionOnExecuteError(false); + MetricsUtils.setMetricsHeader( + request, + MetricsUtils.getGoogleCredentialsMetricsHeader( + RequestType.ID_TOKEN_REQUEST, credentialTypeForMetrics)); + HttpResponse response = request.execute(); int statusCode = response.getStatusCode(); if (statusCode >= 400 && statusCode < HttpStatusCodes.STATUS_CODE_SERVER_ERROR) { diff --git a/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java index f66b29a96..9430d44dd 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java @@ -43,9 +43,11 @@ import com.google.api.client.http.json.JsonHttpContent; import com.google.api.client.json.JsonObjectParser; import com.google.api.client.util.GenericData; +import com.google.auth.CredentialTypeForMetrics; import com.google.auth.ServiceAccountSigner; import com.google.auth.http.HttpCredentialsAdapter; import com.google.auth.http.HttpTransportFactory; +import com.google.auth.oauth2.MetricsUtils.RequestType; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableMap; @@ -406,7 +408,7 @@ static ImpersonatedCredentials fromJson( .setSourceCredentials(sourceCredentials) .setTargetPrincipal(targetPrincipal) .setDelegates(delegates) - .setScopes(new ArrayList()) + .setScopes(new ArrayList<>()) .setLifetime(DEFAULT_LIFETIME_IN_SECONDS) .setHttpTransportFactory(transportFactory) .setQuotaProjectId(quotaProjectId) @@ -431,6 +433,11 @@ public GoogleCredentials createScoped(Collection scopes) { .build(); } + @Override + public CredentialTypeForMetrics getMetricsCredentialType() { + return CredentialTypeForMetrics.IMPERSONATED_CREDENTIALS; + } + /** * Clones the impersonated credentials with a new calendar. * @@ -508,6 +515,10 @@ public AccessToken refreshAccessToken() throws IOException { HttpRequest request = requestFactory.buildPostRequest(url, requestContent); adapter.initialize(request); request.setParser(parser); + MetricsUtils.setMetricsHeader( + request, + MetricsUtils.getGoogleCredentialsMetricsHeader( + RequestType.ACCESS_TOKEN_REQUEST, getMetricsCredentialType())); HttpResponse response = null; try { @@ -557,7 +568,8 @@ public IdToken idTokenWithAudience(String targetAudience, List> getRequestMetadata(URI uri) throws IOException } } - private Map> getRequestMetadataForGdu(URI uri) throws IOException { + @Override + public CredentialTypeForMetrics getMetricsCredentialType() { + return shouldUseAssertionFlow() + ? CredentialTypeForMetrics.SERVICE_ACCOUNT_CREDENTIALS_AT + : CredentialTypeForMetrics.SERVICE_ACCOUNT_CREDENTIALS_JWT; + } + + private boolean shouldUseAssertionFlow() { // If scopes are provided, but we cannot use self-signed JWT or domain-wide delegation is // configured then use scopes to get access token. - if ((!createScopedRequired() && !useJwtAccessWithScope) - || isConfiguredForDomainWideDelegation()) { - return super.getRequestMetadata(uri); - } + return ((!createScopedRequired() && !useJwtAccessWithScope) + || isConfiguredForDomainWideDelegation()); + } - return getRequestMetadataWithSelfSignedJwt(uri); + private Map> getRequestMetadataForGdu(URI uri) throws IOException { + return shouldUseAssertionFlow() + ? super.getRequestMetadata(uri) + : getRequestMetadataWithSelfSignedJwt(uri); } private Map> getRequestMetadataForNonGdu(URI uri) throws IOException { diff --git a/oauth2_http/java/com/google/auth/oauth2/UserCredentials.java b/oauth2_http/java/com/google/auth/oauth2/UserCredentials.java index 67859c33d..a2ba5a52d 100644 --- a/oauth2_http/java/com/google/auth/oauth2/UserCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/UserCredentials.java @@ -45,7 +45,9 @@ import com.google.api.client.json.JsonObjectParser; import com.google.api.client.util.GenericData; import com.google.api.client.util.Preconditions; +import com.google.auth.CredentialTypeForMetrics; import com.google.auth.http.HttpTransportFactory; +import com.google.auth.oauth2.MetricsUtils.RequestType; import com.google.common.base.MoreObjects; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.ByteArrayInputStream; @@ -97,6 +99,11 @@ private UserCredentials(Builder builder) { "Either accessToken or refreshToken must not be null"); } + @Override + public CredentialTypeForMetrics getMetricsCredentialType() { + return CredentialTypeForMetrics.USER_CREDENTIALS; + } + /** * Returns user credentials defined by JSON contents using the format supported by the Cloud SDK. * @@ -264,6 +271,11 @@ private GenericData doRefreshAccessToken() throws IOException { HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory(); HttpRequest request = requestFactory.buildPostRequest(new GenericUrl(tokenServerUri), content); + + MetricsUtils.setMetricsHeader( + request, + MetricsUtils.getGoogleCredentialsMetricsHeader( + RequestType.UNTRACKED, getMetricsCredentialType())); request.setParser(new JsonObjectParser(JSON_FACTORY)); HttpResponse response; diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java index 399456187..10975d874 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java @@ -285,6 +285,11 @@ public void getRequestMetadata_hasAccessToken() throws IOException { Map> metadata = credentials.getRequestMetadata(CALL_URI); TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN); + // verify metrics header added and other header intact + Map> requestHeaders = transportFactory.transport.getRequest().getHeaders(); + com.google.auth.oauth2.TestUtils.validateMetricsHeader(requestHeaders, "at", "mds"); + assertTrue(requestHeaders.containsKey("metadata-flavor")); + assertTrue(requestHeaders.get("metadata-flavor").contains("Google")); } @Test @@ -458,6 +463,10 @@ public void getAccount_sameAs() throws IOException { ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); assertEquals(defaultAccountEmail, credentials.getAccount()); + + // metric headers are not supported for getAccount() + Map> headers = transportFactory.transport.getRequest().getHeaders(); + assertFalse(headers.containsKey(MetricsUtils.API_CLIENT_HEADER)); } @Test @@ -949,6 +958,10 @@ public void idTokenWithAudience_full() throws IOException { assertTrue("Full ID Token format not provided", p.containsKey("google")); ArrayMap googleClaim = (ArrayMap) p.get("google"); assertTrue(googleClaim.containsKey("compute_engine")); + + // verify metrics header + Map> requestHeaders = transportFactory.transport.getRequest().getHeaders(); + com.google.auth.oauth2.TestUtils.validateMetricsHeader(requestHeaders, "it", "mds"); } @Test diff --git a/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java b/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java index 3f0b2f1a5..f715908a1 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java @@ -468,6 +468,11 @@ public void getDefaultCredentials_compute_quotaProject() throws IOException { assertTrue(defaultCredentials instanceof ComputeEngineCredentials); assertEquals(QUOTA_PROJECT_FROM_ENVIRONMENT, defaultCredentials.getQuotaProjectId()); + + // verify metrics header + Map> headers = transportFactory.transport.getRequest().getHeaders(); + com.google.auth.oauth2.TestUtils.validateMetricsHeader(headers, "mds", "untracked"); + assertEquals("Google", headers.get("metadata-flavor").get(0)); } @Test diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java index 6e0b34e60..6fbeac652 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java @@ -471,6 +471,12 @@ public void refreshAccessToken_success() throws IOException, IllegalStateExcepti assertEquals(ACCESS_TOKEN, targetCredentials.refreshAccessToken().getTokenValue()); assertEquals( DEFAULT_IMPERSONATION_URL, mockTransportFactory.getTransport().getRequest().getUrl()); + + // verify metrics header added and authorization header intact + Map> requestHeader = + mockTransportFactory.getTransport().getRequest().getHeaders(); + com.google.auth.oauth2.TestUtils.validateMetricsHeader(requestHeader, "at", "imp"); + assertTrue(requestHeader.containsKey("authorization")); } @Test @@ -868,6 +874,12 @@ public void idTokenWithAudience_withEmail() throws IOException { assertEquals(TOKEN_WITH_EMAIL, tokenCredential.getAccessToken().getTokenValue()); Payload p = tokenCredential.getIdToken().getJsonWebSignature().getPayload(); assertTrue(p.containsKey("email")); + + // verify metrics header + Map> requestHeader = + mockTransportFactory.getTransport().getRequest().getHeaders(); + com.google.auth.oauth2.TestUtils.validateMetricsHeader(requestHeader, "it", "imp"); + assertTrue(requestHeader.containsKey("authorization")); } @Test diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MetricsUtilsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/MetricsUtilsTest.java index aba4d98c9..eb035d09c 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/MetricsUtilsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/MetricsUtilsTest.java @@ -33,6 +33,8 @@ import static org.junit.Assert.*; +import com.google.auth.CredentialTypeForMetrics; +import com.google.auth.oauth2.MetricsUtils.RequestType; import java.util.regex.Pattern; import org.junit.Test; import org.junit.runner.RunWith; @@ -40,17 +42,48 @@ @RunWith(JUnit4.class) public class MetricsUtilsTest { + static final String VERSION_PATTERN = + "gl-java/[\\d\\._-]+ auth/\\d+\\.\\d+\\.\\d+(-sp\\.\\d+)?(-SNAPSHOT)?"; + static final String AUTH_REQUEST_TYPE_PATTERN = + String.format(" %s/[\\w]+", MetricsUtils.AUTH_REQUEST_TYPE); + static final String CRED_TYPE_PATTERN = String.format(" %s/[\\w]+", MetricsUtils.CRED_TYPE); + static final String METRICS_PATTERN_FULL = + VERSION_PATTERN + AUTH_REQUEST_TYPE_PATTERN + CRED_TYPE_PATTERN; + static final String METRICS_PATTERN_NO_REQUEST_TYPE = VERSION_PATTERN + CRED_TYPE_PATTERN; + static final String METRICS_PATTERN_NO_CRED_TYPE = VERSION_PATTERN + AUTH_REQUEST_TYPE_PATTERN; - public static void assertVersions(String version) { - assertNotNull("version constant should not be null", version); - Pattern semverPattern = - Pattern.compile("gl-java/[\\d\\._-]+ auth/\\d+\\.\\d+\\.\\d+(-sp\\.\\d+)?(-SNAPSHOT)?"); - assertTrue(semverPattern.matcher(version).matches()); + private static void assertPatterns(String contentToTest, String patternString) { + assertNotNull("metric header string should not be null", contentToTest); + Pattern pattern = Pattern.compile(patternString); + assertTrue(pattern.matcher(contentToTest).matches()); } @Test - public void getVersionWorks() { + public void getLanguageAndAuthLibraryVersionsTest() { String version = MetricsUtils.getLanguageAndAuthLibraryVersions(); - assertVersions(version); + assertPatterns(version, VERSION_PATTERN); + } + + @Test + public void getGoogleCredentialsMetricsHeaderTest() { + String metricsStringNoRequestType = + MetricsUtils.getGoogleCredentialsMetricsHeader( + RequestType.UNTRACKED, CredentialTypeForMetrics.USER_CREDENTIALS); + assertPatterns(metricsStringNoRequestType, METRICS_PATTERN_NO_REQUEST_TYPE); + + String metricsStringNoCredType = + MetricsUtils.getGoogleCredentialsMetricsHeader( + RequestType.METADATA_SERVER_PING, CredentialTypeForMetrics.DO_NOT_SEND); + assertPatterns(metricsStringNoCredType, METRICS_PATTERN_NO_CRED_TYPE); + + String metricsString = + MetricsUtils.getGoogleCredentialsMetricsHeader( + RequestType.ID_TOKEN_REQUEST, CredentialTypeForMetrics.SERVICE_ACCOUNT_CREDENTIALS_AT); + assertPatterns(metricsString, METRICS_PATTERN_FULL); + + String metricsStringNoTypes = + MetricsUtils.getGoogleCredentialsMetricsHeader( + RequestType.UNTRACKED, CredentialTypeForMetrics.DO_NOT_SEND); + assertPatterns(metricsStringNoTypes, VERSION_PATTERN); } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java index c1f0ac4b0..d21491027 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java @@ -62,6 +62,8 @@ public class MockMetadataServerTransport extends MockHttpTransport { private byte[] signature; + private MockLowLevelHttpRequest request; + public MockMetadataServerTransport() {} public MockMetadataServerTransport(String accessToken) { @@ -101,31 +103,41 @@ public void setIdToken(String idToken) { this.idToken = idToken; } + public MockLowLevelHttpRequest getRequest() { + return request; + } + @Override public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { if (url.startsWith(ComputeEngineCredentials.getTokenServerEncodedUrl())) { - return getMockRequestForTokenEndpoint(url); + this.request = getMockRequestForTokenEndpoint(url); + return this.request; } else if (isGetServiceAccountsUrl(url)) { - return getMockRequestForServiceAccount(url); + this.request = getMockRequestForServiceAccount(url); + return this.request; } else if (isSignRequestUrl(url)) { - return getMockRequestForSign(url); + this.request = getMockRequestForSign(url); + return this.request; } else if (isIdentityDocumentUrl(url)) { - return getMockRequestForIdentityDocument(url); + this.request = getMockRequestForIdentityDocument(url); + return this.request; } - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() { - if (requestStatusCode != null) { - return new MockLowLevelHttpResponse() - .setStatusCode(requestStatusCode) - .setContent("Metadata Error"); - } - - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - response.addHeader("Metadata-Flavor", "Google"); - return response; - } - }; + this.request = + new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() { + if (requestStatusCode != null) { + return new MockLowLevelHttpResponse() + .setStatusCode(requestStatusCode) + .setContent("Metadata Error"); + } + + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.addHeader("Metadata-Flavor", "Google"); + return response; + } + }; + return this.request; } private MockLowLevelHttpRequest getMockRequestForSign(String url) { diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java index f79338f7a..2c8accbeb 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java @@ -52,6 +52,7 @@ import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.api.client.util.Clock; import com.google.api.client.util.Joiner; +import com.google.auth.CredentialTypeForMetrics; import com.google.auth.Credentials; import com.google.auth.RequestMetadataCallback; import com.google.auth.TestUtils; @@ -615,6 +616,11 @@ public void getRequestMetadata_customTokenServer_hasAccessToken() throws IOExcep Map> metadata = credentials.getRequestMetadata(CALL_URI); TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN); + + // verify header + Map> accessTokenRequestHeader = + transportFactory.transport.getRequest().getHeaders(); + com.google.auth.oauth2.TestUtils.validateMetricsHeader(accessTokenRequestHeader, "at", "sa"); } @Test @@ -857,6 +863,11 @@ public void idTokenWithAudience_oauthFlow_targetAudienceMatchesAudClaim() throws transport.addServiceAccount(CLIENT_EMAIL, accessToken1); TestUtils.assertContainsBearerToken(credentials.getRequestMetadata(CALL_URI), accessToken1); + // verify access token request metrics headers + Map> accessTokenRequestHeader = + transportFactory.transport.getRequest().getHeaders(); + com.google.auth.oauth2.TestUtils.validateMetricsHeader(accessTokenRequestHeader, "at", "sa"); + String targetAudience = "https://foo.bar"; IdTokenCredentials tokenCredential = IdTokenCredentials.newBuilder() @@ -871,6 +882,11 @@ public void idTokenWithAudience_oauthFlow_targetAudienceMatchesAudClaim() throws assertEquals( targetAudience, tokenCredential.getIdToken().getJsonWebSignature().getPayload().getAudience()); + + // verify id token request metrics headers + Map> idTokenRequestHeader = + transportFactory.transport.getRequest().getHeaders(); + com.google.auth.oauth2.TestUtils.validateMetricsHeader(idTokenRequestHeader, "it", "sa"); } @Test @@ -1590,6 +1606,12 @@ public void getRequestMetadata_withScopes_selfSignedJWT() throws IOException { Map> metadata = credentials.getRequestMetadata(CALL_URI); assertNotNull(((ServiceAccountCredentials) credentials).getSelfSignedJwtCredentialsWithScope()); verifyJwtAccess(metadata, "dummy.scope"); + + // Verify credentialType is correctly set. This is used for token usage metrics. + // Self signed jwt flow doesn’t call any token endpoint, thus no token request metrics. + assertEquals( + CredentialTypeForMetrics.SERVICE_ACCOUNT_CREDENTIALS_JWT, + credentials.getMetricsCredentialType()); } @Test diff --git a/oauth2_http/javatests/com/google/auth/oauth2/TestUtils.java b/oauth2_http/javatests/com/google/auth/oauth2/TestUtils.java new file mode 100644 index 000000000..99af8c106 --- /dev/null +++ b/oauth2_http/javatests/com/google/auth/oauth2/TestUtils.java @@ -0,0 +1,72 @@ +/* + * Copyright 2024, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.auth.oauth2; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.Map; + +public class TestUtils { + static void validateMetricsHeader( + Map> headers, String requestType, String credentialType) { + assertTrue(headers.containsKey(MetricsUtils.API_CLIENT_HEADER)); + String actualMetricsValue = headers.get(MetricsUtils.API_CLIENT_HEADER).get(0); + String expectedMetricsValue; + if (requestType.equals("untracked")) { + expectedMetricsValue = + String.format( + "%s %s/%s", + MetricsUtils.getLanguageAndAuthLibraryVersions(), + MetricsUtils.CRED_TYPE, + credentialType); + } else if (credentialType.equals("untracked")) { + expectedMetricsValue = + String.format( + "%s %s/%s", + MetricsUtils.getLanguageAndAuthLibraryVersions(), + MetricsUtils.AUTH_REQUEST_TYPE, + requestType); + } else { + expectedMetricsValue = + String.format( + "%s %s/%s %s/%s", + MetricsUtils.getLanguageAndAuthLibraryVersions(), + MetricsUtils.AUTH_REQUEST_TYPE, + requestType, + MetricsUtils.CRED_TYPE, + credentialType); + } + assertEquals(expectedMetricsValue, actualMetricsValue); + } +} diff --git a/oauth2_http/javatests/com/google/auth/oauth2/UserCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/UserCredentialsTest.java index 7fc0a256f..a9019939f 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/UserCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/UserCredentialsTest.java @@ -724,6 +724,12 @@ public void IdTokenCredentials_WithUserEmailScope_success() throws IOException { assertEquals(ACCESS_TOKEN, credentials.getAccessToken().getTokenValue()); + // verify access token request metrics headers + Map> accessTokenRequestHeader = + transportFactory.transport.getRequest().getHeaders(); + com.google.auth.oauth2.TestUtils.validateMetricsHeader( + accessTokenRequestHeader, "untracked", "u"); + IdTokenCredentials tokenCredential = IdTokenCredentials.newBuilder().setIdTokenProvider(credentials).build(); @@ -735,6 +741,11 @@ public void IdTokenCredentials_WithUserEmailScope_success() throws IOException { assertEquals(DEFAULT_ID_TOKEN, tokenCredential.getAccessToken().getTokenValue()); assertEquals(DEFAULT_ID_TOKEN, tokenCredential.getIdToken().getTokenValue()); + + // verify id token request metrics headers, same as access token request + Map> idTokenRequestHeader = + transportFactory.transport.getRequest().getHeaders(); + com.google.auth.oauth2.TestUtils.validateMetricsHeader(idTokenRequestHeader, "untracked", "u"); } @Test