From a4813a2fc9a277462ff5e0d86dee7d73ce90e176 Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Thu, 30 Jul 2026 01:59:16 +0000 Subject: [PATCH 01/10] feat(oauth2): Extract actor tokens for cert-bound OAuth2 STS exchange Implementation of Phase 1-3 of the Cert-Bound Oauth2 Design Document: 1. Extend IdentityPoolCredentialSource to parse actorTokenFieldName. 2. Relax mutual exclusivity to allow BOTH file and certificate configurations. 3. Parse actor_token_type in ExternalAccountCredentials. 4. Refactor FileIdentityPoolTokenSupplier and track file timestamp via volatile CachedFile for the parsed JSON payload. 5. Inject actor_token and actor_token_type into StsTokenExchangeRequest using ActingParty. 6. Enforce that actor token extraction requires an mTLS STS configuration. --- .../oauth2/ExternalAccountCredentials.java | 2 + .../FileIdentityPoolSubjectTokenSupplier.java | 103 ----------- .../oauth2/FileIdentityPoolTokenSupplier.java | 173 ++++++++++++++++++ .../IdentityPoolActorTokenSupplier.java | 48 +++++ .../oauth2/IdentityPoolCredentialSource.java | 11 +- .../auth/oauth2/IdentityPoolCredentials.java | 46 ++++- .../UrlIdentityPoolSubjectTokenSupplier.java | 7 +- .../ExternalAccountCredentialsTest.java | 14 ++ .../FileIdentityPoolTokenSupplierTest.java | 140 ++++++++++++++ .../IdentityPoolCredentialsSourceTest.java | 34 ++++ 10 files changed, 468 insertions(+), 110 deletions(-) delete mode 100644 google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolSubjectTokenSupplier.java create mode 100644 google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java create mode 100644 google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolActorTokenSupplier.java create mode 100644 google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index 917f01fe89e0..4dec0b552270 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -431,6 +431,7 @@ static ExternalAccountCredentials fromJson( Map json, HttpTransportFactory transportFactory) { String audience = (String) json.get("audience"); String subjectTokenType = (String) json.get("subject_token_type"); + String actorTokenType = (String) json.get("actor_token_type"); String tokenUrl = (String) json.get("token_url"); Map credentialSourceMap = (Map) json.get("credential_source"); @@ -487,6 +488,7 @@ static ExternalAccountCredentials fromJson( .setHttpTransportFactory(transportFactory) .setAudience(audience) .setSubjectTokenType(subjectTokenType) + .setActorTokenType(actorTokenType) .setTokenUrl(tokenUrl) .setTokenInfoUrl(tokenInfoUrl) .setCredentialSource(new IdentityPoolCredentialSource(credentialSourceMap)) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolSubjectTokenSupplier.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolSubjectTokenSupplier.java deleted file mode 100644 index 02654578a418..000000000000 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolSubjectTokenSupplier.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following disclaimer - * in the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Google LLC nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package com.google.auth.oauth2; - -import com.google.api.client.json.GenericJson; -import com.google.api.client.json.JsonObjectParser; -import com.google.auth.oauth2.IdentityPoolCredentialSource.CredentialFormatType; -import com.google.common.io.CharStreams; -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.LinkOption; -import java.nio.file.Paths; -import org.jspecify.annotations.NullMarked; - -/** - * Internal provider for retrieving the subject tokens for {@link IdentityPoolCredentials} to - * exchange for GCP access tokens via a local file. - */ -@NullMarked -class FileIdentityPoolSubjectTokenSupplier implements IdentityPoolSubjectTokenSupplier { - - private final long serialVersionUID = 2475549052347431992L; - - private final IdentityPoolCredentialSource credentialSource; - - /** - * Constructor for FileIdentitySubjectTokenProvider - * - * @param credentialSource the credential source to use. - */ - FileIdentityPoolSubjectTokenSupplier(IdentityPoolCredentialSource credentialSource) { - this.credentialSource = credentialSource; - } - - @Override - public String getSubjectToken(ExternalAccountSupplierContext context) throws IOException { - String credentialFilePath = this.credentialSource.getCredentialLocation(); - if (!Files.exists(Paths.get(credentialFilePath), LinkOption.NOFOLLOW_LINKS)) { - throw new IOException( - String.format( - "Invalid credential location. The file at %s does not exist.", credentialFilePath)); - } - try { - return parseToken( - Files.newInputStream(new File(credentialFilePath).toPath()), this.credentialSource); - } catch (IOException e) { - throw new IOException( - "Error when attempting to read the subject token from the credential file.", e); - } - } - - static String parseToken(InputStream inputStream, IdentityPoolCredentialSource credentialSource) - throws IOException { - if (credentialSource.credentialFormatType == CredentialFormatType.TEXT) { - BufferedReader reader = - new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); - return CharStreams.toString(reader); - } - - JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); - GenericJson fileContents = - parser.parseAndClose(inputStream, StandardCharsets.UTF_8, GenericJson.class); - - if (!fileContents.containsKey(credentialSource.subjectTokenFieldName)) { - throw new IOException("Invalid subject token field name. No subject token was found."); - } - return (String) fileContents.get(credentialSource.subjectTokenFieldName); - } -} diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java new file mode 100644 index 000000000000..345fed014c3f --- /dev/null +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java @@ -0,0 +1,173 @@ +/* + * Copyright 2024 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.auth.oauth2; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonObjectParser; +import com.google.auth.oauth2.IdentityPoolCredentialSource.CredentialFormatType; + +import com.google.common.io.CharStreams; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Paths; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * Internal provider for retrieving the subject and actor tokens for {@link IdentityPoolCredentials} + * to exchange for GCP access tokens via a local file. + */ +@NullMarked +class FileIdentityPoolTokenSupplier + implements IdentityPoolSubjectTokenSupplier, IdentityPoolActorTokenSupplier { + + private final long serialVersionUID = 2475549052347431993L; + + private final IdentityPoolCredentialSource credentialSource; + @Nullable private final String targetFieldName; + + private static class CachedFile { + final long lastModified; + final GenericJson parsedJson; + + CachedFile(long lastModified, GenericJson parsedJson) { + this.lastModified = lastModified; + this.parsedJson = parsedJson; + } + } + + private volatile CachedFile cachedFile; + + /** Constructor that defaults to using the subjectTokenFieldName. */ + FileIdentityPoolTokenSupplier(IdentityPoolCredentialSource credentialSource) { + this(credentialSource, credentialSource.subjectTokenFieldName); + } + + /** Overloaded constructor allowing targeting of any specific field in the JSON. */ + FileIdentityPoolTokenSupplier( + IdentityPoolCredentialSource credentialSource, @Nullable String targetFieldName) { + this.credentialSource = checkNotNull(credentialSource, "credentialSource cannot be null"); + this.targetFieldName = targetFieldName; + } + + @Override + public String getSubjectToken(ExternalAccountSupplierContext context) throws IOException { + return getToken(); + } + + @Override + public String getActorToken(ExternalAccountSupplierContext context) throws IOException { + return getToken(); + } + + private String getToken() throws IOException { + String credentialFilePath = credentialSource.getCredentialLocation(); + if (!Files.exists(Paths.get(credentialFilePath), LinkOption.NOFOLLOW_LINKS)) { + throw new IOException( + String.format( + "Invalid credential location. The file at %s does not exist.", credentialFilePath)); + } + + if (credentialSource.credentialFormatType == CredentialFormatType.JSON) { + if (targetFieldName == null) { + throw new IOException("Target field name must be specified for JSON credentials."); + } + File file = new File(credentialFilePath); + long lastModified = file.lastModified(); + + CachedFile cached = this.cachedFile; + + if (cached == null || cached.lastModified < lastModified) { + try (InputStream inputStream = Files.newInputStream(file.toPath())) { + JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); + GenericJson parsedJson = + parser.parseAndClose(inputStream, StandardCharsets.UTF_8, GenericJson.class); + cached = new CachedFile(lastModified, parsedJson); + this.cachedFile = cached; + } catch (Exception e) { + throw new IOException( + "Error when attempting to read the token from the credential file.", e); + } + } + + Object value = cached.parsedJson.get(targetFieldName); + if (value == null) { + throw new IOException( + "Invalid token field name. No token was found for field: " + targetFieldName); + } + return value.toString(); + } + + try (InputStream inputStream = Files.newInputStream(Paths.get(credentialFilePath))) { + BufferedReader reader = + new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + return CharStreams.toString(reader); + } catch (IOException e) { + throw new IOException("Error when attempting to read the token from the credential file.", e); + } + } + + /** Used primarily for UrlIdentityPoolSubjectTokenSupplier */ + static String parseToken( + InputStream inputStream, + IdentityPoolCredentialSource credentialSource, + @Nullable String targetFieldName) + throws IOException { + if (credentialSource.credentialFormatType == CredentialFormatType.TEXT) { + BufferedReader reader = + new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + return CharStreams.toString(reader); + } + + if (targetFieldName == null) { + throw new IOException("Target field name must be specified for JSON credentials."); + } + + JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); + GenericJson fileContents = + parser.parseAndClose(inputStream, StandardCharsets.UTF_8, GenericJson.class); + + if (!fileContents.containsKey(targetFieldName)) { + throw new IOException( + "Invalid token field name. No token was found for field: " + targetFieldName); + } + return (String) fileContents.get(targetFieldName); + } +} diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolActorTokenSupplier.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolActorTokenSupplier.java new file mode 100644 index 000000000000..041b7e16c118 --- /dev/null +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolActorTokenSupplier.java @@ -0,0 +1,48 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.auth.oauth2; + +import java.io.IOException; + +/** Functional interface for supplying an actor token for IdentityPool credentials. */ +@FunctionalInterface +interface IdentityPoolActorTokenSupplier extends java.io.Serializable { + + /** + * Returns a valid actor token as a string. + * + * @param context the context to use to fetch the actor token + * @return the actor token string + * @throws IOException if there was an error retrieving the token + */ + String getActorToken(ExternalAccountSupplierContext context) throws IOException; +} diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentialSource.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentialSource.java index 5ade1458b8d7..350b32f5c63e 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentialSource.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentialSource.java @@ -51,6 +51,7 @@ public class IdentityPoolCredentialSource extends ExternalAccountCredentials.Cre CredentialFormatType credentialFormatType; private String credentialLocation; @Nullable String subjectTokenFieldName; + @Nullable String actorTokenFieldName; @Nullable Map headers; @Nullable private CertificateConfig certificateConfig; @@ -261,16 +262,17 @@ public IdentityPoolCredentialSource(Map credentialSourceMap) { boolean urlPresent = credentialSourceMap.containsKey("url"); boolean certificatePresent = credentialSourceMap.containsKey("certificate"); - if ((filePresent && urlPresent) - || (filePresent && certificatePresent) - || (urlPresent && certificatePresent)) { + if ((filePresent && urlPresent) || (urlPresent && certificatePresent)) { throw new IllegalArgumentException( - "Only one credential source type can be set: 'file', 'url', or 'certificate'."); + "A credential source type of URL can not be used with other credential source types."); } if (filePresent) { credentialLocation = (String) credentialSourceMap.get("file"); credentialSourceType = IdentityPoolCredentialSourceType.FILE; + if (certificatePresent) { + this.certificateConfig = certificateConfigFromSourceMap(credentialSourceMap); + } } else if (urlPresent) { credentialLocation = (String) credentialSourceMap.get("url"); credentialSourceType = IdentityPoolCredentialSourceType.URL; @@ -303,6 +305,7 @@ public IdentityPoolCredentialSource(Map credentialSourceMap) { } credentialFormatType = CredentialFormatType.JSON; subjectTokenFieldName = formatMap.get("subject_token_field_name"); + actorTokenFieldName = formatMap.get("actor_token_field_name"); } else if (type != null && "text".equals(type.toLowerCase(Locale.US))) { credentialFormatType = CredentialFormatType.TEXT; } else { diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java index 10f216139d7b..2ea9444b4498 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @@ -60,6 +60,8 @@ public class IdentityPoolCredentials extends ExternalAccountCredentials { private static final long serialVersionUID = 2471046175477275881L; private final IdentityPoolSubjectTokenSupplier subjectTokenSupplier; + @Nullable private final IdentityPoolActorTokenSupplier actorTokenSupplier; + @Nullable private final String actorTokenType; private final ExternalAccountSupplierContext supplierContext; private final String metricsHeaderValue; @@ -89,7 +91,7 @@ public class IdentityPoolCredentials extends ExternalAccountCredentials { this.subjectTokenSupplier = builder.subjectTokenSupplier; this.metricsHeaderValue = PROGRAMMATIC_METRICS_HEADER_VALUE; } else if (credentialSource.credentialSourceType == IdentityPoolCredentialSourceType.FILE) { - this.subjectTokenSupplier = new FileIdentityPoolSubjectTokenSupplier(credentialSource); + this.subjectTokenSupplier = new FileIdentityPoolTokenSupplier(credentialSource); this.metricsHeaderValue = FILE_METRICS_HEADER_VALUE; } else if (credentialSource.credentialSourceType == IdentityPoolCredentialSourceType.URL) { this.subjectTokenSupplier = @@ -111,6 +113,22 @@ public class IdentityPoolCredentials extends ExternalAccountCredentials { } else { throw new IllegalArgumentException("Source type not supported."); } + + this.actorTokenType = builder.actorTokenType; + if (builder.actorTokenSupplier != null) { + this.actorTokenSupplier = builder.actorTokenSupplier; + } else if (credentialSource != null && credentialSource.actorTokenFieldName != null) { + this.actorTokenSupplier = + new FileIdentityPoolTokenSupplier(credentialSource, credentialSource.actorTokenFieldName); + } else { + this.actorTokenSupplier = null; + } + + if (this.actorTokenSupplier != null + && (getTokenUrl() == null || !getTokenUrl().contains("mtls.googleapis.com"))) { + throw new IllegalArgumentException( + "Actor tokens are only supported for mTLS token URLs."); + } } @Override @@ -120,6 +138,11 @@ public AccessToken refreshAccessToken() throws IOException { StsTokenExchangeRequest.newBuilder(credential, getSubjectTokenType()) .setAudience(getAudience()); + if (this.actorTokenSupplier != null && this.actorTokenType != null) { + String actorToken = this.actorTokenSupplier.getActorToken(supplierContext); + stsTokenExchangeRequest.setActingParty(new ActingParty(actorToken, this.actorTokenType)); + } + Collection scopes = getScopes(); if (scopes != null && !scopes.isEmpty()) { stsTokenExchangeRequest.setScopes(new ArrayList<>(scopes)); @@ -143,6 +166,11 @@ IdentityPoolSubjectTokenSupplier getIdentityPoolSubjectTokenSupplier() { return this.subjectTokenSupplier; } + @VisibleForTesting + String getActorTokenType() { + return this.actorTokenType; + } + /** Clones the IdentityPoolCredentials with the specified scopes. */ @Override public IdentityPoolCredentials createScoped(Collection newScopes) { @@ -205,6 +233,8 @@ private X509Provider getX509Provider( public static class Builder extends ExternalAccountCredentials.Builder { private IdentityPoolSubjectTokenSupplier subjectTokenSupplier; + private IdentityPoolActorTokenSupplier actorTokenSupplier; + private String actorTokenType; private X509Provider x509Provider; Builder() {} @@ -213,7 +243,9 @@ public static class Builder extends ExternalAccountCredentials.Builder { super(credentials); if (this.credentialSource == null) { this.subjectTokenSupplier = credentials.subjectTokenSupplier; + this.actorTokenSupplier = credentials.actorTokenSupplier; } + this.actorTokenType = credentials.actorTokenType; } /** @@ -244,6 +276,18 @@ public Builder setSubjectTokenSupplier(IdentityPoolSubjectTokenSupplier subjectT return this; } + @CanIgnoreReturnValue + public Builder setActorTokenSupplier(IdentityPoolActorTokenSupplier actorTokenSupplier) { + this.actorTokenSupplier = actorTokenSupplier; + return this; + } + + @CanIgnoreReturnValue + public Builder setActorTokenType(String actorTokenType) { + this.actorTokenType = actorTokenType; + return this; + } + @CanIgnoreReturnValue public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) { super.setHttpTransportFactory(transportFactory); diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/UrlIdentityPoolSubjectTokenSupplier.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/UrlIdentityPoolSubjectTokenSupplier.java index 9a95701371b3..31749059c1a8 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/UrlIdentityPoolSubjectTokenSupplier.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/UrlIdentityPoolSubjectTokenSupplier.java @@ -31,7 +31,7 @@ package com.google.auth.oauth2; -import static com.google.auth.oauth2.FileIdentityPoolSubjectTokenSupplier.parseToken; +import static com.google.auth.oauth2.FileIdentityPoolTokenSupplier.parseToken; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpHeaders; @@ -94,7 +94,10 @@ public String getSubjectToken(ExternalAccountSupplierContext context) throws IOE HttpResponse response = request.execute(); LoggingUtils.logResponse( response, LOGGER_PROVIDER, "Received response for subject token request"); - return parseToken(response.getContent(), this.credentialSource); + return parseToken( + response.getContent(), + this.credentialSource, + this.credentialSource.subjectTokenFieldName); } catch (IOException e) { throw new IOException( String.format("Error getting subject token from metadata server: %s", e.getMessage()), e); diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index 1338c0d68fe9..c4632cedd9ef 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -200,6 +200,20 @@ void fromJson_identityPoolCredentialsWorkload() { assertEquals(GOOGLE_DEFAULT_UNIVERSE, credential.getUniverseDomain()); } + @Test + void fromJson_identityPoolCredentials_withActorTokenType() { + GenericJson json = buildJsonIdentityPoolCredential(); + json.put("actor_token_type", "actorTokenType"); + + ExternalAccountCredentials credential = + ExternalAccountCredentials.fromJson(json, OAuth2Utils.HTTP_TRANSPORT_FACTORY); + + assertInstanceOf(IdentityPoolCredentials.class, credential); + IdentityPoolCredentials idpCreds = (IdentityPoolCredentials) credential; + assertEquals("subjectTokenType", idpCreds.getSubjectTokenType()); + assertEquals("actorTokenType", idpCreds.getActorTokenType()); + } + @Test void fromJson_identityPoolCredentialsWorkforce() { ExternalAccountCredentials credential = diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java new file mode 100644 index 000000000000..cc255a2a3c7b --- /dev/null +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java @@ -0,0 +1,140 @@ +/* + * Copyright 2024 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.auth.oauth2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class FileIdentityPoolTokenSupplierTest { + + @Test + void getToken_textFormat(@TempDir Path tempDir) throws IOException { + Path credentialFile = tempDir.resolve("credential.txt"); + Files.write(credentialFile, "plain_token".getBytes()); + + Map credentialSourceMap = new HashMap<>(); + credentialSourceMap.put("file", credentialFile.toString()); + + IdentityPoolCredentialSource source = new IdentityPoolCredentialSource(credentialSourceMap); + FileIdentityPoolTokenSupplier supplier = + new FileIdentityPoolTokenSupplier(source, null); // TEXT doesn't need targetFieldName + + assertEquals("plain_token", supplier.getSubjectToken(null)); + assertEquals("plain_token", supplier.getActorToken(null)); + } + + @Test + void getToken_jsonFormat_cachingLogic(@TempDir Path tempDir) + throws IOException, InterruptedException { + Path credentialFile = tempDir.resolve("credential.json"); + Files.write( + credentialFile, + "{\"sub_token\": \"my_sub_token\", \"act_token\": \"my_act_token\"}".getBytes()); + + Map credentialSourceMap = new HashMap<>(); + credentialSourceMap.put("file", credentialFile.toString()); + Map formatMap = new HashMap<>(); + formatMap.put("type", "json"); + formatMap.put("subject_token_field_name", "sub_token"); + formatMap.put("actor_token_field_name", "act_token"); + credentialSourceMap.put("format", formatMap); + + IdentityPoolCredentialSource source = new IdentityPoolCredentialSource(credentialSourceMap); + FileIdentityPoolTokenSupplier subSupplier = + new FileIdentityPoolTokenSupplier(source, source.subjectTokenFieldName); + FileIdentityPoolTokenSupplier actSupplier = + new FileIdentityPoolTokenSupplier(source, source.actorTokenFieldName); + + // Initial read + assertEquals("my_sub_token", subSupplier.getSubjectToken(null)); + assertEquals("my_act_token", actSupplier.getActorToken(null)); + + // Wait 10ms for mtime to definitely advance for the reload logic + Thread.sleep(10); + + // Modify file + Files.write( + credentialFile, "{\"sub_token\": \"new_sub\", \"act_token\": \"new_act\"}".getBytes()); + + // Validate we read the new token after file modification + assertEquals("new_sub", subSupplier.getSubjectToken(null)); + assertEquals("new_act", actSupplier.getActorToken(null)); + } + + @Test + void getToken_jsonFormat_invalidField(@TempDir Path tempDir) throws IOException { + Path credentialFile = tempDir.resolve("credential.json"); + Files.write(credentialFile, "{\"sub_token\": \"my_sub_token\"}".getBytes()); + + Map credentialSourceMap = new HashMap<>(); + credentialSourceMap.put("file", credentialFile.toString()); + Map formatMap = new HashMap<>(); + formatMap.put("type", "json"); + formatMap.put("subject_token_field_name", "sub_token"); + formatMap.put("actor_token_field_name", "act_token"); + credentialSourceMap.put("format", formatMap); + + IdentityPoolCredentialSource source = new IdentityPoolCredentialSource(credentialSourceMap); + FileIdentityPoolTokenSupplier actSupplier = + new FileIdentityPoolTokenSupplier(source, source.actorTokenFieldName); + + IOException exception = assertThrows(IOException.class, () -> actSupplier.getActorToken(null)); + assertEquals( + "Invalid token field name. No token was found for field: act_token", + exception.getMessage()); + } + + @Test + void getToken_missingFile_throws(@TempDir Path tempDir) { + Path credentialFile = tempDir.resolve("missing_file.txt"); + + Map credentialSourceMap = new HashMap<>(); + credentialSourceMap.put("file", credentialFile.toString()); + + IdentityPoolCredentialSource source = new IdentityPoolCredentialSource(credentialSourceMap); + FileIdentityPoolTokenSupplier supplier = new FileIdentityPoolTokenSupplier(source); + + IOException exception = assertThrows(IOException.class, () -> supplier.getSubjectToken(null)); + assertEquals( + String.format( + "Invalid credential location. The file at %s does not exist.", credentialFile), + exception.getMessage()); + } +} diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsSourceTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsSourceTest.java index aecd82f94d3d..7885b9d00e3f 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsSourceTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsSourceTest.java @@ -152,4 +152,38 @@ void constructor_certificateConfig_invalidType_throws() { "Invalid type for 'use_default_certificate_config' in certificate configuration: expected Boolean, got String.", exception.getMessage()); } + + @Test + void constructor_fileAndCertificatePresent_isSupported() { + Map certificateMap = new HashMap<>(); + certificateMap.put("use_default_certificate_config", true); + + Map credentialSourceMap = new HashMap<>(); + credentialSourceMap.put("file", "/path/to/file"); + credentialSourceMap.put("certificate", certificateMap); + + IdentityPoolCredentialSource credentialSource = + new IdentityPoolCredentialSource(credentialSourceMap); + assertEquals(IdentityPoolCredentialSourceType.FILE, credentialSource.credentialSourceType); + assertEquals("/path/to/file", credentialSource.getCredentialLocation()); + assertNotNull(credentialSource.getCertificateConfig()); + assertTrue(credentialSource.getCertificateConfig().useDefaultCertificateConfig()); + } + + @Test + void constructor_jsonFormat_withActorTokenFieldName() { + Map formatMap = new HashMap<>(); + formatMap.put("type", "json"); + formatMap.put("subject_token_field_name", "sub_field"); + formatMap.put("actor_token_field_name", "act_field"); + + Map credentialSourceMap = new HashMap<>(); + credentialSourceMap.put("file", "/path/to/file"); + credentialSourceMap.put("format", formatMap); + + IdentityPoolCredentialSource credentialSource = + new IdentityPoolCredentialSource(credentialSourceMap); + assertEquals("sub_field", credentialSource.subjectTokenFieldName); + assertEquals("act_field", credentialSource.actorTokenFieldName); + } } From c739b375fe9afd6ae3e4aed3ee9c58c7e0374c68 Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Thu, 30 Jul 2026 02:28:58 +0000 Subject: [PATCH 02/10] fix(oauth2): share parsed JSON cache between subject and actor tokens --- .../oauth2/FileIdentityPoolTokenSupplier.java | 16 ++++------------ .../auth/oauth2/IdentityPoolCredentials.java | 7 +++++-- .../FileIdentityPoolTokenSupplierTest.java | 8 ++++---- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java index 345fed014c3f..6e83931dda3c 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java @@ -61,7 +61,6 @@ class FileIdentityPoolTokenSupplier private final long serialVersionUID = 2475549052347431993L; private final IdentityPoolCredentialSource credentialSource; - @Nullable private final String targetFieldName; private static class CachedFile { final long lastModified; @@ -75,29 +74,21 @@ private static class CachedFile { private volatile CachedFile cachedFile; - /** Constructor that defaults to using the subjectTokenFieldName. */ FileIdentityPoolTokenSupplier(IdentityPoolCredentialSource credentialSource) { - this(credentialSource, credentialSource.subjectTokenFieldName); - } - - /** Overloaded constructor allowing targeting of any specific field in the JSON. */ - FileIdentityPoolTokenSupplier( - IdentityPoolCredentialSource credentialSource, @Nullable String targetFieldName) { this.credentialSource = checkNotNull(credentialSource, "credentialSource cannot be null"); - this.targetFieldName = targetFieldName; } @Override public String getSubjectToken(ExternalAccountSupplierContext context) throws IOException { - return getToken(); + return getToken(credentialSource.subjectTokenFieldName); } @Override public String getActorToken(ExternalAccountSupplierContext context) throws IOException { - return getToken(); + return getToken(credentialSource.actorTokenFieldName); } - private String getToken() throws IOException { + private String getToken(@Nullable String targetFieldName) throws IOException { String credentialFilePath = credentialSource.getCredentialLocation(); if (!Files.exists(Paths.get(credentialFilePath), LinkOption.NOFOLLOW_LINKS)) { throw new IOException( @@ -171,3 +162,4 @@ static String parseToken( return (String) fileContents.get(targetFieldName); } } + diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java index 2ea9444b4498..bb4acd87e562 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @@ -118,8 +118,11 @@ public class IdentityPoolCredentials extends ExternalAccountCredentials { if (builder.actorTokenSupplier != null) { this.actorTokenSupplier = builder.actorTokenSupplier; } else if (credentialSource != null && credentialSource.actorTokenFieldName != null) { - this.actorTokenSupplier = - new FileIdentityPoolTokenSupplier(credentialSource, credentialSource.actorTokenFieldName); + if (this.subjectTokenSupplier instanceof FileIdentityPoolTokenSupplier) { + this.actorTokenSupplier = (FileIdentityPoolTokenSupplier) this.subjectTokenSupplier; + } else { + this.actorTokenSupplier = new FileIdentityPoolTokenSupplier(credentialSource); + } } else { this.actorTokenSupplier = null; } diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java index cc255a2a3c7b..d29b8650efb0 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java @@ -54,7 +54,7 @@ void getToken_textFormat(@TempDir Path tempDir) throws IOException { IdentityPoolCredentialSource source = new IdentityPoolCredentialSource(credentialSourceMap); FileIdentityPoolTokenSupplier supplier = - new FileIdentityPoolTokenSupplier(source, null); // TEXT doesn't need targetFieldName + new FileIdentityPoolTokenSupplier(source); // TEXT doesn't need targetFieldName assertEquals("plain_token", supplier.getSubjectToken(null)); assertEquals("plain_token", supplier.getActorToken(null)); @@ -78,9 +78,9 @@ void getToken_jsonFormat_cachingLogic(@TempDir Path tempDir) IdentityPoolCredentialSource source = new IdentityPoolCredentialSource(credentialSourceMap); FileIdentityPoolTokenSupplier subSupplier = - new FileIdentityPoolTokenSupplier(source, source.subjectTokenFieldName); + new FileIdentityPoolTokenSupplier(source); FileIdentityPoolTokenSupplier actSupplier = - new FileIdentityPoolTokenSupplier(source, source.actorTokenFieldName); + new FileIdentityPoolTokenSupplier(source); // Initial read assertEquals("my_sub_token", subSupplier.getSubjectToken(null)); @@ -113,7 +113,7 @@ void getToken_jsonFormat_invalidField(@TempDir Path tempDir) throws IOException IdentityPoolCredentialSource source = new IdentityPoolCredentialSource(credentialSourceMap); FileIdentityPoolTokenSupplier actSupplier = - new FileIdentityPoolTokenSupplier(source, source.actorTokenFieldName); + new FileIdentityPoolTokenSupplier(source); IOException exception = assertThrows(IOException.class, () -> actSupplier.getActorToken(null)); assertEquals( From 1a32e6f1f654f7da7f85cb3fd592ba951801a877 Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Thu, 30 Jul 2026 02:34:40 +0000 Subject: [PATCH 03/10] feat(oauth2): fail loudly if actor token requested against non-file source --- .../java/com/google/auth/oauth2/IdentityPoolCredentials.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java index bb4acd87e562..435098bf4a79 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @@ -121,7 +121,8 @@ public class IdentityPoolCredentials extends ExternalAccountCredentials { if (this.subjectTokenSupplier instanceof FileIdentityPoolTokenSupplier) { this.actorTokenSupplier = (FileIdentityPoolTokenSupplier) this.subjectTokenSupplier; } else { - this.actorTokenSupplier = new FileIdentityPoolTokenSupplier(credentialSource); + throw new IllegalArgumentException( + "Actor tokens are currently only supported for file-based credential sources."); } } else { this.actorTokenSupplier = null; From c42bd537a8349fa8efa099ad7ca8d1f89e19ac03 Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Thu, 30 Jul 2026 02:42:49 +0000 Subject: [PATCH 04/10] feat(oauth2): relax actor token mTLS URL validation to .mtls. for PSC / universes --- .../java/com/google/auth/oauth2/IdentityPoolCredentials.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java index 435098bf4a79..02d1e18e4080 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @@ -129,7 +129,7 @@ public class IdentityPoolCredentials extends ExternalAccountCredentials { } if (this.actorTokenSupplier != null - && (getTokenUrl() == null || !getTokenUrl().contains("mtls.googleapis.com"))) { + && (getTokenUrl() == null || !getTokenUrl().contains(".mtls."))) { throw new IllegalArgumentException( "Actor tokens are only supported for mTLS token URLs."); } From 803dd53b8071c8ad96aafc8ea53d420b743b87ca Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Thu, 30 Jul 2026 03:04:37 +0000 Subject: [PATCH 05/10] test(oauth2): add tests for strict actor token exceptions --- .../oauth2/FileIdentityPoolTokenSupplier.java | 2 - .../auth/oauth2/IdentityPoolCredentials.java | 3 +- .../FileIdentityPoolTokenSupplierTest.java | 9 +-- .../oauth2/IdentityPoolCredentialsTest.java | 57 +++++++++++++++++++ 4 files changed, 61 insertions(+), 10 deletions(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java index 6e83931dda3c..6782691a385b 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java @@ -36,7 +36,6 @@ import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonObjectParser; import com.google.auth.oauth2.IdentityPoolCredentialSource.CredentialFormatType; - import com.google.common.io.CharStreams; import java.io.BufferedReader; import java.io.File; @@ -162,4 +161,3 @@ static String parseToken( return (String) fileContents.get(targetFieldName); } } - diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java index 02d1e18e4080..4409a585a3e8 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @@ -130,8 +130,7 @@ public class IdentityPoolCredentials extends ExternalAccountCredentials { if (this.actorTokenSupplier != null && (getTokenUrl() == null || !getTokenUrl().contains(".mtls."))) { - throw new IllegalArgumentException( - "Actor tokens are only supported for mTLS token URLs."); + throw new IllegalArgumentException("Actor tokens are only supported for mTLS token URLs."); } } diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java index d29b8650efb0..4747e0cfb463 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java @@ -77,10 +77,8 @@ void getToken_jsonFormat_cachingLogic(@TempDir Path tempDir) credentialSourceMap.put("format", formatMap); IdentityPoolCredentialSource source = new IdentityPoolCredentialSource(credentialSourceMap); - FileIdentityPoolTokenSupplier subSupplier = - new FileIdentityPoolTokenSupplier(source); - FileIdentityPoolTokenSupplier actSupplier = - new FileIdentityPoolTokenSupplier(source); + FileIdentityPoolTokenSupplier subSupplier = new FileIdentityPoolTokenSupplier(source); + FileIdentityPoolTokenSupplier actSupplier = new FileIdentityPoolTokenSupplier(source); // Initial read assertEquals("my_sub_token", subSupplier.getSubjectToken(null)); @@ -112,8 +110,7 @@ void getToken_jsonFormat_invalidField(@TempDir Path tempDir) throws IOException credentialSourceMap.put("format", formatMap); IdentityPoolCredentialSource source = new IdentityPoolCredentialSource(credentialSourceMap); - FileIdentityPoolTokenSupplier actSupplier = - new FileIdentityPoolTokenSupplier(source); + FileIdentityPoolTokenSupplier actSupplier = new FileIdentityPoolTokenSupplier(source); IOException exception = assertThrows(IOException.class, () -> actSupplier.getActorToken(null)); assertEquals( diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java index a1662cc10191..30e64f8def84 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java @@ -1299,4 +1299,61 @@ void setShouldThrowOnGetKeyStore(boolean shouldThrow) { this.shouldThrowOnGetKeyStore = shouldThrow; } } + + @Test + void builder_actorTokenWithInvalidUrl_throws() { + IdentityPoolCredentialSource credentialSource = createFileCredentialSource(); + + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> + IdentityPoolCredentials.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl("https://invalid.googleapis.com/") // Does not contain .mtls. + .setCredentialSource(credentialSource) + .setActorTokenSupplier( + new IdentityPoolActorTokenSupplier() { + @Override + public String getActorToken(ExternalAccountSupplierContext context) { + return "token"; + } + }) + .build()); + + assertEquals("Actor tokens are only supported for mTLS token URLs.", e.getMessage()); + } + + @Test + void builder_actorTokenWithInvalidCredentialSource_throws() { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + Map formatMap = new HashMap<>(); + formatMap.put("type", "json"); + formatMap.put("subject_token_field_name", "subject_token"); + formatMap.put("actor_token_field_name", "actor_token"); + + // Not a file credential source + IdentityPoolCredentialSource credentialSource = + buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl(), formatMap); + + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> + IdentityPoolCredentials.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl("https://sts.mtls.googleapis.com/") // Valid URL + .setCredentialSource(credentialSource) // Invalid source for actor tokens + .build()); + + assertEquals( + "Actor tokens are currently only supported for file-based credential sources.", + e.getMessage()); + } } From cbcce28c1339f4cb04f2aa65380c6954a8d77543 Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Thu, 30 Jul 2026 03:09:00 +0000 Subject: [PATCH 06/10] chore(oauth2): update copyright year to 2026 for new files --- .../com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java | 2 +- .../google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java index 6782691a385b..7c4ab9a619ec 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Google LLC + * Copyright 2026 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java index 4747e0cfb463..5afea1385741 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Google LLC + * Copyright 2026 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are From 745d26b1dc243460a109da8a4d726316ed31082f Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Thu, 30 Jul 2026 20:44:55 +0000 Subject: [PATCH 07/10] Address architectural review feedback from paste 5381957298028544 Fixes test failures and thread synchronization bugs regarding actor token credentials from https://paste.googleplex.com/5381957298028544 --- .../oauth2/FileIdentityPoolTokenSupplier.java | 58 ++++++++++-------- .../auth/oauth2/IdentityPoolCredentials.java | 35 ++++++++++- .../ExternalAccountCredentialsTest.java | 12 +++- .../FileIdentityPoolTokenSupplierTest.java | 55 ++++++++++++++++- .../oauth2/IdentityPoolCredentialsTest.java | 59 ++++++++++++++++++- 5 files changed, 190 insertions(+), 29 deletions(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java index 7c4ab9a619ec..97beb38deffd 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolTokenSupplier.java @@ -84,6 +84,10 @@ public String getSubjectToken(ExternalAccountSupplierContext context) throws IOE @Override public String getActorToken(ExternalAccountSupplierContext context) throws IOException { + if (credentialSource.credentialFormatType == CredentialFormatType.TEXT) { + throw new IllegalArgumentException( + "Actor tokens are only supported for JSON-formatted credential files with distinct field names."); + } return getToken(credentialSource.actorTokenFieldName); } @@ -105,15 +109,20 @@ private String getToken(@Nullable String targetFieldName) throws IOException { CachedFile cached = this.cachedFile; if (cached == null || cached.lastModified < lastModified) { - try (InputStream inputStream = Files.newInputStream(file.toPath())) { - JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); - GenericJson parsedJson = - parser.parseAndClose(inputStream, StandardCharsets.UTF_8, GenericJson.class); - cached = new CachedFile(lastModified, parsedJson); - this.cachedFile = cached; - } catch (Exception e) { - throw new IOException( - "Error when attempting to read the token from the credential file.", e); + synchronized (this) { + cached = this.cachedFile; + if (cached == null || cached.lastModified < lastModified) { + try (InputStream inputStream = Files.newInputStream(file.toPath())) { + JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); + GenericJson parsedJson = + parser.parseAndClose(inputStream, StandardCharsets.UTF_8, GenericJson.class); + cached = new CachedFile(lastModified, parsedJson); + this.cachedFile = cached; + } catch (Exception e) { + throw new IOException( + "Error when attempting to read the token from the credential file.", e); + } + } } } @@ -140,24 +149,25 @@ static String parseToken( IdentityPoolCredentialSource credentialSource, @Nullable String targetFieldName) throws IOException { - if (credentialSource.credentialFormatType == CredentialFormatType.TEXT) { - BufferedReader reader = - new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); - return CharStreams.toString(reader); - } + try (InputStream in = inputStream; + java.io.Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { + if (credentialSource.credentialFormatType == CredentialFormatType.TEXT) { + return CharStreams.toString(new BufferedReader(reader)); + } - if (targetFieldName == null) { - throw new IOException("Target field name must be specified for JSON credentials."); - } + if (targetFieldName == null) { + throw new IOException("Target field name must be specified for JSON credentials."); + } - JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); - GenericJson fileContents = - parser.parseAndClose(inputStream, StandardCharsets.UTF_8, GenericJson.class); + JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); + GenericJson fileContents = + parser.parseAndClose(in, StandardCharsets.UTF_8, GenericJson.class); - if (!fileContents.containsKey(targetFieldName)) { - throw new IOException( - "Invalid token field name. No token was found for field: " + targetFieldName); + if (!fileContents.containsKey(targetFieldName)) { + throw new IOException( + "Invalid token field name. No token was found for field: " + targetFieldName); + } + return (String) fileContents.get(targetFieldName); } - return (String) fileContents.get(targetFieldName); } } diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java index 4409a585a3e8..155d53b75270 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @@ -50,6 +50,11 @@ * Url-sourced, file-sourced, or user provided supplier method-sourced external account credentials. * *

By default, attempts to exchange the external credential for a GCP access token. + * + *

Note: Actor token extraction is currently restricted to file-based JSON credential sources + * over mTLS endpoints. When configuring certificate-bound OAuth 2.0 tokens for GAX channel + * providers, ensure you configure {@code + * InstantiatingGrpcChannelProvider.newBuilder().setMtlsProvider(...)} in tandem. */ @NullMarked public class IdentityPoolCredentials extends ExternalAccountCredentials { @@ -62,6 +67,7 @@ public class IdentityPoolCredentials extends ExternalAccountCredentials { private final IdentityPoolSubjectTokenSupplier subjectTokenSupplier; @Nullable private final IdentityPoolActorTokenSupplier actorTokenSupplier; @Nullable private final String actorTokenType; + @Nullable private final X509Provider x509Provider; private final ExternalAccountSupplierContext supplierContext; private final String metricsHeaderValue; @@ -91,6 +97,17 @@ public class IdentityPoolCredentials extends ExternalAccountCredentials { this.subjectTokenSupplier = builder.subjectTokenSupplier; this.metricsHeaderValue = PROGRAMMATIC_METRICS_HEADER_VALUE; } else if (credentialSource.credentialSourceType == IdentityPoolCredentialSourceType.FILE) { + if (credentialSource.getCertificateConfig() != null) { + try { + X509Provider x509Provider = getX509Provider(builder, credentialSource); + KeyStore mtlsKeyStore = x509Provider.getKeyStore(); + this.transportFactory = new MtlsHttpTransportFactory(mtlsKeyStore); + } catch (Exception e) { + throw new RuntimeException( + "Failed to initialize mTLS transport for file credential source due to certificate error.", + e); + } + } this.subjectTokenSupplier = new FileIdentityPoolTokenSupplier(credentialSource); this.metricsHeaderValue = FILE_METRICS_HEADER_VALUE; } else if (credentialSource.credentialSourceType == IdentityPoolCredentialSourceType.URL) { @@ -129,9 +146,22 @@ public class IdentityPoolCredentials extends ExternalAccountCredentials { } if (this.actorTokenSupplier != null - && (getTokenUrl() == null || !getTokenUrl().contains(".mtls."))) { - throw new IllegalArgumentException("Actor tokens are only supported for mTLS token URLs."); + && (this.actorTokenType == null || this.actorTokenType.trim().isEmpty())) { + throw new IllegalArgumentException( + "An actorTokenType must be specified when an actorTokenSupplier is configured."); + } + if (this.actorTokenSupplier == null && this.actorTokenType != null) { + throw new IllegalArgumentException( + "An actorTokenSupplier must be specified when an actorTokenType is configured."); + } + + if (this.actorTokenSupplier != null + && !(this.transportFactory instanceof MtlsHttpTransportFactory)) { + throw new IllegalArgumentException( + "Actor tokens are only supported for mTLS token exchanges. Please configure a certificate source or MtlsHttpTransportFactory."); } + + this.x509Provider = builder.x509Provider; } @Override @@ -249,6 +279,7 @@ public static class Builder extends ExternalAccountCredentials.Builder { this.actorTokenSupplier = credentials.actorTokenSupplier; } this.actorTokenType = credentials.actorTokenType; + this.x509Provider = credentials.x509Provider; } /** diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index c4632cedd9ef..d81aa8c210b0 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -205,8 +205,18 @@ void fromJson_identityPoolCredentials_withActorTokenType() { GenericJson json = buildJsonIdentityPoolCredential(); json.put("actor_token_type", "actorTokenType"); + Map credentialSource = (Map) json.get("credential_source"); + Map formatMap = new HashMap<>(); + formatMap.put("type", "json"); + formatMap.put("actor_token_field_name", "actor_token"); + formatMap.put("subject_token_field_name", "subject_token"); + credentialSource.put("format", formatMap); + + com.google.auth.mtls.MtlsHttpTransportFactory mockTransportFactory = + org.mockito.Mockito.mock(com.google.auth.mtls.MtlsHttpTransportFactory.class); + ExternalAccountCredentials credential = - ExternalAccountCredentials.fromJson(json, OAuth2Utils.HTTP_TRANSPORT_FACTORY); + ExternalAccountCredentials.fromJson(json, mockTransportFactory); assertInstanceOf(IdentityPoolCredentials.class, credential); IdentityPoolCredentials idpCreds = (IdentityPoolCredentials) credential; diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java index 5afea1385741..4253f979405a 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/FileIdentityPoolTokenSupplierTest.java @@ -57,7 +57,12 @@ void getToken_textFormat(@TempDir Path tempDir) throws IOException { new FileIdentityPoolTokenSupplier(source); // TEXT doesn't need targetFieldName assertEquals("plain_token", supplier.getSubjectToken(null)); - assertEquals("plain_token", supplier.getActorToken(null)); + + IllegalArgumentException exception = + assertThrows(IllegalArgumentException.class, () -> supplier.getActorToken(null)); + assertEquals( + "Actor tokens are only supported for JSON-formatted credential files with distinct field names.", + exception.getMessage()); } @Test @@ -96,6 +101,54 @@ void getToken_jsonFormat_cachingLogic(@TempDir Path tempDir) assertEquals("new_act", actSupplier.getActorToken(null)); } + @Test + void getToken_jsonFormat_cachingLogic_multithreaded(@TempDir Path tempDir) + throws IOException, InterruptedException { + Path credentialFile = tempDir.resolve("credential.json"); + Files.write( + credentialFile, + "{\"sub_token\": \"my_sub_token\", \"act_token\": \"my_act_token\"}".getBytes()); + + Map credentialSourceMap = new HashMap<>(); + credentialSourceMap.put("file", credentialFile.toString()); + Map formatMap = new HashMap<>(); + formatMap.put("type", "json"); + formatMap.put("subject_token_field_name", "sub_token"); + formatMap.put("actor_token_field_name", "act_token"); + credentialSourceMap.put("format", formatMap); + + IdentityPoolCredentialSource source = new IdentityPoolCredentialSource(credentialSourceMap); + FileIdentityPoolTokenSupplier supplier = new FileIdentityPoolTokenSupplier(source); + + int numThreads = 10; + java.util.concurrent.ExecutorService executor = + java.util.concurrent.Executors.newFixedThreadPool(numThreads); + java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(numThreads); + java.util.List> futures = new java.util.ArrayList<>(); + + for (int i = 0; i < numThreads; i++) { + futures.add( + executor.submit( + () -> { + latch.countDown(); + latch.await(); + assertEquals("my_sub_token", supplier.getSubjectToken(null)); + assertEquals("my_act_token", supplier.getActorToken(null)); + return null; + })); + } + + // Wait for all threads to complete and verify no exceptions were thrown + for (java.util.concurrent.Future future : futures) { + try { + future.get(); + } catch (Exception e) { + throw new RuntimeException("Thread execution failed", e); + } + } + executor.shutdown(); + } + @Test void getToken_jsonFormat_invalidField(@TempDir Path tempDir) throws IOException { Path credentialFile = tempDir.resolve("credential.json"); diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java index 30e64f8def84..8b7843ec7a21 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java @@ -254,6 +254,32 @@ void retrieveSubjectToken_urlSourcedWithJsonFormat() throws IOException { assertEquals(transportFactory.transport.getSubjectToken(), subjectToken); } + @Test + void retrieveSubjectToken_urlSourcedWithJsonFormat_withActorTokenField() throws IOException { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + transportFactory.transport.setMetadataServerContentType("json"); + + Map formatMap = new HashMap<>(); + formatMap.put("type", "json"); + formatMap.put("subject_token_field_name", "subjectToken"); + formatMap.put("actor_token_field_name", "actorToken"); + + IdentityPoolCredentialSource credentialSource = + buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl(), formatMap); + + UrlIdentityPoolSubjectTokenSupplier supplier = + new UrlIdentityPoolSubjectTokenSupplier(credentialSource, transportFactory); + + ExternalAccountSupplierContext dummyContext = + ExternalAccountSupplierContext.newBuilder().setAudience("aud").setSubjectTokenType("urn").build(); + + String subjectToken = supplier.getSubjectToken(dummyContext); + + assertEquals(transportFactory.transport.getSubjectToken(), subjectToken); + } + @Test void retrieveSubjectToken_urlSourcedCredential_throws() { MockExternalAccountCredentialsTransportFactory transportFactory = @@ -1314,6 +1340,7 @@ void builder_actorTokenWithInvalidUrl_throws() { .setSubjectTokenType("subjectTokenType") .setTokenUrl("https://invalid.googleapis.com/") // Does not contain .mtls. .setCredentialSource(credentialSource) + .setActorTokenType("actorTokenType") .setActorTokenSupplier( new IdentityPoolActorTokenSupplier() { @Override @@ -1323,7 +1350,37 @@ public String getActorToken(ExternalAccountSupplierContext context) { }) .build()); - assertEquals("Actor tokens are only supported for mTLS token URLs.", e.getMessage()); + assertEquals( + "Actor tokens are only supported for mTLS token exchanges. Please configure a certificate source or MtlsHttpTransportFactory.", + e.getMessage()); + } + + @Test + void builder_actorTokenWithMissingTokenType_throws() { + IdentityPoolCredentialSource credentialSource = createFileCredentialSource(); + + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> + IdentityPoolCredentials.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl("https://sts.mtls.googleapis.com/") + .setCredentialSource(credentialSource) + .setActorTokenSupplier( + new IdentityPoolActorTokenSupplier() { + @Override + public String getActorToken(ExternalAccountSupplierContext context) { + return "token"; + } + }) + .build()); + + assertEquals( + "An actorTokenType must be specified when an actorTokenSupplier is configured.", + e.getMessage()); } @Test From 48c29d2ea8f0459232e5ec77adf413f278ae9407 Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Thu, 30 Jul 2026 21:04:09 +0000 Subject: [PATCH 08/10] Fix trailing whitespace and formatting in IdentityPoolCredentialsTest --- .../google/auth/oauth2/IdentityPoolCredentialsTest.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java index 8b7843ec7a21..ebceb5cb042e 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java @@ -271,9 +271,12 @@ void retrieveSubjectToken_urlSourcedWithJsonFormat_withActorTokenField() throws UrlIdentityPoolSubjectTokenSupplier supplier = new UrlIdentityPoolSubjectTokenSupplier(credentialSource, transportFactory); - + ExternalAccountSupplierContext dummyContext = - ExternalAccountSupplierContext.newBuilder().setAudience("aud").setSubjectTokenType("urn").build(); + ExternalAccountSupplierContext.newBuilder() + .setAudience("aud") + .setSubjectTokenType("urn") + .build(); String subjectToken = supplier.getSubjectToken(dummyContext); From 178d71d8e9a55a90e215008001e2dcd99b883b41 Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Fri, 31 Jul 2026 02:00:46 +0000 Subject: [PATCH 09/10] test: use real MtlsHttpTransportFactory instead of mock to fix Java 8 Mockito --- .../google/auth/oauth2/ExternalAccountCredentialsTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index d81aa8c210b0..4b16f00a6d91 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -201,7 +201,7 @@ void fromJson_identityPoolCredentialsWorkload() { } @Test - void fromJson_identityPoolCredentials_withActorTokenType() { + void fromJson_identityPoolCredentials_withActorTokenType() throws Exception { GenericJson json = buildJsonIdentityPoolCredential(); json.put("actor_token_type", "actorTokenType"); @@ -212,8 +212,10 @@ void fromJson_identityPoolCredentials_withActorTokenType() { formatMap.put("subject_token_field_name", "subject_token"); credentialSource.put("format", formatMap); + java.security.KeyStore ks = java.security.KeyStore.getInstance(java.security.KeyStore.getDefaultType()); + ks.load(null, null); com.google.auth.mtls.MtlsHttpTransportFactory mockTransportFactory = - org.mockito.Mockito.mock(com.google.auth.mtls.MtlsHttpTransportFactory.class); + new com.google.auth.mtls.MtlsHttpTransportFactory(ks); ExternalAccountCredentials credential = ExternalAccountCredentials.fromJson(json, mockTransportFactory); From c22c5213b110f80e20559662ea703e3bd0875f13 Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Fri, 31 Jul 2026 02:07:38 +0000 Subject: [PATCH 10/10] Fix line width formatting in ExternalAccountCredentialsTest --- .../com/google/auth/oauth2/ExternalAccountCredentialsTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index 4b16f00a6d91..c0b6bd6aa6e6 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -212,7 +212,8 @@ void fromJson_identityPoolCredentials_withActorTokenType() throws Exception { formatMap.put("subject_token_field_name", "subject_token"); credentialSource.put("format", formatMap); - java.security.KeyStore ks = java.security.KeyStore.getInstance(java.security.KeyStore.getDefaultType()); + java.security.KeyStore ks = + java.security.KeyStore.getInstance(java.security.KeyStore.getDefaultType()); ks.load(null, null); com.google.auth.mtls.MtlsHttpTransportFactory mockTransportFactory = new com.google.auth.mtls.MtlsHttpTransportFactory(ks);