From bdacbaf2d51011d704aa597b82593c4758864322 Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Tue, 3 Nov 2026 20:18:35 +0000 Subject: [PATCH 1/2] test(oauth2): sign mTLS WIF OIDC test JWTs with static test key to prevent key-rotation breakage --- .../oauth2/ITWorkloadIdentityFederationTest.java | 15 ++++++++++----- .../scripts/workloadidentityfederation-setup.sh | 16 +++++++++++++++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java index b36ecee88ae6..a06f68f54518 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java @@ -60,6 +60,8 @@ import java.io.FileInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.MessageDigest; @@ -98,10 +100,11 @@ final class ITWorkloadIdentityFederationTest { "https://storage.googleapis.com/example-oidc-issuer"; private static final String DEFAULT_MTLS_CERT_CONFIG_PATH = "testresources/mtls/certificate_config.json"; + private static final String DEFAULT_MTLS_TEST_KEY_PATH = "testresources/mtls/test_key.pem"; + private static final String OIDC_JWT_STATIC_KEY_ID = "mtls-test-key"; private String clientEmail; private String clientId; - private String privateKeyId; private PrivateKey privateKey; @BeforeEach @@ -109,8 +112,10 @@ void setup() throws IOException { GenericJson keys = getServiceAccountKeyFileAsJson(); clientEmail = (String) keys.get("client_email"); clientId = (String) keys.get("client_id"); - privateKeyId = (String) keys.get("private_key_id"); - privateKey = OAuth2Utils.privateKeyFromPkcs8((String) keys.get("private_key")); + String testKeyPem = + new String( + Files.readAllBytes(Paths.get(DEFAULT_MTLS_TEST_KEY_PATH)), StandardCharsets.UTF_8); + privateKey = OAuth2Utils.privateKeyFromPkcs8(testKeyPem); } /** @@ -651,7 +656,7 @@ private String generateSignedOidcJwt(String audience, String x5tS256, boolean in JsonWebSignature.Header header = new JsonWebSignature.Header(); header.setAlgorithm("RS256"); header.setType("JWT"); - header.setKeyId(privateKeyId); + header.setKeyId(OIDC_JWT_STATIC_KEY_ID); long nowSeconds = Instant.now().getEpochSecond(); JsonWebToken.Payload payload = new JsonWebToken.Payload(); @@ -676,7 +681,7 @@ private String generateSignedOidcJwt(String audience, String x5tS256, boolean in return JsonWebSignature.signUsingRsaSha256( privateKey, OAuth2Utils.JSON_FACTORY, header, payload); } catch (GeneralSecurityException e) { - throw new IOException("Error signing OIDC JWT with service account private key.", e); + throw new IOException("Error signing OIDC JWT with test private key.", e); } } diff --git a/google-auth-library-java/scripts/workloadidentityfederation-setup.sh b/google-auth-library-java/scripts/workloadidentityfederation-setup.sh index f7aac99ede0b..1effb68da115 100755 --- a/google-auth-library-java/scripts/workloadidentityfederation-setup.sh +++ b/google-auth-library-java/scripts/workloadidentityfederation-setup.sh @@ -136,7 +136,21 @@ gcloud beta iam workload-identity-pools providers create-oidc ${oidc_provider_id # Create the OIDC JWT Provider for mTLS / certificate-bound and actor token tests. jwk_file=$(mktemp) -curl -sS "https://www.googleapis.com/service_accounts/v1/jwk/${service_account_email}" > "${jwk_file}" +python3 - "${service_account_email}" "${jwk_file}" << 'EOF' +import json, sys, urllib.request +url = f"https://www.googleapis.com/service_accounts/v1/jwk/{sys.argv[1]}" +jwks = json.loads(urllib.request.urlopen(url).read().decode()) +jwks["keys"].append({ + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "kid": "mtls-test-key", + "n": "oN7dxAg5Ip0kEIccfF515GDtKXEvbmfSsRjQi6Ie9hqT-1E0ZXx8dxC2kTAVJyBPegpikLR5n9ytWThymlnbXE1lPzUmZW0DoFsrkwdkxqKZi0GeAPdrDAL_8TlwoELfASvv06lXWDIREJrB7qVwZUJDNjYbeTfbU8fZi_3_lvSoWkn_nUoYBosLYAwhk5YERxYxvFRT6JCPEWjhnGZG-d6z1y3CoPRsAeYVROxQfDBIwCYgICikmgQ__wAn6XpBHg5AAO9NWwGjfVlPsz-fm07KEKcBZXE5Gurmu2TH8AvjX6qSXPXtW9Vb1XTi07hbnD2JFqopRvvP9nuWb1HK2w", + "e": "AQAB", +}) +with open(sys.argv[2], "w") as f: + json.dump(jwks, f) +EOF gcloud beta iam workload-identity-pools providers create-oidc ${oidc_jwt_provider_id} \ --workload-identity-pool=${pool_id} \ --issuer-uri="https://storage.googleapis.com/example-oidc-issuer" \ From ad0db210cfce9e2a0806027450a4ea7d5ff9c27c Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Tue, 3 Nov 2026 22:41:16 +0000 Subject: [PATCH 2/2] test(oauth2): auto-sync oidc-jwt-1 provider JWKs on SA key rotation --- .../ITWorkloadIdentityFederationTest.java | 103 ++++++++++++++++-- .../workloadidentityfederation-setup.sh | 16 +-- 2 files changed, 94 insertions(+), 25 deletions(-) diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java index a06f68f54518..f4d5280e6a8d 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java @@ -36,7 +36,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import com.google.api.client.http.ByteArrayContent; import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpContent; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; @@ -60,8 +62,6 @@ import java.io.FileInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.MessageDigest; @@ -100,11 +100,12 @@ final class ITWorkloadIdentityFederationTest { "https://storage.googleapis.com/example-oidc-issuer"; private static final String DEFAULT_MTLS_CERT_CONFIG_PATH = "testresources/mtls/certificate_config.json"; - private static final String DEFAULT_MTLS_TEST_KEY_PATH = "testresources/mtls/test_key.pem"; - private static final String OIDC_JWT_STATIC_KEY_ID = "mtls-test-key"; + + private static volatile String syncedOidcJwtKeyId; private String clientEmail; private String clientId; + private String privateKeyId; private PrivateKey privateKey; @BeforeEach @@ -112,10 +113,8 @@ void setup() throws IOException { GenericJson keys = getServiceAccountKeyFileAsJson(); clientEmail = (String) keys.get("client_email"); clientId = (String) keys.get("client_id"); - String testKeyPem = - new String( - Files.readAllBytes(Paths.get(DEFAULT_MTLS_TEST_KEY_PATH)), StandardCharsets.UTF_8); - privateKey = OAuth2Utils.privateKeyFromPkcs8(testKeyPem); + privateKeyId = (String) keys.get("private_key_id"); + privateKey = OAuth2Utils.privateKeyFromPkcs8((String) keys.get("private_key")); } /** @@ -653,10 +652,12 @@ private String computeCertThumbprint(KeyStore keyStore) throws IOException { private String generateSignedOidcJwt(String audience, String x5tS256, boolean includeMayAct) throws IOException { + ensureOidcJwtProviderJwksSynced(); + JsonWebSignature.Header header = new JsonWebSignature.Header(); header.setAlgorithm("RS256"); header.setType("JWT"); - header.setKeyId(OIDC_JWT_STATIC_KEY_ID); + header.setKeyId(privateKeyId); long nowSeconds = Instant.now().getEpochSecond(); JsonWebToken.Payload payload = new JsonWebToken.Payload(); @@ -681,7 +682,89 @@ private String generateSignedOidcJwt(String audience, String x5tS256, boolean in return JsonWebSignature.signUsingRsaSha256( privateKey, OAuth2Utils.JSON_FACTORY, header, payload); } catch (GeneralSecurityException e) { - throw new IOException("Error signing OIDC JWT with test private key.", e); + throw new IOException("Error signing OIDC JWT with service account private key.", e); + } + } + + /** + * Ensures the {@code oidc-jwt-1} Workload Identity Pool Provider's static {@code jwksJson} + * includes the current service account {@code private_key_id}. When the integration test service + * account rotates keys bi-monthly, the first test run automatically fetches the live public JWK + * set for the service account and patches {@code oidc-jwt-1} using the service account's own + * {@code roles/iam.workloadIdentityPoolAdmin} permission. + */ + private void ensureOidcJwtProviderJwksSynced() throws IOException { + if (privateKeyId.equals(syncedOidcJwtKeyId)) { + return; + } + synchronized (ITWorkloadIdentityFederationTest.class) { + if (privateKeyId.equals(syncedOidcJwtKeyId)) { + return; + } + GoogleCredentials adminCredentials = + GoogleCredentials.getApplicationDefault().createScoped(OAuth2Utils.CLOUD_PLATFORM_SCOPE); + HttpTransport transport = new NetHttpTransport(); + HttpRequestFactory requestFactory = + transport.createRequestFactory(new HttpCredentialsAdapter(adminCredentials)); + JsonObjectParser parser = new JsonObjectParser(GsonFactory.getDefaultInstance()); + + String providerResource = OIDC_JWT_AUDIENCE.substring("//iam.googleapis.com/".length()); + String providerUrl = "https://iam.googleapis.com/v1/" + providerResource; + + HttpRequest getRequest = requestFactory.buildGetRequest(new GenericUrl(providerUrl)); + getRequest.setParser(parser); + GenericData providerData; + HttpResponse getResponse = getRequest.execute(); + try { + providerData = getResponse.parseAs(GenericData.class); + } finally { + getResponse.disconnect(); + } + + Map oidc = (Map) providerData.get("oidc"); + String currentJwksJson = oidc != null ? (String) oidc.get("jwksJson") : null; + if (currentJwksJson != null && currentJwksJson.contains(privateKeyId)) { + syncedOidcJwtKeyId = privateKeyId; + return; + } + + HttpRequest jwksRequest = + transport + .createRequestFactory() + .buildGetRequest( + new GenericUrl( + "https://www.googleapis.com/service_accounts/v1/jwk/" + clientEmail)); + String liveJwksJson; + HttpResponse jwksResponse = jwksRequest.execute(); + try { + liveJwksJson = jwksResponse.parseAsString(); + } finally { + jwksResponse.disconnect(); + } + + GenericJson oidcPatch = new GenericJson(); + oidcPatch.setFactory(OAuth2Utils.JSON_FACTORY); + oidcPatch.put("jwksJson", liveJwksJson); + + GenericJson patchBody = new GenericJson(); + patchBody.setFactory(OAuth2Utils.JSON_FACTORY); + patchBody.put("oidc", oidcPatch); + + HttpContent content = + new ByteArrayContent( + "application/json", patchBody.toPrettyString().getBytes(StandardCharsets.UTF_8)); + HttpRequest patchRequest = + requestFactory.buildPostRequest( + new GenericUrl(providerUrl + "?updateMask=oidc.jwksJson"), content); + patchRequest.getHeaders().set("X-HTTP-Method-Override", "PATCH"); + HttpResponse patchResponse = patchRequest.execute(); + try { + assertTrue(patchResponse.isSuccessStatusCode()); + } finally { + patchResponse.disconnect(); + } + + syncedOidcJwtKeyId = privateKeyId; } } diff --git a/google-auth-library-java/scripts/workloadidentityfederation-setup.sh b/google-auth-library-java/scripts/workloadidentityfederation-setup.sh index 1effb68da115..f7aac99ede0b 100755 --- a/google-auth-library-java/scripts/workloadidentityfederation-setup.sh +++ b/google-auth-library-java/scripts/workloadidentityfederation-setup.sh @@ -136,21 +136,7 @@ gcloud beta iam workload-identity-pools providers create-oidc ${oidc_provider_id # Create the OIDC JWT Provider for mTLS / certificate-bound and actor token tests. jwk_file=$(mktemp) -python3 - "${service_account_email}" "${jwk_file}" << 'EOF' -import json, sys, urllib.request -url = f"https://www.googleapis.com/service_accounts/v1/jwk/{sys.argv[1]}" -jwks = json.loads(urllib.request.urlopen(url).read().decode()) -jwks["keys"].append({ - "kty": "RSA", - "alg": "RS256", - "use": "sig", - "kid": "mtls-test-key", - "n": "oN7dxAg5Ip0kEIccfF515GDtKXEvbmfSsRjQi6Ie9hqT-1E0ZXx8dxC2kTAVJyBPegpikLR5n9ytWThymlnbXE1lPzUmZW0DoFsrkwdkxqKZi0GeAPdrDAL_8TlwoELfASvv06lXWDIREJrB7qVwZUJDNjYbeTfbU8fZi_3_lvSoWkn_nUoYBosLYAwhk5YERxYxvFRT6JCPEWjhnGZG-d6z1y3CoPRsAeYVROxQfDBIwCYgICikmgQ__wAn6XpBHg5AAO9NWwGjfVlPsz-fm07KEKcBZXE5Gurmu2TH8AvjX6qSXPXtW9Vb1XTi07hbnD2JFqopRvvP9nuWb1HK2w", - "e": "AQAB", -}) -with open(sys.argv[2], "w") as f: - json.dump(jwks, f) -EOF +curl -sS "https://www.googleapis.com/service_accounts/v1/jwk/${service_account_email}" > "${jwk_file}" gcloud beta iam workload-identity-pools providers create-oidc ${oidc_jwt_provider_id} \ --workload-identity-pool=${pool_id} \ --issuer-uri="https://storage.googleapis.com/example-oidc-issuer" \