From dc5b79caac0cbf45fe5f126cf58f021b2874a983 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Wed, 4 Mar 2026 16:47:26 -0500 Subject: [PATCH 01/24] fix: allow for ES algorithm in GdchCredentials --- .../google/auth/oauth2/GdchCredentials.java | 401 ++++++++++-------- .../com/google/auth/oauth2/OAuth2Utils.java | 16 +- 2 files changed, 238 insertions(+), 179 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java index c5e8bd576..077f3a618 100644 --- a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java @@ -1,34 +1,3 @@ -/* - * Copyright 2022, 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 com.google.api.client.http.GenericUrl; @@ -37,14 +6,18 @@ import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpTransport; -import com.google.api.client.http.UrlEncodedContent; import com.google.api.client.http.javanet.NetHttpTransport; -import com.google.api.client.json.GenericJson; +import com.google.api.client.http.json.JsonHttpContent; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.JsonObjectParser; +import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.json.webtoken.JsonWebSignature; import com.google.api.client.json.webtoken.JsonWebToken; +import com.google.api.client.util.Base64; +import com.google.api.client.util.Clock; import com.google.api.client.util.GenericData; +import com.google.api.client.util.SecurityUtils; +import com.google.api.client.util.StringUtils; import com.google.auth.http.HttpTransportFactory; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; @@ -56,6 +29,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; +import java.math.BigDecimal; import java.net.URI; import java.net.URISyntaxException; import java.security.GeneralSecurityException; @@ -65,8 +39,15 @@ import java.util.Objects; public class GdchCredentials extends GoogleCredentials { - static final String SUPPORTED_FORMAT_VERSION = "1"; + private static String VALUE_NOT_FOUND_MESSAGE = "%sExpected value %s not found."; + private static String VALUE_WRONG_TYPE_MESSAGE = "%sExpected %s value %s of wrong type."; private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. "; + @VisibleForTesting static final String SUPPORTED_FORMAT_VERSION = "1"; + + private static final String ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"; + private static final String SERVICE_ACCOUNT_TOKEN_TYPE = "urn:k8s:params:oauth:token-type:serviceaccount"; + private static final String TOKEN_TYPE_TOKEN_EXCHANGE = "urn:ietf:params:oauth:token-type:token-exchange"; + private static final int DEFAULT_LIFETIME_IN_SECONDS = 3600; private final PrivateKey privateKey; @@ -74,7 +55,7 @@ public class GdchCredentials extends GoogleCredentials { private final String projectId; private final String serviceIdentityName; private final URI tokenServerUri; - private final URI apiAudience; + private final String apiAudience; private final int lifetime; private final String transportFactoryClassName; private final String caCertPath; @@ -83,7 +64,8 @@ public class GdchCredentials extends GoogleCredentials { /** * Internal constructor. * - * @param builder A builder for {@link GdchCredentials} See {@link GdchCredentials.Builder}. + * @param builder A builder for {@link GdchCredentials} See + * {@link GdchCredentials.Builder}. */ @VisibleForTesting GdchCredentials(GdchCredentials.Builder builder) { @@ -97,59 +79,6 @@ public class GdchCredentials extends GoogleCredentials { this.caCertPath = builder.caCertPath; this.apiAudience = builder.apiAudience; this.lifetime = builder.lifetime; - this.name = GoogleCredentialsInfo.GDCH_CREDENTIALS.getCredentialName(); - } - - /** - * Returns credentials defined by a GdchCredentials key file in JSON format from the Google - * Developers Console. - * - *

Important: If you accept a credential configuration (credential JSON/File/Stream) from an - * external source for authentication to Google Cloud Platform, you must validate it before - * providing it to any Google API or library. Providing an unvalidated credential configuration to - * Google APIs can compromise the security of your systems and data. For more information, refer - * to {@see documentation}. - * - * @param credentialsStream the stream with the credential definition. - * @return the credential defined by the credentialsStream. - * @throws IOException if the credential cannot be created from the stream. - */ - public static GdchCredentials fromStream(InputStream credentialsStream) throws IOException { - return fromStream(credentialsStream, OAuth2Utils.HTTP_TRANSPORT_FACTORY); - } - - /** - * Returns credentials defined by a GdchCredentials key file in JSON format from the Google - * Developers Console. - * - *

Important: If you accept a credential configuration (credential JSON/File/Stream) from an - * external source for authentication to Google Cloud Platform, you must validate it before - * providing it to any Google API or library. Providing an unvalidated credential configuration to - * Google APIs can compromise the security of your systems and data. For more information, refer - * to {@see documentation}. - * - * @param credentialsStream the stream with the credential definition. - * @param transportFactory HTTP transport factory, creates the transport used to get access - * tokens. - * @return the credential defined by the credentialsStream. - * @throws IOException if the credential cannot be created from the stream. - */ - public static GdchCredentials fromStream( - InputStream credentialsStream, HttpTransportFactory transportFactory) throws IOException { - Preconditions.checkNotNull(transportFactory); - GenericJson fileContents = parseJsonInputStream(credentialsStream); - String fileType = extractFromJson(fileContents, "type"); - if (fileType.equals(GoogleCredentialsInfo.GDCH_CREDENTIALS.getFileType())) { - return fromJson(fileContents, transportFactory); - } - - throw new IOException( - String.format( - "Error reading credentials from stream, 'type' value '%s' not recognized." - + " Expecting '%s'.", - fileType, GoogleCredentialsInfo.GDCH_CREDENTIALS.getFileType())); } /** @@ -167,27 +96,27 @@ static GdchCredentials fromJson(Map json) throws IOException { /** * Create GDCH service account credentials defined by JSON. * - * @param json a map from the JSON representing the credentials. - * @param transportFactory HTTP transport factory, creates the transport used to get access - * tokens. + * @param json a map from the JSON representing the credentials. + * @param transportFactory HTTP transport factory, creates the transport used to + * get access + * tokens. * @return the GDCH service account credentials defined by the JSON. * @throws IOException if the credential cannot be created from the JSON. */ @VisibleForTesting static GdchCredentials fromJson(Map json, HttpTransportFactory transportFactory) - throws IOException { + throws IOException { String formatVersion = validateField((String) json.get("format_version"), "format_version"); String projectId = validateField((String) json.get("project"), "project"); String privateKeyId = validateField((String) json.get("private_key_id"), "private_key_id"); String privateKeyPkcs8 = validateField((String) json.get("private_key"), "private_key"); String serviceIdentityName = validateField((String) json.get("name"), "name"); - String tokenServerUriStringFromCreds = - validateField((String) json.get("token_uri"), "token_uri"); + String tokenServerUriStringFromCreds = validateField((String) json.get("token_uri"), "token_uri"); String caCertPath = (String) json.get("ca_cert_path"); if (!SUPPORTED_FORMAT_VERSION.equals(formatVersion)) { throw new IOException( - String.format("Only format version %s is supported.", SUPPORTED_FORMAT_VERSION)); + String.format("Only format version %s is supported.", SUPPORTED_FORMAT_VERSION)); } URI tokenServerUriFromCreds = null; @@ -197,8 +126,7 @@ static GdchCredentials fromJson(Map json, HttpTransportFactory t throw new IOException("Token server URI specified in 'token_uri' could not be parsed."); } - GdchCredentials.Builder builder = - GdchCredentials.newBuilder() + GdchCredentials.Builder builder = GdchCredentials.newBuilder() .setProjectId(projectId) .setPrivateKeyId(privateKeyId) .setTokenServerUri(tokenServerUriFromCreds) @@ -212,13 +140,14 @@ static GdchCredentials fromJson(Map json, HttpTransportFactory t /** * Internal constructor. * - * @param privateKeyPkcs8 RSA private key object for the service account in PKCS#8 format. - * @param builder A builder for GdchCredentials. + * @param privateKeyPkcs8 RSA private key object for the service account in + * PKCS#8 format. + * @param builder A builder for GdchCredentials. * @return an instance of GdchCredentials. */ static GdchCredentials fromPkcs8(String privateKeyPkcs8, GdchCredentials.Builder builder) - throws IOException { - PrivateKey privateKey = OAuth2Utils.privateKeyFromPkcs8(privateKeyPkcs8); + throws IOException { + PrivateKey privateKey = OAuth2Utils.privateKeyFromPkcs8(privateKeyPkcs8, "EC"); builder.setPrivateKey(privateKey); return new GdchCredentials(builder); @@ -229,17 +158,21 @@ static GdchCredentials fromPkcs8(String privateKeyPkcs8, GdchCredentials.Builder * * @param apiAudience The intended audience for GDCH credentials. */ - public GdchCredentials createWithGdchAudience(URI apiAudience) throws IOException { + public GdchCredentials createWithGdchAudience(String apiAudience) throws IOException { Preconditions.checkNotNull( apiAudience, "Audience are not configured for GDCH service account credentials."); return this.toBuilder().setGdchAudience(apiAudience).build(); } /** - * Refresh the OAuth2 access token by getting a new access token using a JSON Web Token (JWT). + * Refresh the OAuth2 access token by getting a new access token using a JSON + * Web Token (JWT). * - *

For GDCH credentials, this class creates a self-signed JWT, and sends to the GDCH - * authentication endpoint (tokenServerUri) to exchange an access token for the intended api + *

+ * For GDCH credentials, this class creates a self-signed JWT, and sends to the + * GDCH + * authentication endpoint (tokenServerUri) to exchange an access token for the + * intended api * audience (apiAudience). */ @Override @@ -249,14 +182,19 @@ public AccessToken refreshAccessToken() throws IOException { "Audience are not configured for GDCH service account. Specify the " + "audience by calling createWithGDCHAudience."); - JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY; - long currentTime = clock.currentTimeMillis(); - String assertion = createAssertion(jsonFactory, currentTime, getApiAudience()); + JsonFactory jsonFactory = GsonFactory.getDefaultInstance(); + + long currentTime = Clock.SYSTEM.currentTimeMillis(); + String assertion = createAssertion(jsonFactory, currentTime, apiAudience); GenericData tokenRequest = new GenericData(); - tokenRequest.set("grant_type", OAuth2Utils.TOKEN_TYPE_TOKEN_EXCHANGE); - tokenRequest.set("assertion", assertion); - UrlEncodedContent content = new UrlEncodedContent(tokenRequest); + tokenRequest.set("audience", apiAudience); + tokenRequest.set("grant_type", TOKEN_TYPE_TOKEN_EXCHANGE); + tokenRequest.set("requested_token_type", ACCESS_TOKEN_TYPE); + tokenRequest.set("subject_token", assertion); + tokenRequest.set("subject_token_type", SERVICE_ACCOUNT_TOKEN_TYPE); + + JsonHttpContent content = new JsonHttpContent(jsonFactory, tokenRequest); HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory(); HttpRequest request = requestFactory.buildPostRequest(new GenericUrl(tokenServerUri), content); @@ -270,32 +208,33 @@ public AccessToken refreshAccessToken() throws IOException { response = request.execute(); } catch (HttpResponseException re) { String message = String.format(errorTemplate, re.getMessage(), getServiceIdentityName()); - throw GoogleAuthException.createWithTokenEndpointResponseException(re, message); + throw new IOException(message, re); } catch (IOException e) { - throw GoogleAuthException.createWithTokenEndpointIOException( - e, String.format(errorTemplate, e.getMessage(), getServiceIdentityName())); + String message = String.format(errorTemplate, e.getMessage(), getServiceIdentityName()); + throw new IOException(message, e); } GenericData responseData = response.parseAs(GenericData.class); - String accessToken = - OAuth2Utils.validateString(responseData, "access_token", PARSE_ERROR_PREFIX); - int expiresInSeconds = - OAuth2Utils.validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX); - long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000L; + String accessToken = validateString(responseData, "access_token", PARSE_ERROR_PREFIX); + int expiresInSeconds = validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX); + long expiresAtMilliseconds = Clock.SYSTEM.currentTimeMillis() + expiresInSeconds * 1000L; return new AccessToken(accessToken, new Date(expiresAtMilliseconds)); } /** * Create a self-signed JWT for GDCH authentication flow. * - *

The self-signed JWT is used to exchange access token from GDCH authentication - * (tokenServerUri), not for API call. It uses the serviceIdentityName as the `iss` and `sub` - * claim, and the tokenServerUri as the `aud` claim. The JWT is signed with the privateKey. + *

+ * The self-signed JWT is used to exchange access token from GDCH authentication + * (tokenServerUri), not for API call. It uses the serviceIdentityName as the + * `iss` and `sub` + * claim, and the tokenServerUri as the `aud` claim. The JWT is signed with the + * privateKey. */ - String createAssertion(JsonFactory jsonFactory, long currentTime, URI apiAudience) - throws IOException { + String createAssertion(JsonFactory jsonFactory, long currentTime, String apiAudience) + throws IOException { JsonWebSignature.Header header = new JsonWebSignature.Header(); - header.setAlgorithm("RS256"); + header.setAlgorithm("ES256"); header.setType("JWT"); header.setKeyId(privateKeyId); @@ -304,15 +243,14 @@ String createAssertion(JsonFactory jsonFactory, long currentTime, URI apiAudienc payload.setSubject(getIssuerSubjectValue(projectId, serviceIdentityName)); payload.setIssuedAtTimeSeconds(currentTime / 1000); payload.setExpirationTimeSeconds(currentTime / 1000 + this.lifetime); - payload.setAudience(getTokenServerUri().toString()); + payload.setAudience(tokenServerUri.toString()); String assertion; try { - payload.set("api_audience", apiAudience.toString()); - assertion = JsonWebSignature.signUsingRsaSha256(privateKey, jsonFactory, header, payload); + assertion = signUsingEsSha256(privateKey, jsonFactory, header, payload); } catch (GeneralSecurityException e) { throw new IOException( - "Error signing service account access token request with private key.", e); + "Error signing service account access token request with private key.", e); } return assertion; @@ -321,7 +259,9 @@ String createAssertion(JsonFactory jsonFactory, long currentTime, URI apiAudienc /** * Get the issuer and subject value in the format GDCH token server required. * - *

This value is specific to GDCH and combined parameter used for both `iss` and `sub` fields + *

+ * This value is specific to GDCH and combined parameter used for both `iss` and + * `sub` fields * in JWT claim. */ @VisibleForTesting @@ -329,10 +269,6 @@ static String getIssuerSubjectValue(String projectId, String serviceIdentityName return String.format("system:serviceaccount:%s:%s", projectId, serviceIdentityName); } - /** - * @return the projectId set in the GDCH SA Key file or the user set projectId - */ - @Override public final String getProjectId() { return projectId; } @@ -353,7 +289,7 @@ public final URI getTokenServerUri() { return tokenServerUri; } - public final URI getApiAudience() { + public final String getApiAudience() { return apiAudience; } @@ -384,29 +320,29 @@ private void readObject(ObjectInputStream input) throws IOException, ClassNotFou @Override public int hashCode() { return Objects.hash( - projectId, - privateKeyId, - privateKey, - serviceIdentityName, - tokenServerUri, - transportFactoryClassName, - apiAudience, - caCertPath, - lifetime); + projectId, + privateKeyId, + privateKey, + serviceIdentityName, + tokenServerUri, + transportFactoryClassName, + apiAudience, + caCertPath, + lifetime); } @Override public String toString() { return MoreObjects.toStringHelper(this) - .add("projectId", projectId) - .add("privateKeyId", privateKeyId) - .add("serviceIdentityName", serviceIdentityName) - .add("tokenServerUri", tokenServerUri) - .add("transportFactoryClassName", transportFactoryClassName) - .add("caCertPath", caCertPath) - .add("apiAudience", apiAudience) - .add("lifetime", lifetime) - .toString(); + .add("projectId", projectId) + .add("privateKeyId", privateKeyId) + .add("serviceIdentityName", serviceIdentityName) + .add("tokenServerUri", tokenServerUri) + .add("transportFactoryClassName", transportFactoryClassName) + .add("caCertPath", caCertPath) + .add("apiAudience", apiAudience) + .add("lifetime", lifetime) + .toString(); } @Override @@ -416,14 +352,14 @@ public boolean equals(Object obj) { } GdchCredentials other = (GdchCredentials) obj; return Objects.equals(this.projectId, other.projectId) - && Objects.equals(this.privateKeyId, other.privateKeyId) - && Objects.equals(this.privateKey, other.privateKey) - && Objects.equals(this.serviceIdentityName, other.serviceIdentityName) - && Objects.equals(this.tokenServerUri, other.tokenServerUri) - && Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName) - && Objects.equals(this.apiAudience, other.apiAudience) - && Objects.equals(this.caCertPath, other.caCertPath) - && Objects.equals(this.lifetime, other.lifetime); + && Objects.equals(this.privateKeyId, other.privateKeyId) + && Objects.equals(this.privateKey, other.privateKey) + && Objects.equals(this.serviceIdentityName, other.serviceIdentityName) + && Objects.equals(this.tokenServerUri, other.tokenServerUri) + && Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName) + && Objects.equals(this.apiAudience, other.apiAudience) + && Objects.equals(this.caCertPath, other.caCertPath) + && Objects.equals(this.lifetime, other.lifetime); } static InputStream readStream(File file) throws FileNotFoundException { @@ -436,12 +372,13 @@ public static class Builder extends GoogleCredentials.Builder { private PrivateKey privateKey; private String serviceIdentityName; private URI tokenServerUri; - private URI apiAudience; + private String apiAudience; private HttpTransportFactory transportFactory; private String caCertPath; private int lifetime = DEFAULT_LIFETIME_IN_SECONDS; - protected Builder() {} + protected Builder() { + } protected Builder(GdchCredentials credentials) { this.projectId = credentials.projectId; @@ -497,7 +434,7 @@ public Builder setCaCertPath(String caCertPath) { } @CanIgnoreReturnValue - public Builder setGdchAudience(URI apiAudience) { + public Builder setGdchAudience(String apiAudience) { this.apiAudience = apiAudience; return this; } @@ -543,9 +480,9 @@ public GdchCredentials build() { private static String validateField(String field, String fieldName) throws IOException { if (field == null || field.isEmpty()) { throw new IOException( - String.format( - "Error reading GDCH service account credential from JSON, %s is misconfigured.", - fieldName)); + String.format( + "Error reading GDCH service account credential from JSON, %s is misconfigured.", + fieldName)); } return field; } @@ -553,13 +490,16 @@ private static String validateField(String field, String fieldName) throws IOExc /* * Internal HttpTransportFactory for GDCH credentials. * - *

GDCH authentication server could use a self-signed certificate, thus the client could + *

GDCH authentication server could use a self-signed certificate, thus the + * client could * provide the CA certificate path through the `ca_cert_path` in GDCH JSON file. * - *

The TransportFactoryForGdch subclass would read the certificate and create a trust store, + *

The TransportFactoryForGdch subclass would read the certificate and + * create a trust store, * then use the trust store to create a transport. * - *

If the GDCH authentication server uses well known CA certificate, then a regular transport + *

If the GDCH authentication server uses well known CA certificate, then a + * regular transport * would be set. */ static class TransportFactoryForGdch implements HttpTransportFactory { @@ -581,17 +521,122 @@ private void setTransport(String caCertPath) throws IOException { } try { InputStream certificateStream = readStream(new File(caCertPath)); - this.transport = - new NetHttpTransport.Builder().trustCertificatesFromStream(certificateStream).build(); + this.transport = new NetHttpTransport.Builder().trustCertificatesFromStream(certificateStream).build(); } catch (IOException e) { throw new IOException( - String.format( - "Error reading certificate file from CA cert path, value '%s': %s", - caCertPath, e.getMessage()), - e); + String.format( + "Error reading certificate file from CA cert path, value '%s': %s", + caCertPath, e.getMessage()), + e); } catch (GeneralSecurityException e) { throw new IOException("Error initiating transport with certificate stream.", e); } } } -} + + /** Return the specified string from JSON or throw a helpful error message. */ + private static String validateString(Map map, String key, String errorPrefix) + throws IOException { + Object value = map.get(key); + if (value == null) { + throw new IOException(String.format(VALUE_NOT_FOUND_MESSAGE, errorPrefix, key)); + } + if (!(value instanceof String)) { + throw new IOException(String.format(VALUE_WRONG_TYPE_MESSAGE, errorPrefix, "string", key)); + } + return (String) value; + } + + private static int validateInt32(Map map, String key, String errorPrefix) + throws IOException { + Object value = map.get(key); + if (value == null) { + throw new IOException(String.format(VALUE_NOT_FOUND_MESSAGE, errorPrefix, key)); + } + if (value instanceof BigDecimal) { + BigDecimal bigDecimalValue = (BigDecimal) value; + return bigDecimalValue.intValueExact(); + } + if (!(value instanceof Integer)) { + throw new IOException(String.format(VALUE_WRONG_TYPE_MESSAGE, errorPrefix, "integer", key)); + } + return (Integer) value; + } + + private static String signUsingEsSha256( + PrivateKey privateKey, + JsonFactory jsonFactory, + JsonWebSignature.Header header, + JsonWebToken.Payload payload) + throws GeneralSecurityException, IOException { + String content = Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(header)) + + "." + + Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(payload)); + byte[] contentBytes = StringUtils.getBytesUtf8(content); + byte[] signature = SecurityUtils.sign( + SecurityUtils.getEs256SignatureAlgorithm(), privateKey, contentBytes); + + // The JCA returns a DER-encoded signature, but JWS needs the concatenated R|S + // format. + // We need to transcode it. For ES256, the output length is 64 bytes. + byte[] jwsSignature = transcodeDerToConcat(signature, 64); + return content + "." + Base64.encodeBase64URLSafeString(jwsSignature); + } + + private static byte[] transcodeDerToConcat(byte[] derSignature, int outputLength) + throws IOException { + if (derSignature.length < 8 || derSignature[0] != 0x30) { + throw new IOException("Invalid DER signature format."); + } + + int offset = 2; + int seqLength = derSignature[1] & 0xFF; + if (seqLength == 0x81) { + offset = 3; + seqLength = derSignature[2] & 0xFF; + } + + if (derSignature.length - offset != seqLength) { + throw new IOException("Invalid DER signature length."); + } + + // R + if (derSignature[offset++] != 0x02) { + throw new IOException("Expected INTEGER for R."); + } + int rLength = derSignature[offset++]; + if (derSignature[offset] == 0x00 && rLength > 1 && (derSignature[offset + 1] & 0x80) != 0) { + offset++; + rLength--; + } + byte[] r = new byte[rLength]; + System.arraycopy(derSignature, offset, r, 0, rLength); + offset += rLength; + + // S + if (derSignature[offset++] != 0x02) { + throw new IOException("Expected INTEGER for S."); + } + int sLength = derSignature[offset++]; + if (derSignature[offset] == 0x00 && sLength > 1 && (derSignature[offset + 1] & 0x80) != 0) { + offset++; + sLength--; + } + byte[] s = new byte[sLength]; + System.arraycopy(derSignature, offset, s, 0, sLength); + + int keySizeBytes = outputLength / 2; + if (r.length > keySizeBytes || s.length > keySizeBytes) { + throw new IOException( + String.format( + "Invalid R or S length. R: %d, S: %d, Expected: %d", + r.length, s.length, keySizeBytes)); + } + + byte[] result = new byte[outputLength]; + System.arraycopy(r, 0, result, keySizeBytes - r.length, r.length); + System.arraycopy(s, 0, result, outputLength - s.length, s.length); + + return result; + } +} \ No newline at end of file diff --git a/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java b/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java index 21278e8b6..a61f64551 100644 --- a/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java +++ b/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java @@ -267,6 +267,20 @@ static Map validateMap(Map map, String key, Stri * key creation. */ public static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8) throws IOException { + return privateKeyFromPkcs8(privateKeyPkcs8, "RSA"); + } + + /** + * Converts a PKCS#8 string to a private key of the specified algorithm. + * + * @param privateKeyPkcs8 the PKCS#8 string. + * @param algorithm the algorithm of the private key. + * @return the private key. + * @throws IOException if the PKCS#8 data is invalid or if an unexpected exception occurs during + * key creation. + */ + public static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8, String algorithm) + throws IOException { Reader reader = new StringReader(privateKeyPkcs8); Section section = PemReader.readFirstSectionAndClose(reader, "PRIVATE KEY"); if (section == null) { @@ -276,7 +290,7 @@ public static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8) throws IOEx PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes); Exception unexpectedException; try { - KeyFactory keyFactory = SecurityUtils.getRsaKeyFactory(); + KeyFactory keyFactory = KeyFactory.getInstance(algorithm); return keyFactory.generatePrivate(keySpec); } catch (NoSuchAlgorithmException | InvalidKeySpecException exception) { unexpectedException = exception; From 0c5e42375ec110106e0bf03cc19fc8f621b19f55 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Wed, 4 Mar 2026 16:48:26 -0500 Subject: [PATCH 02/24] test: partially adapt tests --- .../DefaultCredentialsProviderTest.java | 2 +- .../auth/oauth2/GdchCredentialsTest.java | 21 +++++++------------ .../auth/oauth2/GoogleCredentialsTest.java | 2 +- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java b/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java index 1852629ee..16e4b57c9 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java @@ -94,7 +94,7 @@ class DefaultCredentialsProviderTest { private static final String GDCH_SA_CA_CERT_FILE_NAME = "cert.pem"; private static final String GDCH_SA_CA_CERT_PATH = GdchCredentialsTest.class.getClassLoader().getResource(GDCH_SA_CA_CERT_FILE_NAME).getPath(); - private static final URI GDCH_SA_API_AUDIENCE = URI.create("https://gdch-api-audience"); + private static final String GDCH_SA_API_AUDIENCE = "https://gdch-api-audience"; private static final Collection SCOPES = Collections.singletonList("dummy.scope"); private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo"); private static final String QUOTA_PROJECT = "sample-quota-project-id"; diff --git a/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java index b55514916..6f797b166 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java @@ -61,18 +61,11 @@ class GdchCredentialsTest extends BaseSerializationTest { private static final String FORMAT_VERSION = GdchCredentials.SUPPORTED_FORMAT_VERSION; private static final String PRIVATE_KEY_ID = "d84a4fefcf50791d4a90f2d7af17469d6282df9d"; static final String PRIVATE_KEY_PKCS8 = - "-----BEGIN PRIVATE KEY-----\n" - + "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALX0PQoe1igW12i" - + "kv1bN/r9lN749y2ijmbc/mFHPyS3hNTyOCjDvBbXYbDhQJzWVUikh4mvGBA07qTj79Xc3yBDfKP2IeyYQIFe0t0" - + "zkd7R9Zdn98Y2rIQC47aAbDfubtkU1U72t4zL11kHvoa0/RuFZjncvlr42X7be7lYh4p3NAgMBAAECgYASk5wDw" - + "4Az2ZkmeuN6Fk/y9H+Lcb2pskJIXjrL533vrDWGOC48LrsThMQPv8cxBky8HFSEklPpkfTF95tpD43iVwJRB/Gr" - + "CtGTw65IfJ4/tI09h6zGc4yqvIo1cHX/LQ+SxKLGyir/dQM925rGt/VojxY5ryJR7GLbCzxPnJm/oQJBANwOCO6" - + "D2hy1LQYJhXh7O+RLtA/tSnT1xyMQsGT+uUCMiKS2bSKx2wxo9k7h3OegNJIu1q6nZ6AbxDK8H3+d0dUCQQDTrP" - + "SXagBxzp8PecbaCHjzNRSQE2in81qYnrAFNB4o3DpHyMMY6s5ALLeHKscEWnqP8Ur6X4PvzZecCWU9BKAZAkAut" - + "LPknAuxSCsUOvUfS1i87ex77Ot+w6POp34pEX+UWb+u5iFn2cQacDTHLV1LtE80L8jVLSbrbrlH43H0DjU5AkEA" - + "gidhycxS86dxpEljnOMCw8CKoUBd5I880IUahEiUltk7OLJYS/Ts1wbn3kPOVX3wyJs8WBDtBkFrDHW2ezth2QJ" - + "ADj3e1YhMVdjJW5jqwlD/VNddGjgzyunmiZg0uOXsHXbytYmsA545S8KRQFaJKFXYYFo2kOjqOiC1T2cAzMDjCQ" - + "==\n-----END PRIVATE KEY-----\n"; + "-----BEGIN PRIVATE KEY-----\n" + + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgyITXsUvRm1C3lnyz\n" + + "OaMY7TNXZois4NH0bkMwqTAnVbqhRANCAASk5+U9skHVTo+sEVd2/yKY7A2eYn8K\n" + + "Cygd3bQalfWs533aTu93XwVx0YNN310aFquv3/VIiFofm1JEBAhUiG8e\n" + + "-----END PRIVATE KEY-----"; private static final String PROJECT_ID = "project-id"; private static final String SERVICE_IDENTITY_NAME = "service-identity-name"; private static final String ACCESS_TOKEN = "1/MkSJoj1xsli0AccessToken_NKPY2"; @@ -81,7 +74,7 @@ class GdchCredentialsTest extends BaseSerializationTest { private static final String CA_CERT_FILE_NAME = "cert.pem"; private static final String CA_CERT_PATH = GdchCredentialsTest.class.getClassLoader().getResource(CA_CERT_FILE_NAME).getPath(); - private static final URI API_AUDIENCE = URI.create("https://gdch-api-audience"); + private static final String API_AUDIENCE = URI.create("https://gdch-api-audience").toString(); private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo"); @Test @@ -771,7 +764,7 @@ void equals_false_tokenServer() throws IOException { @Test void equals_false_apiAudience() throws IOException { - URI otherApiAudience = URI.create("https://foo1.com/bar"); + String otherApiAudience = URI.create("https://foo1.com/bar").toString(); GenericJson json = writeGdchServiceAccountJson( diff --git a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java index 503c87d54..214932d53 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java @@ -82,7 +82,7 @@ class GoogleCredentialsTest extends BaseSerializationTest { private static final String GDCH_SA_CA_CERT_FILE_NAME = "cert.pem"; private static final String GDCH_SA_CA_CERT_PATH = GdchCredentialsTest.class.getClassLoader().getResource(GDCH_SA_CA_CERT_FILE_NAME).getPath(); - private static final URI GDCH_API_AUDIENCE = URI.create("https://gdch-api-audience"); + private static final String GDCH_API_AUDIENCE = "https://gdch-api-audience"; private static final String USER_CLIENT_SECRET = "jakuaL9YyieakhECKL2SwZcu"; private static final String USER_CLIENT_ID = "ya29.1.AADtN_UtlxN3PuGAxrN2XQnZTVRvDyVWnYq4I6dws"; private static final String REFRESH_TOKEN = "1/Tl6awhpFjkMkSJoj1xsli0H2eL5YsMgU_NKPY2TyGWY"; From 5851b3e22147e82099453e0ec957e5f1fb20ac38 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Wed, 4 Mar 2026 17:06:31 -0500 Subject: [PATCH 03/24] test: finish adjusting tests --- .../google/auth/oauth2/GdchCredentials.java | 2 +- .../javatests/com/google/auth/TestUtils.java | 20 +++++++++++++++++++ .../auth/oauth2/MockTokenServerTransport.java | 14 +++++++++---- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java index 077f3a618..ea7f34faa 100644 --- a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java @@ -217,7 +217,7 @@ public AccessToken refreshAccessToken() throws IOException { GenericData responseData = response.parseAs(GenericData.class); String accessToken = validateString(responseData, "access_token", PARSE_ERROR_PREFIX); int expiresInSeconds = validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX); - long expiresAtMilliseconds = Clock.SYSTEM.currentTimeMillis() + expiresInSeconds * 1000L; + long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000L; return new AccessToken(accessToken, new Date(expiresAtMilliseconds)); } diff --git a/oauth2_http/javatests/com/google/auth/TestUtils.java b/oauth2_http/javatests/com/google/auth/TestUtils.java index d794ba184..332961485 100644 --- a/oauth2_http/javatests/com/google/auth/TestUtils.java +++ b/oauth2_http/javatests/com/google/auth/TestUtils.java @@ -116,6 +116,26 @@ public static Map parseQuery(String query) throws IOException { return map; } + /** + * Parses the request body as either JSON or a query string. + * + * @param content The request body content. + * @return A map of the parsed parameters. + * @throws IOException If the content cannot be parsed. + */ + public static Map parseBody(String content) throws IOException { + if (content != null && content.trim().startsWith("{")) { + GenericJson json = JSON_FACTORY.fromString(content, GenericJson.class); + Map map = new HashMap<>(); + for (Map.Entry entry : json.entrySet()) { + Object value = entry.getValue(); + map.put(entry.getKey(), value == null ? null : value.toString()); + } + return map; + } + return parseQuery(content); + } + public static String errorJson(String message) throws IOException { GenericJson errorResponse = new GenericJson(); errorResponse.setFactory(JSON_FACTORY); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockTokenServerTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockTokenServerTransport.java index a61c185b5..0e32c9a2c 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/MockTokenServerTransport.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/MockTokenServerTransport.java @@ -213,7 +213,7 @@ public LowLevelHttpResponse execute() throws IOException { } String content = this.getContentAsString(); - Map query = TestUtils.parseQuery(content); + Map query = TestUtils.parseBody(content); String accessToken = null; String refreshToken = null; String grantedScopesString = null; @@ -255,6 +255,9 @@ public LowLevelHttpResponse execute() throws IOException { } else if (query.containsKey("grant_type")) { String grantType = query.get("grant_type"); String assertion = query.get("assertion"); + if (assertion == null) { + assertion = query.get("subject_token"); + } JsonWebSignature signature = JsonWebSignature.parse(JSON_FACTORY, assertion); if (OAuth2Utils.GRANT_TYPE_JWT_BEARER.equals(grantType)) { String foundEmail = signature.getPayload().getIssuer(); @@ -284,7 +287,10 @@ public LowLevelHttpResponse execute() throws IOException { "GDCH Service Account Service Identity Name not found as issuer."); } accessToken = gdchServiceAccounts.get(foundServiceIdentityName); - String foundApiAudience = (String) signature.getPayload().get("api_audience"); + String foundApiAudience = query.get("audience"); + if (foundApiAudience == null || foundApiAudience.isEmpty()) { + foundApiAudience = (String) signature.getPayload().get("api_audience"); + } if ((foundApiAudience == null || foundApiAudience.length() == 0)) { throw new IOException("Api_audience must be specified."); } @@ -326,7 +332,7 @@ public LowLevelHttpResponse execute() throws IOException { new MockLowLevelHttpRequest(url) { @Override public LowLevelHttpResponse execute() throws IOException { - Map parameters = TestUtils.parseQuery(this.getContentAsString()); + Map parameters = TestUtils.parseBody(this.getContentAsString()); String token = parameters.get("token"); if (token == null) { throw new IOException("Token to revoke not found."); @@ -358,7 +364,7 @@ public LowLevelHttpResponse execute() throws IOException { } String content = this.getContentAsString(); - Map query = TestUtils.parseQuery(content); + Map query = TestUtils.parseBody(content); // Validate required fields. if (!query.containsKey("code") From 258ac9d7675074559b7fb9fcc92655f679a3997f Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Wed, 4 Mar 2026 17:07:03 -0500 Subject: [PATCH 04/24] chore: format --- .../google/auth/oauth2/GdchCredentials.java | 163 +++++++++--------- .../com/google/auth/oauth2/OAuth2Utils.java | 1 - .../auth/oauth2/GdchCredentialsTest.java | 10 +- 3 files changed, 83 insertions(+), 91 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java index ea7f34faa..2552bee31 100644 --- a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java @@ -45,8 +45,10 @@ public class GdchCredentials extends GoogleCredentials { @VisibleForTesting static final String SUPPORTED_FORMAT_VERSION = "1"; private static final String ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"; - private static final String SERVICE_ACCOUNT_TOKEN_TYPE = "urn:k8s:params:oauth:token-type:serviceaccount"; - private static final String TOKEN_TYPE_TOKEN_EXCHANGE = "urn:ietf:params:oauth:token-type:token-exchange"; + private static final String SERVICE_ACCOUNT_TOKEN_TYPE = + "urn:k8s:params:oauth:token-type:serviceaccount"; + private static final String TOKEN_TYPE_TOKEN_EXCHANGE = + "urn:ietf:params:oauth:token-type:token-exchange"; private static final int DEFAULT_LIFETIME_IN_SECONDS = 3600; @@ -64,8 +66,7 @@ public class GdchCredentials extends GoogleCredentials { /** * Internal constructor. * - * @param builder A builder for {@link GdchCredentials} See - * {@link GdchCredentials.Builder}. + * @param builder A builder for {@link GdchCredentials} See {@link GdchCredentials.Builder}. */ @VisibleForTesting GdchCredentials(GdchCredentials.Builder builder) { @@ -96,27 +97,27 @@ static GdchCredentials fromJson(Map json) throws IOException { /** * Create GDCH service account credentials defined by JSON. * - * @param json a map from the JSON representing the credentials. - * @param transportFactory HTTP transport factory, creates the transport used to - * get access - * tokens. + * @param json a map from the JSON representing the credentials. + * @param transportFactory HTTP transport factory, creates the transport used to get access + * tokens. * @return the GDCH service account credentials defined by the JSON. * @throws IOException if the credential cannot be created from the JSON. */ @VisibleForTesting static GdchCredentials fromJson(Map json, HttpTransportFactory transportFactory) - throws IOException { + throws IOException { String formatVersion = validateField((String) json.get("format_version"), "format_version"); String projectId = validateField((String) json.get("project"), "project"); String privateKeyId = validateField((String) json.get("private_key_id"), "private_key_id"); String privateKeyPkcs8 = validateField((String) json.get("private_key"), "private_key"); String serviceIdentityName = validateField((String) json.get("name"), "name"); - String tokenServerUriStringFromCreds = validateField((String) json.get("token_uri"), "token_uri"); + String tokenServerUriStringFromCreds = + validateField((String) json.get("token_uri"), "token_uri"); String caCertPath = (String) json.get("ca_cert_path"); if (!SUPPORTED_FORMAT_VERSION.equals(formatVersion)) { throw new IOException( - String.format("Only format version %s is supported.", SUPPORTED_FORMAT_VERSION)); + String.format("Only format version %s is supported.", SUPPORTED_FORMAT_VERSION)); } URI tokenServerUriFromCreds = null; @@ -126,7 +127,8 @@ static GdchCredentials fromJson(Map json, HttpTransportFactory t throw new IOException("Token server URI specified in 'token_uri' could not be parsed."); } - GdchCredentials.Builder builder = GdchCredentials.newBuilder() + GdchCredentials.Builder builder = + GdchCredentials.newBuilder() .setProjectId(projectId) .setPrivateKeyId(privateKeyId) .setTokenServerUri(tokenServerUriFromCreds) @@ -140,13 +142,12 @@ static GdchCredentials fromJson(Map json, HttpTransportFactory t /** * Internal constructor. * - * @param privateKeyPkcs8 RSA private key object for the service account in - * PKCS#8 format. - * @param builder A builder for GdchCredentials. + * @param privateKeyPkcs8 RSA private key object for the service account in PKCS#8 format. + * @param builder A builder for GdchCredentials. * @return an instance of GdchCredentials. */ static GdchCredentials fromPkcs8(String privateKeyPkcs8, GdchCredentials.Builder builder) - throws IOException { + throws IOException { PrivateKey privateKey = OAuth2Utils.privateKeyFromPkcs8(privateKeyPkcs8, "EC"); builder.setPrivateKey(privateKey); @@ -165,14 +166,10 @@ public GdchCredentials createWithGdchAudience(String apiAudience) throws IOExcep } /** - * Refresh the OAuth2 access token by getting a new access token using a JSON - * Web Token (JWT). + * Refresh the OAuth2 access token by getting a new access token using a JSON Web Token (JWT). * - *

- * For GDCH credentials, this class creates a self-signed JWT, and sends to the - * GDCH - * authentication endpoint (tokenServerUri) to exchange an access token for the - * intended api + *

For GDCH credentials, this class creates a self-signed JWT, and sends to the GDCH + * authentication endpoint (tokenServerUri) to exchange an access token for the intended api * audience (apiAudience). */ @Override @@ -224,15 +221,12 @@ public AccessToken refreshAccessToken() throws IOException { /** * Create a self-signed JWT for GDCH authentication flow. * - *

- * The self-signed JWT is used to exchange access token from GDCH authentication - * (tokenServerUri), not for API call. It uses the serviceIdentityName as the - * `iss` and `sub` - * claim, and the tokenServerUri as the `aud` claim. The JWT is signed with the - * privateKey. + *

The self-signed JWT is used to exchange access token from GDCH authentication + * (tokenServerUri), not for API call. It uses the serviceIdentityName as the `iss` and `sub` + * claim, and the tokenServerUri as the `aud` claim. The JWT is signed with the privateKey. */ String createAssertion(JsonFactory jsonFactory, long currentTime, String apiAudience) - throws IOException { + throws IOException { JsonWebSignature.Header header = new JsonWebSignature.Header(); header.setAlgorithm("ES256"); header.setType("JWT"); @@ -250,7 +244,7 @@ String createAssertion(JsonFactory jsonFactory, long currentTime, String apiAudi assertion = signUsingEsSha256(privateKey, jsonFactory, header, payload); } catch (GeneralSecurityException e) { throw new IOException( - "Error signing service account access token request with private key.", e); + "Error signing service account access token request with private key.", e); } return assertion; @@ -259,9 +253,7 @@ String createAssertion(JsonFactory jsonFactory, long currentTime, String apiAudi /** * Get the issuer and subject value in the format GDCH token server required. * - *

- * This value is specific to GDCH and combined parameter used for both `iss` and - * `sub` fields + *

This value is specific to GDCH and combined parameter used for both `iss` and `sub` fields * in JWT claim. */ @VisibleForTesting @@ -320,29 +312,29 @@ private void readObject(ObjectInputStream input) throws IOException, ClassNotFou @Override public int hashCode() { return Objects.hash( - projectId, - privateKeyId, - privateKey, - serviceIdentityName, - tokenServerUri, - transportFactoryClassName, - apiAudience, - caCertPath, - lifetime); + projectId, + privateKeyId, + privateKey, + serviceIdentityName, + tokenServerUri, + transportFactoryClassName, + apiAudience, + caCertPath, + lifetime); } @Override public String toString() { return MoreObjects.toStringHelper(this) - .add("projectId", projectId) - .add("privateKeyId", privateKeyId) - .add("serviceIdentityName", serviceIdentityName) - .add("tokenServerUri", tokenServerUri) - .add("transportFactoryClassName", transportFactoryClassName) - .add("caCertPath", caCertPath) - .add("apiAudience", apiAudience) - .add("lifetime", lifetime) - .toString(); + .add("projectId", projectId) + .add("privateKeyId", privateKeyId) + .add("serviceIdentityName", serviceIdentityName) + .add("tokenServerUri", tokenServerUri) + .add("transportFactoryClassName", transportFactoryClassName) + .add("caCertPath", caCertPath) + .add("apiAudience", apiAudience) + .add("lifetime", lifetime) + .toString(); } @Override @@ -352,14 +344,14 @@ public boolean equals(Object obj) { } GdchCredentials other = (GdchCredentials) obj; return Objects.equals(this.projectId, other.projectId) - && Objects.equals(this.privateKeyId, other.privateKeyId) - && Objects.equals(this.privateKey, other.privateKey) - && Objects.equals(this.serviceIdentityName, other.serviceIdentityName) - && Objects.equals(this.tokenServerUri, other.tokenServerUri) - && Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName) - && Objects.equals(this.apiAudience, other.apiAudience) - && Objects.equals(this.caCertPath, other.caCertPath) - && Objects.equals(this.lifetime, other.lifetime); + && Objects.equals(this.privateKeyId, other.privateKeyId) + && Objects.equals(this.privateKey, other.privateKey) + && Objects.equals(this.serviceIdentityName, other.serviceIdentityName) + && Objects.equals(this.tokenServerUri, other.tokenServerUri) + && Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName) + && Objects.equals(this.apiAudience, other.apiAudience) + && Objects.equals(this.caCertPath, other.caCertPath) + && Objects.equals(this.lifetime, other.lifetime); } static InputStream readStream(File file) throws FileNotFoundException { @@ -377,8 +369,7 @@ public static class Builder extends GoogleCredentials.Builder { private String caCertPath; private int lifetime = DEFAULT_LIFETIME_IN_SECONDS; - protected Builder() { - } + protected Builder() {} protected Builder(GdchCredentials credentials) { this.projectId = credentials.projectId; @@ -480,9 +471,9 @@ public GdchCredentials build() { private static String validateField(String field, String fieldName) throws IOException { if (field == null || field.isEmpty()) { throw new IOException( - String.format( - "Error reading GDCH service account credential from JSON, %s is misconfigured.", - fieldName)); + String.format( + "Error reading GDCH service account credential from JSON, %s is misconfigured.", + fieldName)); } return field; } @@ -521,13 +512,14 @@ private void setTransport(String caCertPath) throws IOException { } try { InputStream certificateStream = readStream(new File(caCertPath)); - this.transport = new NetHttpTransport.Builder().trustCertificatesFromStream(certificateStream).build(); + this.transport = + new NetHttpTransport.Builder().trustCertificatesFromStream(certificateStream).build(); } catch (IOException e) { throw new IOException( - String.format( - "Error reading certificate file from CA cert path, value '%s': %s", - caCertPath, e.getMessage()), - e); + String.format( + "Error reading certificate file from CA cert path, value '%s': %s", + caCertPath, e.getMessage()), + e); } catch (GeneralSecurityException e) { throw new IOException("Error initiating transport with certificate stream.", e); } @@ -536,7 +528,7 @@ private void setTransport(String caCertPath) throws IOException { /** Return the specified string from JSON or throw a helpful error message. */ private static String validateString(Map map, String key, String errorPrefix) - throws IOException { + throws IOException { Object value = map.get(key); if (value == null) { throw new IOException(String.format(VALUE_NOT_FOUND_MESSAGE, errorPrefix, key)); @@ -548,7 +540,7 @@ private static String validateString(Map map, String key, String } private static int validateInt32(Map map, String key, String errorPrefix) - throws IOException { + throws IOException { Object value = map.get(key); if (value == null) { throw new IOException(String.format(VALUE_NOT_FOUND_MESSAGE, errorPrefix, key)); @@ -564,17 +556,18 @@ private static int validateInt32(Map map, String key, String err } private static String signUsingEsSha256( - PrivateKey privateKey, - JsonFactory jsonFactory, - JsonWebSignature.Header header, - JsonWebToken.Payload payload) - throws GeneralSecurityException, IOException { - String content = Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(header)) + PrivateKey privateKey, + JsonFactory jsonFactory, + JsonWebSignature.Header header, + JsonWebToken.Payload payload) + throws GeneralSecurityException, IOException { + String content = + Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(header)) + "." + Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(payload)); byte[] contentBytes = StringUtils.getBytesUtf8(content); - byte[] signature = SecurityUtils.sign( - SecurityUtils.getEs256SignatureAlgorithm(), privateKey, contentBytes); + byte[] signature = + SecurityUtils.sign(SecurityUtils.getEs256SignatureAlgorithm(), privateKey, contentBytes); // The JCA returns a DER-encoded signature, but JWS needs the concatenated R|S // format. @@ -584,7 +577,7 @@ private static String signUsingEsSha256( } private static byte[] transcodeDerToConcat(byte[] derSignature, int outputLength) - throws IOException { + throws IOException { if (derSignature.length < 8 || derSignature[0] != 0x30) { throw new IOException("Invalid DER signature format."); } @@ -628,9 +621,9 @@ private static byte[] transcodeDerToConcat(byte[] derSignature, int outputLength int keySizeBytes = outputLength / 2; if (r.length > keySizeBytes || s.length > keySizeBytes) { throw new IOException( - String.format( - "Invalid R or S length. R: %d, S: %d, Expected: %d", - r.length, s.length, keySizeBytes)); + String.format( + "Invalid R or S length. R: %d, S: %d, Expected: %d", + r.length, s.length, keySizeBytes)); } byte[] result = new byte[outputLength]; @@ -639,4 +632,4 @@ private static byte[] transcodeDerToConcat(byte[] derSignature, int outputLength return result; } -} \ No newline at end of file +} diff --git a/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java b/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java index a61f64551..52db896b8 100644 --- a/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java +++ b/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java @@ -40,7 +40,6 @@ import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.util.PemReader; import com.google.api.client.util.PemReader.Section; -import com.google.api.client.util.SecurityUtils; import com.google.auth.http.AuthHttpConstants; import com.google.auth.http.HttpTransportFactory; import com.google.common.base.Strings; diff --git a/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java index 6f797b166..0eaa57dde 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java @@ -61,11 +61,11 @@ class GdchCredentialsTest extends BaseSerializationTest { private static final String FORMAT_VERSION = GdchCredentials.SUPPORTED_FORMAT_VERSION; private static final String PRIVATE_KEY_ID = "d84a4fefcf50791d4a90f2d7af17469d6282df9d"; static final String PRIVATE_KEY_PKCS8 = - "-----BEGIN PRIVATE KEY-----\n" + - "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgyITXsUvRm1C3lnyz\n" + - "OaMY7TNXZois4NH0bkMwqTAnVbqhRANCAASk5+U9skHVTo+sEVd2/yKY7A2eYn8K\n" + - "Cygd3bQalfWs533aTu93XwVx0YNN310aFquv3/VIiFofm1JEBAhUiG8e\n" + - "-----END PRIVATE KEY-----"; + "-----BEGIN PRIVATE KEY-----\n" + + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgyITXsUvRm1C3lnyz\n" + + "OaMY7TNXZois4NH0bkMwqTAnVbqhRANCAASk5+U9skHVTo+sEVd2/yKY7A2eYn8K\n" + + "Cygd3bQalfWs533aTu93XwVx0YNN310aFquv3/VIiFofm1JEBAhUiG8e\n" + + "-----END PRIVATE KEY-----"; private static final String PROJECT_ID = "project-id"; private static final String SERVICE_IDENTITY_NAME = "service-identity-name"; private static final String ACCESS_TOKEN = "1/MkSJoj1xsli0AccessToken_NKPY2"; From e8511bb49c383da9645eb7be15b2ccaa780cc7ed Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Wed, 4 Mar 2026 17:24:11 -0500 Subject: [PATCH 05/24] fix: restore credential name --- oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java index 2552bee31..2885c1014 100644 --- a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java @@ -80,6 +80,8 @@ public class GdchCredentials extends GoogleCredentials { this.caCertPath = builder.caCertPath; this.apiAudience = builder.apiAudience; this.lifetime = builder.lifetime; + this.name = GoogleCredentialsInfo.GDCH_CREDENTIALS.getCredentialName(); + } /** From a4d0f93f2955e0710288110a98900737a03b57e4 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Wed, 4 Mar 2026 17:24:24 -0500 Subject: [PATCH 06/24] docs: restore license --- .../google/auth/oauth2/GdchCredentials.java | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java index 2885c1014..a15279b36 100644 --- a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java @@ -1,3 +1,34 @@ +/* + * Copyright 2022, 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 com.google.api.client.http.GenericUrl; @@ -81,7 +112,6 @@ public class GdchCredentials extends GoogleCredentials { this.apiAudience = builder.apiAudience; this.lifetime = builder.lifetime; this.name = GoogleCredentialsInfo.GDCH_CREDENTIALS.getCredentialName(); - } /** From e3d5302bcb1be30edddd073879f03f910f5943e8 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Wed, 4 Mar 2026 17:27:55 -0500 Subject: [PATCH 07/24] fix: restore removed code --- .../google/auth/oauth2/GdchCredentials.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java index a15279b36..a4c55b1c1 100644 --- a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java @@ -39,6 +39,7 @@ import com.google.api.client.http.HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.http.json.JsonHttpContent; +import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.JsonObjectParser; import com.google.api.client.json.gson.GsonFactory; @@ -114,6 +115,58 @@ public class GdchCredentials extends GoogleCredentials { this.name = GoogleCredentialsInfo.GDCH_CREDENTIALS.getCredentialName(); } + /** + * Returns credentials defined by a GdchCredentials key file in JSON format from the Google + * Developers Console. + * + *

Important: If you accept a credential configuration (credential JSON/File/Stream) from an + * external source for authentication to Google Cloud Platform, you must validate it before + * providing it to any Google API or library. Providing an unvalidated credential configuration to + * Google APIs can compromise the security of your systems and data. For more information, refer + * to {@see documentation}. + * + * @param credentialsStream the stream with the credential definition. + * @return the credential defined by the credentialsStream. + * @throws IOException if the credential cannot be created from the stream. + */ + public static GdchCredentials fromStream(InputStream credentialsStream) throws IOException { + return fromStream(credentialsStream, OAuth2Utils.HTTP_TRANSPORT_FACTORY); + } + + /** + * Returns credentials defined by a GdchCredentials key file in JSON format from the Google + * Developers Console. + * + *

Important: If you accept a credential configuration (credential JSON/File/Stream) from an + * external source for authentication to Google Cloud Platform, you must validate it before + * providing it to any Google API or library. Providing an unvalidated credential configuration to + * Google APIs can compromise the security of your systems and data. For more information, refer + * to {@see documentation}. + * + * @param credentialsStream the stream with the credential definition. + * @param transportFactory HTTP transport factory, creates the transport used to get access + * tokens. + * @return the credential defined by the credentialsStream. + * @throws IOException if the credential cannot be created from the stream. + */ + public static GdchCredentials fromStream( + InputStream credentialsStream, HttpTransportFactory transportFactory) throws IOException { + Preconditions.checkNotNull(transportFactory); + GenericJson fileContents = parseJsonInputStream(credentialsStream); + String fileType = extractFromJson(fileContents, "type"); + if (fileType.equals(GoogleCredentialsInfo.GDCH_CREDENTIALS.getFileType())) { + return fromJson(fileContents, transportFactory); + } + + throw new IOException( + String.format( + "Error reading credentials from stream, 'type' value '%s' not recognized." + + " Expecting '%s'.", + fileType, GoogleCredentialsInfo.GDCH_CREDENTIALS.getFileType())); + } + /** * Create GDCH service account credentials defined by JSON. * From e6635c6e8b59ceaaa49112bd51f6903f2583aad4 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Wed, 4 Mar 2026 19:02:10 -0500 Subject: [PATCH 08/24] test: increase coverage --- .../auth/oauth2/GdchCredentialsTest.java | 240 ++++++++++++++++++ 1 file changed, 240 insertions(+) diff --git a/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java index 0eaa57dde..d3278f3b7 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java @@ -40,10 +40,12 @@ import static org.junit.jupiter.api.Assertions.fail; import com.google.api.client.json.GenericJson; +import com.google.api.client.json.Json; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.webtoken.JsonWebSignature; import com.google.api.client.json.webtoken.JsonWebToken; import com.google.api.client.testing.http.FixedClock; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.api.client.util.Clock; import com.google.auth.TestUtils; import com.google.auth.oauth2.GoogleCredentials.GoogleCredentialsInfo; @@ -407,6 +409,77 @@ void fromJSON_hasAccessToken() throws IOException { TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN); } + @Test + void fromStream_correct() throws IOException { + InputStream stream = + writeGdchServiceAccountStream( + FORMAT_VERSION, + PROJECT_ID, + PRIVATE_KEY_ID, + PRIVATE_KEY_PKCS8, + SERVICE_IDENTITY_NAME, + CA_CERT_PATH, + TOKEN_SERVER_URI); + GdchCredentials credentials = GdchCredentials.fromStream(stream); + + assertEquals(PROJECT_ID, credentials.getProjectId()); + assertEquals(SERVICE_IDENTITY_NAME, credentials.getServiceIdentityName()); + } + + @Test + void fromStream_invalidType() throws IOException { + GenericJson json = + writeGdchServiceAccountJson( + FORMAT_VERSION, + PROJECT_ID, + PRIVATE_KEY_ID, + PRIVATE_KEY_PKCS8, + SERVICE_IDENTITY_NAME, + CA_CERT_PATH, + TOKEN_SERVER_URI); + json.put("type", "invalid_type"); + InputStream stream = TestUtils.jsonToInputStream(json); + + try { + GdchCredentials.fromStream(stream); + fail("Should not be able to create GDCH credential with invalid type."); + } catch (IOException ex) { + assertTrue(ex.getMessage().contains("not recognized")); + } + } + + @Test + void fromStream_withTransportFactory() throws IOException { + MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory(); + InputStream stream = + writeGdchServiceAccountStream( + FORMAT_VERSION, + PROJECT_ID, + PRIVATE_KEY_ID, + PRIVATE_KEY_PKCS8, + SERVICE_IDENTITY_NAME, + CA_CERT_PATH, + TOKEN_SERVER_URI); + GdchCredentials credentials = GdchCredentials.fromStream(stream, transportFactory); + + assertEquals(transportFactory, credentials.getTransportFactory()); + } + + @Test + void fromPkcs8_correct() throws IOException { + GdchCredentials.Builder builder = + GdchCredentials.newBuilder() + .setProjectId(PROJECT_ID) + .setPrivateKeyId(PRIVATE_KEY_ID) + .setServiceIdentityName(SERVICE_IDENTITY_NAME) + .setTokenServerUri(TOKEN_SERVER_URI) + .setHttpTransportFactory(new MockTokenServerTransportFactory()); + + GdchCredentials credentials = GdchCredentials.fromPkcs8(PRIVATE_KEY_PKCS8, builder); + assertNotNull(credentials.getPrivateKey()); + assertEquals(PROJECT_ID, credentials.getProjectId()); + } + @Test void createWithGdchAudience_correct() throws IOException { GenericJson json = @@ -875,6 +948,173 @@ void serialize_correct() throws IOException, ClassNotFoundException { deserializedCredentials.toBuilder().getHttpTransportFactory().getClass()); } + @Test + void refreshAccessToken_invalidResponse_missingAccessToken() throws IOException { + MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory(); + GenericJson json = + writeGdchServiceAccountJson( + FORMAT_VERSION, + PROJECT_ID, + PRIVATE_KEY_ID, + PRIVATE_KEY_PKCS8, + SERVICE_IDENTITY_NAME, + CA_CERT_PATH, + TOKEN_SERVER_URI); + GdchCredentials credentials = GdchCredentials.fromJson(json, transportFactory); + GdchCredentials gdchWithAudience = credentials.createWithGdchAudience(API_AUDIENCE); + + transportFactory.transport.addGdchServiceAccount( + GdchCredentials.getIssuerSubjectValue(PROJECT_ID, SERVICE_IDENTITY_NAME), null); + transportFactory.transport.setTokenServerUri(TOKEN_SERVER_URI); + + try { + gdchWithAudience.refreshAccessToken(); + fail("Should not be able to refresh access token without exception."); + } catch (IOException ex) { + assertTrue( + ex.getMessage() + .contains( + "Error parsing token refresh response. Expected value access_token not found.")); + } + } + + @Test + void refreshAccessToken_invalidResponse_wrongTypeAccessToken() throws IOException { + MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory(); + GenericJson json = + writeGdchServiceAccountJson( + FORMAT_VERSION, + PROJECT_ID, + PRIVATE_KEY_ID, + PRIVATE_KEY_PKCS8, + SERVICE_IDENTITY_NAME, + CA_CERT_PATH, + TOKEN_SERVER_URI); + GdchCredentials credentials = GdchCredentials.fromJson(json, transportFactory); + GdchCredentials gdchWithAudience = credentials.createWithGdchAudience(API_AUDIENCE); + + transportFactory.transport.addResponseSequence( + new MockLowLevelHttpResponse() + .setContentType(Json.MEDIA_TYPE) + .setContent("{\"access_token\": 123, \"expires_in\": 3600}")); + + try { + gdchWithAudience.refreshAccessToken(); + fail("Should not be able to refresh access token with wrong type."); + } catch (IOException ex) { + assertTrue(ex.getMessage().contains("Expected string value access_token of wrong type")); + } + } + + @Test + void refreshAccessToken_invalidResponse_missingExpiresIn() throws IOException { + MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory(); + GenericJson json = + writeGdchServiceAccountJson( + FORMAT_VERSION, + PROJECT_ID, + PRIVATE_KEY_ID, + PRIVATE_KEY_PKCS8, + SERVICE_IDENTITY_NAME, + CA_CERT_PATH, + TOKEN_SERVER_URI); + GdchCredentials credentials = GdchCredentials.fromJson(json, transportFactory); + GdchCredentials gdchWithAudience = credentials.createWithGdchAudience(API_AUDIENCE); + + transportFactory.transport.addResponseSequence( + new MockLowLevelHttpResponse() + .setContentType(Json.MEDIA_TYPE) + .setContent("{\"access_token\": \"token\"}")); + + try { + gdchWithAudience.refreshAccessToken(); + fail("Should not be able to refresh access token with missing expires_in."); + } catch (IOException ex) { + assertTrue(ex.getMessage().contains("Expected value expires_in not found")); + } + } + + @Test + void refreshAccessToken_invalidResponse_wrongTypeExpiresIn() throws IOException { + MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory(); + GenericJson json = + writeGdchServiceAccountJson( + FORMAT_VERSION, + PROJECT_ID, + PRIVATE_KEY_ID, + PRIVATE_KEY_PKCS8, + SERVICE_IDENTITY_NAME, + CA_CERT_PATH, + TOKEN_SERVER_URI); + GdchCredentials credentials = GdchCredentials.fromJson(json, transportFactory); + GdchCredentials gdchWithAudience = credentials.createWithGdchAudience(API_AUDIENCE); + + transportFactory.transport.addResponseSequence( + new MockLowLevelHttpResponse() + .setContentType(Json.MEDIA_TYPE) + .setContent("{\"access_token\": \"token\", \"expires_in\": \"3600\"}")); + + try { + gdchWithAudience.refreshAccessToken(); + fail("Should not be able to refresh access token with wrong type expires_in."); + } catch (IOException ex) { + assertTrue(ex.getMessage().contains("Expected integer value expires_in of wrong type")); + } + } + + @Test + void refreshAccessToken_serverError() throws IOException { + MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory(); + GenericJson json = + writeGdchServiceAccountJson( + FORMAT_VERSION, + PROJECT_ID, + PRIVATE_KEY_ID, + PRIVATE_KEY_PKCS8, + SERVICE_IDENTITY_NAME, + CA_CERT_PATH, + TOKEN_SERVER_URI); + GdchCredentials credentials = GdchCredentials.fromJson(json, transportFactory); + GdchCredentials gdchWithAudience = credentials.createWithGdchAudience(API_AUDIENCE); + + transportFactory.transport.addResponseSequence( + new MockLowLevelHttpResponse().setStatusCode(400).setReasonPhrase("Bad Request")); + + try { + gdchWithAudience.refreshAccessToken(); + fail("Should not be able to refresh access token with server error."); + } catch (IOException ex) { + assertTrue(ex.getMessage().contains("Error getting access token for GDCH service account")); + assertTrue(ex.getMessage().contains("400 Bad Request")); + } + } + + @Test + void refreshAccessToken_ioException() throws IOException { + MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory(); + GenericJson json = + writeGdchServiceAccountJson( + FORMAT_VERSION, + PROJECT_ID, + PRIVATE_KEY_ID, + PRIVATE_KEY_PKCS8, + SERVICE_IDENTITY_NAME, + CA_CERT_PATH, + TOKEN_SERVER_URI); + GdchCredentials credentials = GdchCredentials.fromJson(json, transportFactory); + GdchCredentials gdchWithAudience = credentials.createWithGdchAudience(API_AUDIENCE); + + transportFactory.transport.addResponseErrorSequence(new IOException("Connection reset")); + + try { + gdchWithAudience.refreshAccessToken(); + fail("Should not be able to refresh access token with IO exception."); + } catch (IOException ex) { + assertTrue(ex.getMessage().contains("Error getting access token for GDCH service account")); + assertTrue(ex.getMessage().contains("Connection reset")); + } + } + static GenericJson writeGdchServiceAccountJson( String formatVersion, String project, From cc49dbacebefdb75002492955a81b3cc0ad6cb55 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 10 Mar 2026 16:52:43 -0400 Subject: [PATCH 09/24] test(gdch): parameterize test --- .../google/auth/oauth2/GdchCredentials.java | 1 + .../DefaultCredentialsProviderTest.java | 2 +- .../auth/oauth2/GdchCredentialsTest.java | 88 +++++-------------- oauth2_http/pom.xml | 5 ++ 4 files changed, 31 insertions(+), 65 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java index a4c55b1c1..0f221d3f4 100644 --- a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java @@ -346,6 +346,7 @@ static String getIssuerSubjectValue(String projectId, String serviceIdentityName return String.format("system:serviceaccount:%s:%s", projectId, serviceIdentityName); } + @Override public final String getProjectId() { return projectId; } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java b/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java index 16e4b57c9..4a7447df6 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java @@ -214,7 +214,7 @@ void getDefaultCredentials_static_windows_configuredAsLinux_notGce() throws IOEx } @Test - void getDefaultCredentials_static_unsupportedPlatform_notGce() throws IOException { + void getDefaultCredentials_static_unsupportedPlatform_notGce() { TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider(); testProvider.setProperty("os.name", "macos"); String productFilePath = SMBIOS_PATH_LINUX; diff --git a/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java index d3278f3b7..e5b4e117e 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java @@ -36,12 +36,14 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.params.provider.Arguments.arguments; import com.google.api.client.json.GenericJson; import com.google.api.client.json.Json; -import com.google.api.client.json.JsonFactory; +t import com.google.api.client.json.JsonFactory; import com.google.api.client.json.webtoken.JsonWebSignature; import com.google.api.client.json.webtoken.JsonWebToken; import com.google.api.client.testing.http.FixedClock; @@ -56,7 +58,11 @@ import java.nio.file.Files; import java.util.List; import java.util.Map; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; /** Test case for {@link GdchCredentials}. */ class GdchCredentialsTest extends BaseSerializationTest { @@ -978,36 +984,10 @@ void refreshAccessToken_invalidResponse_missingAccessToken() throws IOException } } - @Test - void refreshAccessToken_invalidResponse_wrongTypeAccessToken() throws IOException { - MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory(); - GenericJson json = - writeGdchServiceAccountJson( - FORMAT_VERSION, - PROJECT_ID, - PRIVATE_KEY_ID, - PRIVATE_KEY_PKCS8, - SERVICE_IDENTITY_NAME, - CA_CERT_PATH, - TOKEN_SERVER_URI); - GdchCredentials credentials = GdchCredentials.fromJson(json, transportFactory); - GdchCredentials gdchWithAudience = credentials.createWithGdchAudience(API_AUDIENCE); - - transportFactory.transport.addResponseSequence( - new MockLowLevelHttpResponse() - .setContentType(Json.MEDIA_TYPE) - .setContent("{\"access_token\": 123, \"expires_in\": 3600}")); - - try { - gdchWithAudience.refreshAccessToken(); - fail("Should not be able to refresh access token with wrong type."); - } catch (IOException ex) { - assertTrue(ex.getMessage().contains("Expected string value access_token of wrong type")); - } - } - - @Test - void refreshAccessToken_invalidResponse_missingExpiresIn() throws IOException { + @ParameterizedTest + @MethodSource("provideInvalidResponses") + void refreshAccessToken_invalidResponse(String responseContent, String expectedErrorMessage) + throws IOException { MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory(); GenericJson json = writeGdchServiceAccountJson( @@ -1024,42 +1004,22 @@ void refreshAccessToken_invalidResponse_missingExpiresIn() throws IOException { transportFactory.transport.addResponseSequence( new MockLowLevelHttpResponse() .setContentType(Json.MEDIA_TYPE) - .setContent("{\"access_token\": \"token\"}")); + .setContent(responseContent)); - try { - gdchWithAudience.refreshAccessToken(); - fail("Should not be able to refresh access token with missing expires_in."); - } catch (IOException ex) { - assertTrue(ex.getMessage().contains("Expected value expires_in not found")); - } + IOException exception = + assertThrows(IOException.class, () -> gdchWithAudience.refreshAccessToken()); + assertTrue(exception.getMessage().contains(expectedErrorMessage)); } - @Test - void refreshAccessToken_invalidResponse_wrongTypeExpiresIn() throws IOException { - MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory(); - GenericJson json = - writeGdchServiceAccountJson( - FORMAT_VERSION, - PROJECT_ID, - PRIVATE_KEY_ID, - PRIVATE_KEY_PKCS8, - SERVICE_IDENTITY_NAME, - CA_CERT_PATH, - TOKEN_SERVER_URI); - GdchCredentials credentials = GdchCredentials.fromJson(json, transportFactory); - GdchCredentials gdchWithAudience = credentials.createWithGdchAudience(API_AUDIENCE); - - transportFactory.transport.addResponseSequence( - new MockLowLevelHttpResponse() - .setContentType(Json.MEDIA_TYPE) - .setContent("{\"access_token\": \"token\", \"expires_in\": \"3600\"}")); - - try { - gdchWithAudience.refreshAccessToken(); - fail("Should not be able to refresh access token with wrong type expires_in."); - } catch (IOException ex) { - assertTrue(ex.getMessage().contains("Expected integer value expires_in of wrong type")); - } + private static Stream provideInvalidResponses() { + return Stream.of( + arguments( + "{\"access_token\": 123, \"expires_in\": 3600}", + "Expected string value access_token of wrong type"), + arguments("{\"access_token\": \"token\"}", "Expected value expires_in not found"), + arguments( + "{\"access_token\": \"token\", \"expires_in\": \"3600\"}", + "Expected integer value expires_in of wrong type")); } @Test diff --git a/oauth2_http/pom.xml b/oauth2_http/pom.xml index 9cd053be0..5623c8fad 100644 --- a/oauth2_http/pom.xml +++ b/oauth2_http/pom.xml @@ -311,6 +311,11 @@ junit-jupiter-api test + + org.junit.jupiter + junit-jupiter-params + test + org.junit.jupiter junit-jupiter-engine From f58f5938ec7e81cc96803ff5e56ae047c36e2c53 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 10 Mar 2026 16:53:05 -0400 Subject: [PATCH 10/24] test: remove unused var --- .../javatests/com/google/auth/oauth2/GdchCredentialsTest.java | 2 +- .../javatests/com/google/auth/oauth2/GoogleCredentialsTest.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java index e5b4e117e..b95028dfb 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java @@ -43,7 +43,7 @@ import com.google.api.client.json.GenericJson; import com.google.api.client.json.Json; -t import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.JsonFactory; import com.google.api.client.json.webtoken.JsonWebSignature; import com.google.api.client.json.webtoken.JsonWebToken; import com.google.api.client.testing.http.FixedClock; diff --git a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java index 214932d53..978e8a79d 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java @@ -158,7 +158,6 @@ void fromStream_noType_throws() throws IOException { @Test void fromStream_nullStream_throws() { - MockHttpTransportFactory transportFactory = new MockHttpTransportFactory(); assertThrows(NullPointerException.class, () -> GoogleCredentials.parseJsonInputStream(null)); } From 42f2662de9176f65eabc4afcc9e2c51c1212060c Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 10 Mar 2026 16:54:46 -0400 Subject: [PATCH 11/24] chore: remove unused throw clause --- .../com/google/auth/oauth2/DefaultCredentialsProviderTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java b/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java index 4a7447df6..e0c8ef8c1 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java @@ -191,7 +191,7 @@ void getDefaultCredentials_noCredentials_linuxNotGce() throws IOException { } @Test - void getDefaultCredentials_static_linux() throws IOException { + void getDefaultCredentials_static_linux() { TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider(); testProvider.setProperty("os.name", "Linux"); String productFilePath = SMBIOS_PATH_LINUX; From 91a37512addc6826b9000f2af3383b581e7b7d43 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 10 Mar 2026 16:57:09 -0400 Subject: [PATCH 12/24] test: parameterize more --- .../auth/oauth2/GdchCredentialsTest.java | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java index b95028dfb..9712c8137 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java @@ -36,10 +36,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import static org.junit.jupiter.params.provider.Arguments.arguments; import com.google.api.client.json.GenericJson; import com.google.api.client.json.Json; @@ -58,11 +56,7 @@ import java.nio.file.Files; import java.util.List; import java.util.Map; -import java.util.stream.Stream; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; /** Test case for {@link GdchCredentials}. */ class GdchCredentialsTest extends BaseSerializationTest { @@ -984,10 +978,28 @@ void refreshAccessToken_invalidResponse_missingAccessToken() throws IOException } } - @ParameterizedTest - @MethodSource("provideInvalidResponses") - void refreshAccessToken_invalidResponse(String responseContent, String expectedErrorMessage) - throws IOException { + @Test + void refreshAccessToken_invalidResponse_wrongTypeAccessToken() throws IOException { + refreshAccessToken_invalidResponse( + "{\"access_token\": 123, \"expires_in\": 3600}", + "Expected string value access_token of wrong type"); + } + + @Test + void refreshAccessToken_invalidResponse_missingExpiresIn() throws IOException { + refreshAccessToken_invalidResponse( + "{\"access_token\": \"token\"}", "Expected value expires_in not found"); + } + + @Test + void refreshAccessToken_invalidResponse_wrongTypeExpiresIn() throws IOException { + refreshAccessToken_invalidResponse( + "{\"access_token\": \"token\", \"expires_in\": \"3600\"}", + "Expected integer value expires_in of wrong type"); + } + + private void refreshAccessToken_invalidResponse( + String responseContent, String expectedErrorMessage) throws IOException { MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory(); GenericJson json = writeGdchServiceAccountJson( @@ -1006,20 +1018,12 @@ void refreshAccessToken_invalidResponse(String responseContent, String expectedE .setContentType(Json.MEDIA_TYPE) .setContent(responseContent)); - IOException exception = - assertThrows(IOException.class, () -> gdchWithAudience.refreshAccessToken()); - assertTrue(exception.getMessage().contains(expectedErrorMessage)); - } - - private static Stream provideInvalidResponses() { - return Stream.of( - arguments( - "{\"access_token\": 123, \"expires_in\": 3600}", - "Expected string value access_token of wrong type"), - arguments("{\"access_token\": \"token\"}", "Expected value expires_in not found"), - arguments( - "{\"access_token\": \"token\", \"expires_in\": \"3600\"}", - "Expected integer value expires_in of wrong type")); + try { + gdchWithAudience.refreshAccessToken(); + fail("Should not be able to refresh access token with invalid response."); + } catch (IOException ex) { + assertTrue(ex.getMessage().contains(expectedErrorMessage)); + } } @Test From b83a511be8d899651dda2ff4911204e429f60614 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 10 Mar 2026 17:01:46 -0400 Subject: [PATCH 13/24] fix: remove unused parameter --- oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java | 4 ++-- .../javatests/com/google/auth/oauth2/GdchCredentialsTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java index 0f221d3f4..9d0194771 100644 --- a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java @@ -267,7 +267,7 @@ public AccessToken refreshAccessToken() throws IOException { JsonFactory jsonFactory = GsonFactory.getDefaultInstance(); long currentTime = Clock.SYSTEM.currentTimeMillis(); - String assertion = createAssertion(jsonFactory, currentTime, apiAudience); + String assertion = createAssertion(jsonFactory, currentTime); GenericData tokenRequest = new GenericData(); tokenRequest.set("audience", apiAudience); @@ -310,7 +310,7 @@ public AccessToken refreshAccessToken() throws IOException { * (tokenServerUri), not for API call. It uses the serviceIdentityName as the `iss` and `sub` * claim, and the tokenServerUri as the `aud` claim. The JWT is signed with the privateKey. */ - String createAssertion(JsonFactory jsonFactory, long currentTime, String apiAudience) + String createAssertion(JsonFactory jsonFactory, long currentTime) throws IOException { JsonWebSignature.Header header = new JsonWebSignature.Header(); header.setAlgorithm("ES256"); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java index 9712c8137..562ca5a8e 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java @@ -543,7 +543,7 @@ void createAssertion_correct() throws IOException { GdchCredentials credentials = GdchCredentials.fromJson(json); JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY; long currentTimeMillis = Clock.SYSTEM.currentTimeMillis(); - String assertion = credentials.createAssertion(jsonFactory, currentTimeMillis, API_AUDIENCE); + String assertion = credentials.createAssertion(jsonFactory, currentTimeMillis); JsonWebSignature signature = JsonWebSignature.parse(jsonFactory, assertion); JsonWebToken.Payload payload = signature.getPayload(); From 625741ecf3d998f04a268df7373e4b71e9a95d45 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 10 Mar 2026 17:05:56 -0400 Subject: [PATCH 14/24] fix: make variables final as intended --- oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java index 9d0194771..9472af536 100644 --- a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java @@ -71,8 +71,8 @@ import java.util.Objects; public class GdchCredentials extends GoogleCredentials { - private static String VALUE_NOT_FOUND_MESSAGE = "%sExpected value %s not found."; - private static String VALUE_WRONG_TYPE_MESSAGE = "%sExpected %s value %s of wrong type."; + private static final String VALUE_NOT_FOUND_MESSAGE = "%sExpected value %s not found."; + private static final String VALUE_WRONG_TYPE_MESSAGE = "%sExpected %s value %s of wrong type."; private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. "; @VisibleForTesting static final String SUPPORTED_FORMAT_VERSION = "1"; From 1e0a7c3eb1c354233fa4a605fede32d83656a0c8 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 10 Mar 2026 17:07:25 -0400 Subject: [PATCH 15/24] fix: remove unused throw clause --- .../com/google/auth/oauth2/DefaultCredentialsProviderTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java b/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java index e0c8ef8c1..75b039c10 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java @@ -180,7 +180,7 @@ void getDefaultCredentials_noCredentials_singleGceTestRequest() { } @Test - void getDefaultCredentials_noCredentials_linuxNotGce() throws IOException { + void getDefaultCredentials_noCredentials_linuxNotGce() { TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider(); testProvider.setProperty("os.name", "Linux"); String productFilePath = SMBIOS_PATH_LINUX; From 24cd25d01638f5463634b0590cfaa8994af79706 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 10 Mar 2026 17:08:11 -0400 Subject: [PATCH 16/24] fix: remove unused throw clause --- oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java index 9472af536..b345a734f 100644 --- a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java @@ -244,7 +244,7 @@ static GdchCredentials fromPkcs8(String privateKeyPkcs8, GdchCredentials.Builder * * @param apiAudience The intended audience for GDCH credentials. */ - public GdchCredentials createWithGdchAudience(String apiAudience) throws IOException { + public GdchCredentials createWithGdchAudience(String apiAudience) { Preconditions.checkNotNull( apiAudience, "Audience are not configured for GDCH service account credentials."); return this.toBuilder().setGdchAudience(apiAudience).build(); From 027a242a7cd3642b69e65390f5f184e26bbc7992 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 10 Mar 2026 17:10:34 -0400 Subject: [PATCH 17/24] fix: make OAuth2Credentials clock package private for production code --- oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java b/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java index f86e3c8d7..b94346a1d 100644 --- a/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java @@ -86,7 +86,7 @@ public class OAuth2Credentials extends Credentials { // Change listeners are not serialized private transient List changeListeners; // Until we expose this to the users it can remain transient and non-serializable - @VisibleForTesting transient Clock clock = Clock.SYSTEM; + transient Clock clock = Clock.SYSTEM; /** * Returns the credentials instance from the given access token. From a8b459b379d49253c9e82fbbf585fc1a256a5016 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 10 Mar 2026 17:16:50 -0400 Subject: [PATCH 18/24] fix: use non deprecated base 64 encoder --- .../java/com/google/auth/oauth2/GdchCredentials.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java index b345a734f..3c5f54d5d 100644 --- a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java @@ -45,7 +45,6 @@ import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.json.webtoken.JsonWebSignature; import com.google.api.client.json.webtoken.JsonWebToken; -import com.google.api.client.util.Base64; import com.google.api.client.util.Clock; import com.google.api.client.util.GenericData; import com.google.api.client.util.SecurityUtils; @@ -54,6 +53,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; +import com.google.common.io.BaseEncoding; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.File; import java.io.FileInputStream; @@ -648,9 +648,9 @@ private static String signUsingEsSha256( JsonWebToken.Payload payload) throws GeneralSecurityException, IOException { String content = - Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(header)) + BaseEncoding.base64Url().omitPadding().encode(jsonFactory.toByteArray(header)) + "." - + Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(payload)); + + BaseEncoding.base64Url().omitPadding().encode(jsonFactory.toByteArray(payload)); byte[] contentBytes = StringUtils.getBytesUtf8(content); byte[] signature = SecurityUtils.sign(SecurityUtils.getEs256SignatureAlgorithm(), privateKey, contentBytes); @@ -659,7 +659,7 @@ private static String signUsingEsSha256( // format. // We need to transcode it. For ES256, the output length is 64 bytes. byte[] jwsSignature = transcodeDerToConcat(signature, 64); - return content + "." + Base64.encodeBase64URLSafeString(jwsSignature); + return content + "." + BaseEncoding.base64Url().omitPadding().encode(jwsSignature); } private static byte[] transcodeDerToConcat(byte[] derSignature, int outputLength) From 4e8af24c1bc70fddc1504b27bbb1f75de13891d2 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 10 Mar 2026 17:20:01 -0400 Subject: [PATCH 19/24] test: parameterize flagged tests --- .../DefaultCredentialsProviderTest.java | 41 ++++++------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java b/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java index 75b039c10..2e74cdb60 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java @@ -181,47 +181,32 @@ void getDefaultCredentials_noCredentials_singleGceTestRequest() { @Test void getDefaultCredentials_noCredentials_linuxNotGce() { - TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider(); - testProvider.setProperty("os.name", "Linux"); - String productFilePath = SMBIOS_PATH_LINUX; - InputStream productStream = new ByteArrayInputStream("test".getBytes()); - testProvider.addFile(productFilePath, productStream); - - assertFalse(ComputeEngineCredentials.checkStaticGceDetection(testProvider)); + checkStaticGceDetection("Linux", "test", false); } @Test void getDefaultCredentials_static_linux() { - TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider(); - testProvider.setProperty("os.name", "Linux"); - String productFilePath = SMBIOS_PATH_LINUX; - File productFile = new File(productFilePath); - InputStream productStream = new ByteArrayInputStream("Googlekdjsfhg".getBytes()); - testProvider.addFile(productFile.getAbsolutePath(), productStream); - - assertTrue(ComputeEngineCredentials.checkStaticGceDetection(testProvider)); + checkStaticGceDetection("Linux", "Googlekdjsfhg", true); } @Test - void getDefaultCredentials_static_windows_configuredAsLinux_notGce() throws IOException { - TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider(); - testProvider.setProperty("os.name", "windows"); - String productFilePath = SMBIOS_PATH_LINUX; - InputStream productStream = new ByteArrayInputStream("Googlekdjsfhg".getBytes()); - testProvider.addFile(productFilePath, productStream); - - assertFalse(ComputeEngineCredentials.checkStaticGceDetection(testProvider)); + void getDefaultCredentials_static_windows_configuredAsLinux_notGce() { + checkStaticGceDetection("windows", "Googlekdjsfhg", false); } @Test void getDefaultCredentials_static_unsupportedPlatform_notGce() { + checkStaticGceDetection("macos", "Googlekdjsfhg", false); + } + + private void checkStaticGceDetection(String osName, String productContent, boolean expected) { TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider(); - testProvider.setProperty("os.name", "macos"); + testProvider.setProperty("os.name", osName); String productFilePath = SMBIOS_PATH_LINUX; - InputStream productStream = new ByteArrayInputStream("Googlekdjsfhg".getBytes()); + InputStream productStream = new ByteArrayInputStream(productContent.getBytes()); testProvider.addFile(productFilePath, productStream); - assertFalse(ComputeEngineCredentials.checkStaticGceDetection(testProvider)); + assertEquals(expected, ComputeEngineCredentials.checkStaticGceDetection(testProvider)); } @Test @@ -411,7 +396,7 @@ void getDefaultCredentials_GdchServiceAccount() throws IOException { GDCH_SA_SERVICE_IDENTITY_NAME, ((GdchCredentials) defaultCredentials).getServiceIdentityName()); assertEquals( - GDCH_SA_TOKEN_SERVER_URI, ((GdchCredentials) defaultCredentials).getTokenServerUri()); + GDCH_SA_TOKEN_SERVER_URI, ((GdchCredentials) defaultCredentials).getServiceIdentityName()); assertEquals(GDCH_SA_CA_CERT_PATH, ((GdchCredentials) defaultCredentials).getCaCertPath()); assertNull(((GdchCredentials) defaultCredentials).getApiAudience()); @@ -424,7 +409,7 @@ void getDefaultCredentials_GdchServiceAccount() throws IOException { GDCH_SA_SERVICE_IDENTITY_NAME, ((GdchCredentials) defaultCredentials).getServiceIdentityName()); assertEquals( - GDCH_SA_TOKEN_SERVER_URI, ((GdchCredentials) defaultCredentials).getTokenServerUri()); + GDCH_SA_TOKEN_SERVER_URI, ((GdchCredentials) defaultCredentials).getServiceIdentityName()); assertEquals(GDCH_SA_CA_CERT_PATH, ((GdchCredentials) defaultCredentials).getCaCertPath()); assertNotNull(((GdchCredentials) defaultCredentials).getApiAudience()); } From f257a8123129251455f38c8f91e2a8d68e3d21c8 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 10 Mar 2026 17:21:58 -0400 Subject: [PATCH 20/24] chore: format --- oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java | 3 +-- .../javatests/com/google/auth/oauth2/GdchCredentialsTest.java | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java index 3c5f54d5d..2f3658787 100644 --- a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java @@ -310,8 +310,7 @@ public AccessToken refreshAccessToken() throws IOException { * (tokenServerUri), not for API call. It uses the serviceIdentityName as the `iss` and `sub` * claim, and the tokenServerUri as the `aud` claim. The JWT is signed with the privateKey. */ - String createAssertion(JsonFactory jsonFactory, long currentTime) - throws IOException { + String createAssertion(JsonFactory jsonFactory, long currentTime) throws IOException { JsonWebSignature.Header header = new JsonWebSignature.Header(); header.setAlgorithm("ES256"); header.setType("JWT"); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java index 562ca5a8e..ede426c8d 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/GdchCredentialsTest.java @@ -1014,9 +1014,7 @@ private void refreshAccessToken_invalidResponse( GdchCredentials gdchWithAudience = credentials.createWithGdchAudience(API_AUDIENCE); transportFactory.transport.addResponseSequence( - new MockLowLevelHttpResponse() - .setContentType(Json.MEDIA_TYPE) - .setContent(responseContent)); + new MockLowLevelHttpResponse().setContentType(Json.MEDIA_TYPE).setContent(responseContent)); try { gdchWithAudience.refreshAccessToken(); From c523df2c74ada5ef8c5ca4f8de224e4f120eda3d Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 10 Mar 2026 17:27:46 -0400 Subject: [PATCH 21/24] test: fix assertion --- .../google/auth/oauth2/DefaultCredentialsProviderTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java b/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java index 2e74cdb60..fca345983 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java @@ -396,7 +396,7 @@ void getDefaultCredentials_GdchServiceAccount() throws IOException { GDCH_SA_SERVICE_IDENTITY_NAME, ((GdchCredentials) defaultCredentials).getServiceIdentityName()); assertEquals( - GDCH_SA_TOKEN_SERVER_URI, ((GdchCredentials) defaultCredentials).getServiceIdentityName()); + GDCH_SA_TOKEN_SERVER_URI, ((GdchCredentials) defaultCredentials).getTokenServerUri()); assertEquals(GDCH_SA_CA_CERT_PATH, ((GdchCredentials) defaultCredentials).getCaCertPath()); assertNull(((GdchCredentials) defaultCredentials).getApiAudience()); @@ -409,7 +409,7 @@ void getDefaultCredentials_GdchServiceAccount() throws IOException { GDCH_SA_SERVICE_IDENTITY_NAME, ((GdchCredentials) defaultCredentials).getServiceIdentityName()); assertEquals( - GDCH_SA_TOKEN_SERVER_URI, ((GdchCredentials) defaultCredentials).getServiceIdentityName()); + GDCH_SA_TOKEN_SERVER_URI, ((GdchCredentials) defaultCredentials).getTokenServerUri()); assertEquals(GDCH_SA_CA_CERT_PATH, ((GdchCredentials) defaultCredentials).getCaCertPath()); assertNotNull(((GdchCredentials) defaultCredentials).getApiAudience()); } From ed026a4c7c1670b55448085c8d95fea22dd421a7 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 10 Mar 2026 17:28:55 -0400 Subject: [PATCH 22/24] build: remove unused dependency --- oauth2_http/pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/oauth2_http/pom.xml b/oauth2_http/pom.xml index 5623c8fad..9cd053be0 100644 --- a/oauth2_http/pom.xml +++ b/oauth2_http/pom.xml @@ -311,11 +311,6 @@ junit-jupiter-api test - - org.junit.jupiter - junit-jupiter-params - test - org.junit.jupiter junit-jupiter-engine From 1528e76f2a45e7a1bc3f87a035351ba59c39739c Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 10 Mar 2026 17:36:38 -0400 Subject: [PATCH 23/24] test: run linux gce only on linux envs --- .../google/auth/oauth2/DefaultCredentialsProviderTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java b/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java index fca345983..25eec3b82 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java @@ -38,6 +38,7 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.LowLevelHttpRequest; @@ -200,6 +201,9 @@ void getDefaultCredentials_static_unsupportedPlatform_notGce() { } private void checkStaticGceDetection(String osName, String productContent, boolean expected) { + assumeTrue( + System.getProperty("os.name").toLowerCase().startsWith("linux"), + "This test only runs on Linux."); TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider(); testProvider.setProperty("os.name", osName); String productFilePath = SMBIOS_PATH_LINUX; From 7300e5978edd31c3d0a965e583fb9b371fdab22a Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 10 Mar 2026 17:59:55 -0400 Subject: [PATCH 24/24] fix: sonarqube flags (use java.util.Base64) --- .../java/com/google/auth/oauth2/GdchCredentials.java | 10 ++++++---- .../com/google/auth/oauth2/GoogleCredentialsTest.java | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java index 2f3658787..174984595 100644 --- a/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java @@ -53,7 +53,6 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; -import com.google.common.io.BaseEncoding; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.File; import java.io.FileInputStream; @@ -66,6 +65,7 @@ import java.net.URISyntaxException; import java.security.GeneralSecurityException; import java.security.PrivateKey; +import java.util.Base64; import java.util.Date; import java.util.Map; import java.util.Objects; @@ -647,9 +647,11 @@ private static String signUsingEsSha256( JsonWebToken.Payload payload) throws GeneralSecurityException, IOException { String content = - BaseEncoding.base64Url().omitPadding().encode(jsonFactory.toByteArray(header)) + Base64.getUrlEncoder().withoutPadding().encodeToString(jsonFactory.toByteArray(header)) + "." - + BaseEncoding.base64Url().omitPadding().encode(jsonFactory.toByteArray(payload)); + + Base64.getUrlEncoder() + .withoutPadding() + .encodeToString(jsonFactory.toByteArray(payload)); byte[] contentBytes = StringUtils.getBytesUtf8(content); byte[] signature = SecurityUtils.sign(SecurityUtils.getEs256SignatureAlgorithm(), privateKey, contentBytes); @@ -658,7 +660,7 @@ private static String signUsingEsSha256( // format. // We need to transcode it. For ES256, the output length is 64 bytes. byte[] jwsSignature = transcodeDerToConcat(signature, 64); - return content + "." + BaseEncoding.base64Url().omitPadding().encode(jwsSignature); + return content + "." + Base64.getUrlEncoder().withoutPadding().encodeToString(jwsSignature); } private static byte[] transcodeDerToConcat(byte[] derSignature, int outputLength) diff --git a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java index 978e8a79d..c2678794d 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java @@ -781,7 +781,7 @@ void serialize() throws IOException, ClassNotFoundException { } @Test - void toString_containsFields() throws IOException { + void toString_containsFields() { String expectedToString = String.format( "GoogleCredentials{quotaProjectId=%s, universeDomain=%s, isExplicitUniverseDomain=%s}", @@ -792,7 +792,7 @@ void toString_containsFields() throws IOException { } @Test - void hashCode_equals() throws IOException { + void hashCode_equals() { GoogleCredentials credentials = GoogleCredentials.newBuilder().setUniverseDomain("some-domain").build(); GoogleCredentials otherCredentials = @@ -801,7 +801,7 @@ void hashCode_equals() throws IOException { } @Test - void equals_true() throws IOException { + void equals_true() { GoogleCredentials credentials = GoogleCredentials.newBuilder().setUniverseDomain("some-domain").build(); GoogleCredentials otherCredentials =