+ String gcpWorkloadAudience = System.getenv("GCP_WORKLOAD_AUDIENCE");
+
+ // The bucket to fetch data from.
+ String gcsBucketName = System.getenv("GCS_BUCKET_NAME");
+
+ // (Optional) The service account impersonation URL.
+ String saImpersonationUrl = System.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL");
+
+ if (gcpWorkloadAudience == null || gcsBucketName == null) {
+ System.err.println(
+ "Error: GCP_WORKLOAD_AUDIENCE and GCS_BUCKET_NAME environment variables are required.");
+ return;
+ }
+
+ System.out.println("Getting metadata for bucket: " + gcsBucketName + "...");
+ Bucket bucket =
+ authenticateWithAwsCredentials(gcpWorkloadAudience, saImpersonationUrl, gcsBucketName);
+
+ System.out.println(" --- SUCCESS! ---");
+ System.out.printf("Bucket Name: %s%n", bucket.getName());
+ System.out.printf("Bucket Location: %s%n", bucket.getLocation());
+ }
+
+ /**
+ * Authenticates using a custom AWS credential supplier and retrieves bucket metadata.
+ *
+ * @param gcpWorkloadAudience The WIF provider audience.
+ * @param saImpersonationUrl Optional service account impersonation URL.
+ * @param gcsBucketName The GCS bucket name.
+ * @return The Bucket object containing metadata.
+ * @throws IOException If authentication fails.
+ */
+ // [START auth_custom_credential_supplier_aws]
+ public static Bucket authenticateWithAwsCredentials(
+ String gcpWorkloadAudience, String saImpersonationUrl, String gcsBucketName)
+ throws IOException {
+
+ // 1. Instantiate the custom supplier.
+ CustomAwsSupplier customSupplier = new CustomAwsSupplier();
+
+ // 2. Configure the AwsCredentials options.
+ AwsCredentials.Builder credentialsBuilder =
+ AwsCredentials.newBuilder()
+ .setAudience(gcpWorkloadAudience)
+ // This token type indicates that the subject token is an AWS Signature Version 4 signed
+ // request. This is required for AWS Workload Identity Federation.
+ .setSubjectTokenType("urn:ietf:params:aws:token-type:aws4_request")
+ .setAwsSecurityCredentialsSupplier(customSupplier);
+
+ if (saImpersonationUrl != null) {
+ credentialsBuilder.setServiceAccountImpersonationUrl(saImpersonationUrl);
+ }
+
+ GoogleCredentials credentials = credentialsBuilder.build();
+
+ // 3. Use the credentials to make an authenticated request.
+ Storage storage = StorageOptions.newBuilder().setCredentials(credentials).build().getService();
+
+ return storage.get(gcsBucketName);
+ }
+
+ /**
+ * Custom AWS Security Credentials Supplier.
+ *
+ * This implementation resolves AWS credentials and regions using the default provider chains
+ * from the AWS SDK (v2). This supports environment variables, ~/.aws/credentials, and EC2/EKS
+ * metadata.
+ */
+ private static class CustomAwsSupplier implements AwsSecurityCredentialsSupplier {
+ private final AwsCredentialsProvider awsCredentialsProvider;
+ private String region;
+
+ public CustomAwsSupplier() {
+ // The AWS SDK handles memoization and refreshing internally.
+ this.awsCredentialsProvider = DefaultCredentialsProvider.create();
+ }
+
+ @Override
+ public String getRegion(ExternalAccountSupplierContext context) {
+ if (this.region == null) {
+ Region awsRegion = new DefaultAwsRegionProviderChain().getRegion();
+ if (awsRegion == null) {
+ throw new IllegalStateException(
+ "Unable to resolve AWS region. Ensure AWS_REGION is set or configured.");
+ }
+ this.region = awsRegion.id();
+ }
+ return this.region;
+ }
+
+ @Override
+ public AwsSecurityCredentials getCredentials(ExternalAccountSupplierContext context) {
+ software.amazon.awssdk.auth.credentials.AwsCredentials credentials =
+ this.awsCredentialsProvider.resolveCredentials();
+
+ if (credentials == null) {
+ throw new IllegalStateException("Unable to resolve AWS credentials.");
+ }
+
+ String sessionToken = null;
+ if (credentials instanceof AwsSessionCredentials) {
+ sessionToken = ((AwsSessionCredentials) credentials).sessionToken();
+ }
+
+ return new AwsSecurityCredentials(
+ credentials.accessKeyId(), credentials.secretAccessKey(), sessionToken);
+ }
+ }
+ // [END auth_custom_credential_supplier_aws]
+}
diff --git a/auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierOktaWorkload.java b/auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierOktaWorkload.java
new file mode 100644
index 00000000000..cb720b2805f
--- /dev/null
+++ b/auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierOktaWorkload.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.auth.samples;
+
+// [START auth_custom_credential_supplier_okta]
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.auth.oauth2.ExternalAccountSupplierContext;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.auth.oauth2.IdentityPoolCredentials;
+import com.google.auth.oauth2.IdentityPoolSubjectTokenSupplier;
+import com.google.cloud.storage.Bucket;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageOptions;
+import java.io.BufferedReader;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.Base64;
+
+// [END auth_custom_credential_supplier_okta]
+
+/**
+ * This sample demonstrates how to use a custom subject token supplier to authenticate to Google
+ * Cloud Storage, using Okta as the identity provider.
+ */
+public class CustomCredentialSupplierOktaWorkload {
+
+ public static void main(String[] args) throws IOException {
+ // The audience for the workload identity federation.
+ // Format: //iam.googleapis.com/projects//locations/global/
+ // workloadIdentityPools//providers/
+ String gcpWorkloadAudience = System.getenv("GCP_WORKLOAD_AUDIENCE");
+
+ // The bucket to fetch data from.
+ String gcsBucketName = System.getenv("GCS_BUCKET_NAME");
+
+ // (Optional) The service account impersonation URL.
+ String saImpersonationUrl = System.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL");
+
+ // Okta Configuration
+ String oktaDomain = System.getenv("OKTA_DOMAIN");
+ String oktaClientId = System.getenv("OKTA_CLIENT_ID");
+ String oktaClientSecret = System.getenv("OKTA_CLIENT_SECRET");
+
+ if (gcpWorkloadAudience == null
+ || gcsBucketName == null
+ || oktaDomain == null
+ || oktaClientId == null
+ || oktaClientSecret == null) {
+ System.err.println(
+ "Error: Missing required environment variables. "
+ + "Required: GCP_WORKLOAD_AUDIENCE, GCS_BUCKET_NAME, "
+ + "OKTA_DOMAIN, OKTA_CLIENT_ID, OKTA_CLIENT_SECRET");
+ return;
+ }
+
+ System.out.println("Getting metadata for bucket: " + gcsBucketName + "...");
+ Bucket bucket =
+ authenticateWithOktaCredentials(
+ gcpWorkloadAudience,
+ saImpersonationUrl,
+ gcsBucketName,
+ oktaDomain,
+ oktaClientId,
+ oktaClientSecret);
+
+ System.out.println(" --- SUCCESS! ---");
+ System.out.printf("Bucket Name: %s%n", bucket.getName());
+ System.out.printf("Bucket Location: %s%n", bucket.getLocation());
+ }
+
+ /**
+ * Authenticates using a custom Okta credential supplier and retrieves bucket metadata.
+ *
+ * @param gcpWorkloadAudience The WIF provider audience.
+ * @param saImpersonationUrl Optional service account impersonation URL.
+ * @param gcsBucketName The GCS bucket name.
+ * @param oktaDomain The Okta organization domain.
+ * @param oktaClientId The Okta application Client ID.
+ * @param oktaClientSecret The Okta application Client Secret.
+ * @return The Bucket object containing metadata.
+ * @throws IOException If authentication or the API request fails.
+ */
+ // [START auth_custom_credential_supplier_okta]
+ public static Bucket authenticateWithOktaCredentials(
+ String gcpWorkloadAudience,
+ String saImpersonationUrl,
+ String gcsBucketName,
+ String oktaDomain,
+ String oktaClientId,
+ String oktaClientSecret)
+ throws IOException {
+
+ // 1. Instantiate our custom supplier with Okta credentials.
+ OktaClientCredentialsSupplier oktaSupplier =
+ new OktaClientCredentialsSupplier(oktaDomain, oktaClientId, oktaClientSecret);
+
+ // 2. Instantiate an IdentityPoolCredentials with the required configuration.
+ IdentityPoolCredentials.Builder credentialsBuilder =
+ IdentityPoolCredentials.newBuilder()
+ .setAudience(gcpWorkloadAudience)
+ // This token type indicates that the subject token is a JSON Web Token (JWT).
+ // This is required for Workload Identity Federation with an OIDC provider like Okta.
+ .setSubjectTokenType("urn:ietf:params:oauth:token-type:jwt")
+ .setTokenUrl("https://sts.googleapis.com/v1/token")
+ .setSubjectTokenSupplier(oktaSupplier);
+
+ if (saImpersonationUrl != null) {
+ credentialsBuilder.setServiceAccountImpersonationUrl(saImpersonationUrl);
+ }
+
+ GoogleCredentials credentials = credentialsBuilder.build();
+
+ // 3. Use the credentials to make an authenticated request.
+ Storage storage = StorageOptions.newBuilder().setCredentials(credentials).build().getService();
+
+ return storage.get(gcsBucketName);
+ }
+
+ /**
+ * A custom SubjectTokenSupplier that authenticates with Okta using the Client Credentials grant
+ * flow.
+ */
+ private static class OktaClientCredentialsSupplier implements IdentityPoolSubjectTokenSupplier {
+
+ private static final long TOKEN_REFRESH_BUFFER_SECONDS = 60;
+
+ private final String oktaTokenUrl;
+ private final String clientId;
+ private final String clientSecret;
+ private String accessToken;
+ private Instant expiryTime;
+
+ public OktaClientCredentialsSupplier(String domain, String clientId, String clientSecret) {
+ // Ensure domain doesn't have a trailing slash for cleaner URL construction
+ String cleanedDomain =
+ domain.endsWith("/") ? domain.substring(0, domain.length() - 1) : domain;
+ this.oktaTokenUrl = cleanedDomain + "/oauth2/default/v1/token";
+ this.clientId = clientId;
+ this.clientSecret = clientSecret;
+ }
+
+ /**
+ * Main method called by the auth library. It will fetch a new token if one is not already
+ * cached.
+ */
+ @Override
+ public String getSubjectToken(ExternalAccountSupplierContext context) throws IOException {
+ // Check if the current token is still valid (with a 60-second buffer).
+ boolean isTokenValid =
+ this.accessToken != null
+ && this.expiryTime != null
+ && Instant.now().isBefore(this.expiryTime.minusSeconds(TOKEN_REFRESH_BUFFER_SECONDS));
+
+ if (isTokenValid) {
+ return this.accessToken;
+ }
+
+ fetchOktaAccessToken();
+ return this.accessToken;
+ }
+
+ /**
+ * Performs the Client Credentials grant flow by making a POST request to Okta's token endpoint.
+ */
+ private void fetchOktaAccessToken() throws IOException {
+ URL url = new URL(this.oktaTokenUrl);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod("POST");
+ conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+ conn.setRequestProperty("Accept", "application/json");
+
+ // The client_id and client_secret are sent in a Basic Auth header.
+ String auth = this.clientId + ":" + this.clientSecret;
+ String encodedAuth =
+ Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
+ conn.setRequestProperty("Authorization", "Basic " + encodedAuth);
+
+ conn.setDoOutput(true);
+ try (DataOutputStream out = new DataOutputStream(conn.getOutputStream())) {
+ // Scopes define the permissions the access token will have.
+ // Update "gcp.test.read" to match your Okta configuration.
+ String params = "grant_type=client_credentials&scope=gcp.test.read";
+ out.writeBytes(params);
+ out.flush();
+ }
+
+ int responseCode = conn.getResponseCode();
+ if (responseCode == HttpURLConnection.HTTP_OK) {
+ try (BufferedReader in =
+ new BufferedReader(
+ new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
+
+ GenericJson jsonObject =
+ GsonFactory.getDefaultInstance().createJsonParser(in).parse(GenericJson.class);
+
+ if (jsonObject.containsKey("access_token") && jsonObject.containsKey("expires_in")) {
+ this.accessToken = (String) jsonObject.get("access_token");
+ Number expiresInNumber = (Number) jsonObject.get("expires_in");
+ this.expiryTime = Instant.now().plusSeconds(expiresInNumber.longValue());
+ } else {
+ throw new IOException("Access token or expires_in not found in Okta response.");
+ }
+ }
+ } else {
+ throw new IOException("Failed to authenticate with Okta. Response code: " + responseCode);
+ }
+ }
+ }
+ // [END auth_custom_credential_supplier_okta]
+}
diff --git a/auth/src/test/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkloadTest.java b/auth/src/test/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkloadTest.java
new file mode 100644
index 00000000000..36bdd9b536d
--- /dev/null
+++ b/auth/src/test/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkloadTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.auth.samples;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
+import com.google.cloud.storage.Bucket;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class CustomCredentialSupplierAwsWorkloadTest {
+
+ private static final String AUDIENCE_ENV = "GCP_WORKLOAD_AUDIENCE";
+ private static final String BUCKET_ENV = "GCS_BUCKET_NAME";
+ private static final String IMPERSONATION_URL_ENV = "GCP_SERVICE_ACCOUNT_IMPERSONATION_URL";
+
+ // AWS Credentials required for the AWS SDK DefaultCredentialsProvider to work
+ private static final String AWS_REGION_ENV = "AWS_REGION";
+ private static final String AWS_KEY_ENV = "AWS_ACCESS_KEY_ID";
+ private static final String AWS_SECRET_KEY_ENV = "AWS_SECRET_ACCESS_KEY";
+
+ @BeforeClass
+ public static void checkRequirements() {
+ // Skip the test if required environment variables are missing
+ requireEnvVar(AUDIENCE_ENV);
+ requireEnvVar(BUCKET_ENV);
+
+ // Verify AWS specific environment variables
+ requireEnvVar(AWS_REGION_ENV);
+ requireEnvVar(AWS_KEY_ENV);
+ requireEnvVar(AWS_SECRET_KEY_ENV);
+ }
+
+ private static void requireEnvVar(String varName) {
+ assumeTrue(
+ "Skipping test: " + varName + " is missing.",
+ System.getenv(varName) != null && !System.getenv(varName).isEmpty());
+ }
+
+ @Test
+ public void testAuthenticateWithAwsCredentials_system() throws Exception {
+ String audience = System.getenv(AUDIENCE_ENV);
+ String bucketName = System.getenv(BUCKET_ENV);
+ String impersonationUrl = System.getenv(IMPERSONATION_URL_ENV);
+
+ // Act: Run the authentication sample
+ Bucket bucket =
+ CustomCredentialSupplierAwsWorkload.authenticateWithAwsCredentials(
+ audience, impersonationUrl, bucketName);
+
+ // Assert: Verify we got a valid bucket object back from the API
+ assertThat(bucket).isNotNull();
+ assertThat(bucket.getName()).isEqualTo(bucketName);
+
+ // Verify we can actually access metadata (proving auth worked)
+ assertThat(bucket.getLocation()).isNotNull();
+ }
+}
diff --git a/auth/src/test/java/com/google/cloud/auth/samples/CustomCredentialSupplierOktaWorkloadTest.java b/auth/src/test/java/com/google/cloud/auth/samples/CustomCredentialSupplierOktaWorkloadTest.java
new file mode 100644
index 00000000000..be6eda6cad1
--- /dev/null
+++ b/auth/src/test/java/com/google/cloud/auth/samples/CustomCredentialSupplierOktaWorkloadTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.auth.samples;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
+import com.google.cloud.storage.Bucket;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class CustomCredentialSupplierOktaWorkloadTest {
+
+ private static final String AUDIENCE_ENV = "GCP_WORKLOAD_AUDIENCE";
+ private static final String BUCKET_ENV = "GCS_BUCKET_NAME";
+ private static final String IMPERSONATION_URL_ENV = "GCP_SERVICE_ACCOUNT_IMPERSONATION_URL";
+
+ private static final String OKTA_DOMAIN_ENV = "OKTA_DOMAIN";
+ private static final String OKTA_CLIENT_ID_ENV = "OKTA_CLIENT_ID";
+ private static final String OKTA_CLIENT_SECRET_ENV = "OKTA_CLIENT_SECRET";
+
+ @BeforeClass
+ public static void checkRequirements() {
+ // System tests require these variables to be set.
+ // If they are missing, the test suite is skipped (standard behavior for Google Cloud samples).
+ requireEnvVar(AUDIENCE_ENV);
+ requireEnvVar(BUCKET_ENV);
+ requireEnvVar(OKTA_DOMAIN_ENV);
+ requireEnvVar(OKTA_CLIENT_ID_ENV);
+ requireEnvVar(OKTA_CLIENT_SECRET_ENV);
+ }
+
+ private static void requireEnvVar(String varName) {
+ assumeTrue(
+ "Skipping test: " + varName + " is missing.",
+ System.getenv(varName) != null && !System.getenv(varName).isEmpty());
+ }
+
+ /**
+ * System Test: Verifies the full end-to-end authentication flow. This runs against the real
+ * Google Cloud and Okta APIs.
+ */
+ @Test
+ public void testAuthenticateWithOktaCredentials_system() throws Exception {
+ String audience = System.getenv(AUDIENCE_ENV);
+ String bucketName = System.getenv(BUCKET_ENV);
+ String impersonationUrl = System.getenv(IMPERSONATION_URL_ENV);
+
+ String oktaDomain = System.getenv(OKTA_DOMAIN_ENV);
+ String oktaClientId = System.getenv(OKTA_CLIENT_ID_ENV);
+ String oktaSecret = System.getenv(OKTA_CLIENT_SECRET_ENV);
+
+ // Act: Run the authentication sample
+ Bucket bucket =
+ CustomCredentialSupplierOktaWorkload.authenticateWithOktaCredentials(
+ audience, impersonationUrl, bucketName, oktaDomain, oktaClientId, oktaSecret);
+
+ // Assert: Verify we got a valid bucket object back from the API
+ assertThat(bucket).isNotNull();
+ assertThat(bucket.getName()).isEqualTo(bucketName);
+
+ // Verify we can actually access metadata (proving auth worked)
+ assertThat(bucket.getLocation()).isNotNull();
+ }
+}