diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java index 98651f295..6613b3f76 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java @@ -197,6 +197,11 @@ public GoogleCredentials createScoped(Collection newScopes) { return new AwsCredentials((AwsCredentials.Builder) newBuilder(this).setScopes(newScopes)); } + @Override + String getCredentialSourceType() { + return "aws"; + } + private String retrieveResource(String url, String resourceName, Map headers) throws IOException { return retrieveResource(url, resourceName, HttpMethods.GET, headers, /* content= */ null); diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index ba4b30d4e..089c3b0a5 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -33,6 +33,7 @@ import static com.google.common.base.Preconditions.checkNotNull; +import com.google.api.client.http.HttpHeaders; import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonObjectParser; import com.google.auth.RequestMetadataCallback; @@ -90,6 +91,7 @@ abstract static class CredentialSource implements java.io.Serializable { private final CredentialSource credentialSource; private final Collection scopes; private final ServiceAccountImpersonationOptions serviceAccountImpersonationOptions; + private ExternalAccountMetricsHandler metricsHandler; @Nullable private final String tokenInfoUrl; @Nullable private final String serviceAccountImpersonationUrl; @@ -224,6 +226,8 @@ protected ExternalAccountCredentials( validateServiceAccountImpersonationInfoUrl(serviceAccountImpersonationUrl); } + this.metricsHandler = new ExternalAccountMetricsHandler(this); + this.impersonatedCredentials = buildImpersonatedCredentials(); } @@ -274,6 +278,11 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder) validateServiceAccountImpersonationInfoUrl(serviceAccountImpersonationUrl); } + this.metricsHandler = + builder.metricsHandler == null + ? new ExternalAccountMetricsHandler(this) + : builder.metricsHandler; + this.impersonatedCredentials = buildImpersonatedCredentials(); } @@ -505,6 +514,12 @@ protected AccessToken exchangeExternalCredentialForAccessToken( requestHandler.setInternalOptions(options.toString()); } + // Set BYOID Metrics header. + HttpHeaders additionalHeaders = new HttpHeaders(); + additionalHeaders.set( + MetricsUtils.API_CLIENT_HEADER, this.metricsHandler.getExternalAccountMetricsHeader()); + requestHandler.setHeaders(additionalHeaders); + if (stsTokenExchangeRequest.getInternalOptions() != null) { // Overwrite internal options. Let subclass handle setting options. requestHandler.setInternalOptions(stsTokenExchangeRequest.getInternalOptions()); @@ -589,6 +604,10 @@ public ServiceAccountImpersonationOptions getServiceAccountImpersonationOptions( return serviceAccountImpersonationOptions; } + String getCredentialSourceType() { + return "unknown"; + } + EnvironmentProvider getEnvironmentProvider() { return environmentProvider; } @@ -663,8 +682,11 @@ static final class ServiceAccountImpersonationOptions implements java.io.Seriali private final int lifetime; + final boolean customTokenLifetimeRequested; + ServiceAccountImpersonationOptions(Map optionsMap) { - if (!optionsMap.containsKey(TOKEN_LIFETIME_SECONDS_KEY)) { + customTokenLifetimeRequested = optionsMap.containsKey(TOKEN_LIFETIME_SECONDS_KEY); + if (!customTokenLifetimeRequested) { lifetime = DEFAULT_TOKEN_LIFETIME_SECONDS; return; } @@ -714,6 +736,7 @@ public abstract static class Builder extends GoogleCredentials.Builder { @Nullable protected String workforcePoolUserProject; @Nullable protected ServiceAccountImpersonationOptions serviceAccountImpersonationOptions; @Nullable protected String universeDomain; + @Nullable protected ExternalAccountMetricsHandler metricsHandler; protected Builder() {} @@ -733,6 +756,7 @@ protected Builder(ExternalAccountCredentials credentials) { this.workforcePoolUserProject = credentials.workforcePoolUserProject; this.serviceAccountImpersonationOptions = credentials.serviceAccountImpersonationOptions; this.universeDomain = credentials.universeDomain; + this.metricsHandler = credentials.metricsHandler; } /** diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountMetricsHandler.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountMetricsHandler.java new file mode 100644 index 000000000..fcb656b5d --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountMetricsHandler.java @@ -0,0 +1,78 @@ +/* + * Copyright 2023 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.oauth2; + +/** + * A handler for generating the x-goog-api-client header value for BYOID external account + * credentials. + */ +class ExternalAccountMetricsHandler implements java.io.Serializable { + private static final String SOURCE_KEY = "source"; + private static final String IMPERSONATION_KEY = "sa-impersonation"; + private static final String CONFIG_LIFETIME_KEY = "config-lifetime"; + private static final String BYOID_METRICS_SECTION = "google-byoid-sdk"; + + private final boolean configLifetime; + private final boolean saImpersonation; + private String credentialSourceType; + + /** + * Constructor for the external account metrics handler. + * + * @param creds the {@code ExternalAccountCredentials} object to set the external account metrics + * options from. + */ + ExternalAccountMetricsHandler(ExternalAccountCredentials creds) { + this.saImpersonation = creds.getServiceAccountImpersonationUrl() != null; + this.configLifetime = + creds.getServiceAccountImpersonationOptions().customTokenLifetimeRequested; + this.credentialSourceType = creds.getCredentialSourceType(); + } + + /** + * Gets the external account metrics header value for the x-goog-api-client header. + * + * @return the header value. + */ + String getExternalAccountMetricsHeader() { + return String.format( + "%s %s %s/%s %s/%s %s/%s", + MetricsUtils.getLanguageAndAuthLibraryVersions(), + BYOID_METRICS_SECTION, + SOURCE_KEY, + this.credentialSourceType, + IMPERSONATION_KEY, + this.saImpersonation, + CONFIG_LIFETIME_KEY, + this.configLifetime); + } +} diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java index 91b837e45..aab014f2e 100644 --- a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @@ -38,6 +38,7 @@ import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonObjectParser; import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource.CredentialFormatType; +import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource.IdentityPoolCredentialSourceType; import com.google.common.io.CharStreams; import java.io.BufferedReader; import java.io.File; @@ -192,6 +193,16 @@ public String retrieveSubjectToken() throws IOException { return getSubjectTokenFromMetadataServer(); } + @Override + String getCredentialSourceType() { + if (((IdentityPoolCredentialSource) this.getCredentialSource()).credentialSourceType + == IdentityPoolCredentialSourceType.FILE) { + return "file"; + } else { + return "url"; + } + } + private String retrieveSubjectTokenFromCredentialFile() throws IOException { String credentialFilePath = identityPoolCredentialSource.credentialLocation; if (!Files.exists(Paths.get(credentialFilePath), LinkOption.NOFOLLOW_LINKS)) { diff --git a/oauth2_http/java/com/google/auth/oauth2/MetricsUtils.java b/oauth2_http/java/com/google/auth/oauth2/MetricsUtils.java new file mode 100644 index 000000000..0267e8729 --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/MetricsUtils.java @@ -0,0 +1,70 @@ +/* + * Copyright 2023 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.oauth2; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +class MetricsUtils { + static final String API_CLIENT_HEADER = "x-goog-api-client"; + private static final String authLibraryVersion = getAuthLibraryVersion(); + private static final String javaLanguageVersion = System.getProperty("java.version"); + + /** + * Gets the x-goog-api-client header value for the current Java language version and the auth + * library version. + * + * @return the header value. + */ + static String getLanguageAndAuthLibraryVersions() { + return String.format("gl-java/%s auth/%s", javaLanguageVersion, authLibraryVersion); + } + + private static String getAuthLibraryVersion() { + // Attempt to read the library's version from a properties file generated during the build. + // This value should be read and cached for later use. + String version = "unknown-version"; + try (InputStream inputStream = + MetricsUtils.class.getResourceAsStream( + "/com/google/auth/oauth2/google-auth-library.properties")) { + if (inputStream != null) { + final Properties properties = new Properties(); + properties.load(inputStream); + version = properties.getProperty("google-auth-library.version"); + } + } catch (IOException e) { + // Ignore. + } + return version; + } +} diff --git a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java index 0042dfdc2..0fe3c9800 100644 --- a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java @@ -292,6 +292,11 @@ public PluggableAuthCredentials createScoped(Collection newScopes) { (PluggableAuthCredentials.Builder) newBuilder(this).setScopes(newScopes)); } + @Override + String getCredentialSourceType() { + return "executable"; + } + public static Builder newBuilder() { return new Builder(); } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java index 248bc92df..9abbcc822 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java @@ -131,6 +131,11 @@ public void refreshAccessToken_withoutServiceAccountImpersonation() throws IOExc AccessToken accessToken = awsCredential.refreshAccessToken(); assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue()); + + // Validate metrics header is set correctly on the sts request. + Map> headers = + transportFactory.transport.getRequests().get(3).getHeaders(); + ExternalAccountCredentialsTest.validateMetricsHeader(headers, "aws", false, false); } @Test @@ -142,18 +147,26 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept AwsCredentials awsCredential = (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) + AwsCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") .setTokenUrl(transportFactory.transport.getStsUrl()) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(buildAwsCredentialSource(transportFactory)) .setServiceAccountImpersonationUrl( transportFactory.transport.getServiceAccountImpersonationUrl()) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsCredentialSource(transportFactory)) .build(); AccessToken accessToken = awsCredential.refreshAccessToken(); assertEquals( transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); + + // Validate metrics header is set correctly on the sts request. + Map> headers = + transportFactory.transport.getRequests().get(6).getHeaders(); + ExternalAccountCredentialsTest.validateMetricsHeader(headers, "aws", true, false); } @Test @@ -165,12 +178,15 @@ public void refreshAccessToken_withServiceAccountImpersonationOptions() throws I AwsCredentials awsCredential = (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) + AwsCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") .setTokenUrl(transportFactory.transport.getStsUrl()) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(buildAwsCredentialSource(transportFactory)) .setServiceAccountImpersonationUrl( transportFactory.transport.getServiceAccountImpersonationUrl()) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsCredentialSource(transportFactory)) .setServiceAccountImpersonationOptions( ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions(2800)) .build(); @@ -187,6 +203,11 @@ public void refreshAccessToken_withServiceAccountImpersonationOptions() throws I .parseAndClose(GenericJson.class); assertEquals("2800s", query.get("lifetime")); + + // Validate metrics header is set correctly on the sts request. + Map> headers = + transportFactory.transport.getRequests().get(6).getHeaders(); + ExternalAccountCredentialsTest.validateMetricsHeader(headers, "aws", true, true); } @Test diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index 6e0f1efd3..c147675d5 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -834,6 +834,11 @@ public void exchangeExternalCredentialForAccessToken() throws IOException { Map query = TestUtils.parseQuery(transportFactory.transport.getLastRequest().getContentAsString()); assertNull(query.get("options")); + + // Validate metrics header is set correctly on the sts request. + Map> headers = + transportFactory.transport.getRequests().get(0).getHeaders(); + validateMetricsHeader(headers, "file", false, false); } @Test @@ -952,6 +957,11 @@ public void exchangeExternalCredentialForAccessToken_withServiceAccountImpersona .parseAndClose(GenericJson.class); assertEquals("3600s", query.get("lifetime")); + + // Validate metrics header is set correctly on the sts request. + Map> headers = + transportFactory.transport.getRequests().get(1).getHeaders(); + validateMetricsHeader(headers, "url", true, false); } @Test @@ -983,6 +993,10 @@ public void exchangeExternalCredentialForAccessToken_withServiceAccountImpersona .createJsonParser(transportFactory.transport.getLastRequest().getContentAsString()) .parseAndClose(GenericJson.class); + // Validate metrics header is set correctly on the sts request. + Map> headers = + transportFactory.transport.getRequests().get(1).getHeaders(); + validateMetricsHeader(headers, "url", true, true); assertEquals("2800s", query.get("lifetime")); } @@ -1257,6 +1271,23 @@ static Map buildServiceAccountImpersonationOptions(Integer lifet return map; } + static void validateMetricsHeader( + Map> headers, + String source, + boolean saImpersonationUsed, + boolean configLifetimeUsed) { + assertTrue(headers.containsKey(MetricsUtils.API_CLIENT_HEADER)); + String actualMetricsValue = headers.get(MetricsUtils.API_CLIENT_HEADER).get(0); + String expectedMetricsValue = + String.format( + "%s google-byoid-sdk source/%s sa-impersonation/%s config-lifetime/%s", + MetricsUtils.getLanguageAndAuthLibraryVersions(), + source, + saImpersonationUsed, + configLifetimeUsed); + assertEquals(expectedMetricsValue, actualMetricsValue); + } + static class TestExternalAccountCredentials extends ExternalAccountCredentials { static class TestCredentialSource extends IdentityPoolCredentials.IdentityPoolCredentialSource { protected TestCredentialSource(Map credentialSourceMap) { diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java index 560334965..cf04a43fb 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java @@ -318,7 +318,12 @@ public void refreshAccessToken_withoutServiceAccountImpersonation() throws IOExc IdentityPoolCredentials credential = (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) + IdentityPoolCredentials.newBuilder() + .setAudience( + "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(FILE_CREDENTIAL_SOURCE) .setTokenUrl(transportFactory.transport.getStsUrl()) .setHttpTransportFactory(transportFactory) .setCredentialSource( @@ -328,6 +333,11 @@ public void refreshAccessToken_withoutServiceAccountImpersonation() throws IOExc AccessToken accessToken = credential.refreshAccessToken(); assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue()); + + // Validate metrics header is set correctly on the sts request. + Map> headers = + transportFactory.transport.getRequests().get(1).getHeaders(); + ExternalAccountCredentialsTest.validateMetricsHeader(headers, "url", false, false); } @Test @@ -372,10 +382,14 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); IdentityPoolCredentials credential = (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) - .setTokenUrl(transportFactory.transport.getStsUrl()) + IdentityPoolCredentials.newBuilder() + .setAudience( + "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenInfoUrl("tokenInfoUrl") .setServiceAccountImpersonationUrl( transportFactory.transport.getServiceAccountImpersonationUrl()) + .setTokenUrl(transportFactory.transport.getStsUrl()) .setHttpTransportFactory(transportFactory) .setCredentialSource( buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) @@ -385,6 +399,11 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept assertEquals( transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); + + // Validate metrics header is set correctly on the sts request. + Map> headers = + transportFactory.transport.getRequests().get(2).getHeaders(); + ExternalAccountCredentialsTest.validateMetricsHeader(headers, "url", true, false); } @Test @@ -395,11 +414,15 @@ public void refreshAccessToken_withServiceAccountImpersonationOptions() throws I transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); IdentityPoolCredentials credential = (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) + IdentityPoolCredentials.newBuilder() + .setAudience( + "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenInfoUrl("tokenInfoUrl") .setTokenUrl(transportFactory.transport.getStsUrl()) + .setHttpTransportFactory(transportFactory) .setServiceAccountImpersonationUrl( transportFactory.transport.getServiceAccountImpersonationUrl()) - .setHttpTransportFactory(transportFactory) .setCredentialSource( buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) .setServiceAccountImpersonationOptions( @@ -418,6 +441,11 @@ public void refreshAccessToken_withServiceAccountImpersonationOptions() throws I .parseAndClose(GenericJson.class); assertEquals("2800s", query.get("lifetime")); + + // Validate metrics header is set correctly on the sts request. + Map> headers = + transportFactory.transport.getRequests().get(2).getHeaders(); + ExternalAccountCredentialsTest.validateMetricsHeader(headers, "url", true, true); } @Test diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MetricsUtilsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/MetricsUtilsTest.java new file mode 100644 index 000000000..aba4d98c9 --- /dev/null +++ b/oauth2_http/javatests/com/google/auth/oauth2/MetricsUtilsTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2023, 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.oauth2; + +import static org.junit.Assert.*; + +import java.util.regex.Pattern; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class MetricsUtilsTest { + + 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()); + } + + @Test + public void getVersionWorks() { + String version = MetricsUtils.getLanguageAndAuthLibraryVersions(); + assertVersions(version); + } +} diff --git a/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java index ddc321fdd..fcd845e7a 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java @@ -200,6 +200,11 @@ public void refreshAccessToken_withoutServiceAccountImpersonation() throws IOExc Map query = TestUtils.parseQuery(transportFactory.transport.getRequests().get(0).getContentAsString()); assertEquals(query.get("subject_token"), "pluggableAuthToken"); + + // Validate metrics header is set correctly on the sts request. + Map> headers = + transportFactory.transport.getRequests().get(0).getHeaders(); + ExternalAccountCredentialsTest.validateMetricsHeader(headers, "executable", false, false); } @Test @@ -211,14 +216,23 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept PluggableAuthCredentials credential = (PluggableAuthCredentials) - PluggableAuthCredentials.newBuilder(CREDENTIAL) - .setExecutableHandler(options -> "pluggableAuthToken") + PluggableAuthCredentials.newBuilder() + .setAudience( + "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenInfoUrl("tokenInfoUrl") .setTokenUrl(transportFactory.transport.getStsUrl()) + .setCredentialSource(buildCredentialSource()) .setServiceAccountImpersonationUrl( transportFactory.transport.getServiceAccountImpersonationUrl()) .setHttpTransportFactory(transportFactory) .build(); + credential = + PluggableAuthCredentials.newBuilder(credential) + .setExecutableHandler(options -> "pluggableAuthToken") + .build(); + AccessToken accessToken = credential.refreshAccessToken(); assertEquals( @@ -228,6 +242,11 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept Map query = TestUtils.parseQuery(transportFactory.transport.getRequests().get(0).getContentAsString()); assertEquals(query.get("subject_token"), "pluggableAuthToken"); + + // Validate metrics header is set correctly on the sts request. + Map> headers = + transportFactory.transport.getRequests().get(0).getHeaders(); + ExternalAccountCredentialsTest.validateMetricsHeader(headers, "executable", true, false); } @Test @@ -239,16 +258,25 @@ public void refreshAccessToken_withServiceAccountImpersonationOptions() throws I PluggableAuthCredentials credential = (PluggableAuthCredentials) - PluggableAuthCredentials.newBuilder(CREDENTIAL) - .setExecutableHandler(options -> "pluggableAuthToken") + PluggableAuthCredentials.newBuilder() + .setAudience( + "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenInfoUrl("tokenInfoUrl") .setTokenUrl(transportFactory.transport.getStsUrl()) + .setCredentialSource(buildCredentialSource()) .setServiceAccountImpersonationUrl( transportFactory.transport.getServiceAccountImpersonationUrl()) - .setHttpTransportFactory(transportFactory) .setServiceAccountImpersonationOptions( ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions(2800)) + .setHttpTransportFactory(transportFactory) .build(); + credential = + PluggableAuthCredentials.newBuilder(credential) + .setExecutableHandler(options -> "pluggableAuthToken") + .build(); + AccessToken accessToken = credential.refreshAccessToken(); assertEquals( @@ -261,6 +289,11 @@ public void refreshAccessToken_withServiceAccountImpersonationOptions() throws I .parseAndClose(GenericJson.class); assertEquals("2800s", query.get("lifetime")); + + // Validate metrics header is set correctly on the sts request. + Map> headers = + transportFactory.transport.getRequests().get(0).getHeaders(); + ExternalAccountCredentialsTest.validateMetricsHeader(headers, "executable", true, true); } @Test diff --git a/oauth2_http/pom.xml b/oauth2_http/pom.xml index 4e8c69678..3c158f708 100644 --- a/oauth2_http/pom.xml +++ b/oauth2_http/pom.xml @@ -86,6 +86,7 @@ resources + true javatests @@ -95,6 +96,17 @@ + + org.apache.maven.plugins + maven-resources-plugin + + + + resources + + + + org.sonatype.plugins nexus-staging-maven-plugin @@ -120,6 +132,9 @@ maven-jar-plugin + + true + com.google.auth.oauth2 diff --git a/oauth2_http/resources/com/google/auth/oauth2/google-auth-library.properties b/oauth2_http/resources/com/google/auth/oauth2/google-auth-library.properties new file mode 100644 index 000000000..32474acb0 --- /dev/null +++ b/oauth2_http/resources/com/google/auth/oauth2/google-auth-library.properties @@ -0,0 +1 @@ +google-auth-library.version=${project.parent.version} diff --git a/pom.xml b/pom.xml index 996cc08d5..d8da8a347 100644 --- a/pom.xml +++ b/pom.xml @@ -316,6 +316,11 @@ + + org.apache.maven.plugins + maven-resources-plugin + 3.3.0 +