From e8fcf22900d011fe8492062a6dc0fed2997f4c0a Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Sat, 15 Nov 2025 14:57:16 -0800 Subject: [PATCH 1/5] Added some samples for custom credentials. Including tests. --- auth/README.md | 45 ++++ auth/pom.xml | 17 +- .../CustomCredentialSupplierAwsWorkload.java | 155 ++++++++++++ .../CustomCredentialSupplierOktaWorkload.java | 230 ++++++++++++++++++ ...stomCredentialSupplierAwsWorkloadTest.java | 73 ++++++ ...tomCredentialSupplierOktaWorkloadTest.java | 84 +++++++ 6 files changed, 603 insertions(+), 1 deletion(-) create mode 100644 auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkload.java create mode 100644 auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierOktaWorkload.java create mode 100644 auth/src/test/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkloadTest.java create mode 100644 auth/src/test/java/com/google/cloud/auth/samples/CustomCredentialSupplierOktaWorkloadTest.java diff --git a/auth/README.md b/auth/README.md index 186970b53dd..fba4ca01abe 100644 --- a/auth/README.md +++ b/auth/README.md @@ -67,6 +67,51 @@ You can then run `DownscopingExample` via: mvn exec:java -Dexec.mainClass=com.google.cloud.auth.samples.DownscopingExample +## Custom Credential Suppliers + +If you want to use external credentials (like AWS or Okta) that require custom retrieval logic not supported natively by the library, you can provide a custom supplier implementation. + +### Authenticate with Okta (Custom Supplier) + +This sample demonstrates how to use a custom `IdentityPoolSubjectTokenSupplier` to fetch an OIDC token from Okta using the Client Credentials flow and exchange it for Google Cloud credentials. + +1. **Set required environment variables:** + ```bash + export OKTA_DOMAIN="https://your-domain.okta.com" + export OKTA_CLIENT_ID="your-client-id" + export OKTA_CLIENT_SECRET="your-client-secret" + export GCP_WORKLOAD_AUDIENCE="//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/my-pool/providers/my-provider" + export GCS_BUCKET_NAME="your-bucket-name" + # Optional: + # export GCP_SERVICE_ACCOUNT_IMPERSONATION_URL="..." + ``` + +2. **Run the sample:** + ```bash + mvn exec:java -Dexec.mainClass=com.google.cloud.auth.samples.CustomCredentialSupplierOktaWorkload + ``` + +### Authenticate with AWS (Custom Supplier) + +This sample demonstrates how to use the **AWS SDK for Java (v2)** as a custom `AwsSecurityCredentialsSupplier` to bridge AWS credentials (from environment, `~/.aws/credentials`, or EKS/ECS metadata) to Google Cloud Workload Identity. + +1. **Set required environment variables:** + ```bash + # Google Cloud Config + export GCP_WORKLOAD_AUDIENCE="//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/my-pool/providers/my-aws-provider" + export GCS_BUCKET_NAME="your-bucket-name" + + # AWS Credentials (or use ~/.aws/credentials) + export AWS_ACCESS_KEY_ID="your-aws-key" + export AWS_SECRET_ACCESS_KEY="your-aws-secret" + export AWS_REGION="us-east-1" + ``` + +2. **Run the sample:** + ```bash + mvn exec:java -Dexec.mainClass=com.google.cloud.auth.samples.CustomCredentialSupplierAwsWorkload + ``` + ## Tests Run all tests: ``` diff --git a/auth/pom.xml b/auth/pom.xml index cd51f47198d..c943b0a1ca4 100644 --- a/auth/pom.xml +++ b/auth/pom.xml @@ -30,7 +30,7 @@ limitations under the License. com.google.cloud.samples shared-configuration - 1.2.0 + 1.2.2 @@ -52,6 +52,13 @@ limitations under the License. pom import + + software.amazon.awssdk + bom + 2.25.41 + pom + import + @@ -82,6 +89,14 @@ limitations under the License. com.google.cloud google-cloud-language + + software.amazon.awssdk + auth + + + software.amazon.awssdk + regions + junit diff --git a/auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkload.java b/auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkload.java new file mode 100644 index 00000000000..7770bf95e41 --- /dev/null +++ b/auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkload.java @@ -0,0 +1,155 @@ +package com.google.cloud.auth.samples; + +/* + * 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. + */ + +// [START auth_custom_credential_supplier_aws] +import com.google.auth.oauth2.AwsCredentials; +import com.google.auth.oauth2.AwsSecurityCredentials; +import com.google.auth.oauth2.AwsSecurityCredentialsSupplier; +import com.google.auth.oauth2.ExternalAccountSupplierContext; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.storage.Bucket; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; +import java.io.IOException; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain; +// [END auth_custom_credential_supplier_aws] + +/** + * This sample demonstrates how to use a custom AWS security credentials supplier to authenticate to + * Google Cloud Storage using AWS Workload Identity Federation. + */ +public class CustomCredentialSupplierAwsWorkload { + + 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"); + + 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] +} \ No newline at end of file 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..601d81b9f26 --- /dev/null +++ b/auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierOktaWorkload.java @@ -0,0 +1,230 @@ +package com.google.cloud.auth.samples; + +/* + * 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. + */ + +// [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] +} \ No newline at end of file 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..1d64ca1fc2e --- /dev/null +++ b/auth/src/test/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkloadTest.java @@ -0,0 +1,73 @@ +package com.google.cloud.auth.samples; + +/* + * 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. + */ + +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(); + } +} \ No newline at end of file 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..1848115fd45 --- /dev/null +++ b/auth/src/test/java/com/google/cloud/auth/samples/CustomCredentialSupplierOktaWorkloadTest.java @@ -0,0 +1,84 @@ +package com.google.cloud.auth.samples; + +/* + * 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. + */ + +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(); + } +} \ No newline at end of file From a9b2c59b4efc042a121c79fed45d26b76bbf9873 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Sat, 15 Nov 2025 15:48:36 -0800 Subject: [PATCH 2/5] Newlines at the end of files and formatting fixes. --- .../CustomCredentialSupplierAwsWorkload.java | 199 +++++----- .../CustomCredentialSupplierOktaWorkload.java | 348 +++++++++--------- ...stomCredentialSupplierAwsWorkloadTest.java | 80 ++-- ...tomCredentialSupplierOktaWorkloadTest.java | 97 +++-- 4 files changed, 360 insertions(+), 364 deletions(-) diff --git a/auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkload.java b/auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkload.java index 7770bf95e41..dd85cc6322a 100644 --- a/auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkload.java +++ b/auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkload.java @@ -1,5 +1,3 @@ -package com.google.cloud.auth.samples; - /* * Copyright 2025 Google LLC * @@ -16,6 +14,8 @@ * limitations under the License. */ +package com.google.cloud.auth.samples; + // [START auth_custom_credential_supplier_aws] import com.google.auth.oauth2.AwsCredentials; import com.google.auth.oauth2.AwsSecurityCredentials; @@ -31,6 +31,7 @@ import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain; + // [END auth_custom_credential_supplier_aws] /** @@ -39,117 +40,117 @@ */ public class CustomCredentialSupplierAwsWorkload { - 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"); + 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"); - // (Optional) The service account impersonation URL. - String saImpersonationUrl = System.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"); + // The bucket to fetch data from. + String gcsBucketName = System.getenv("GCS_BUCKET_NAME"); - if (gcpWorkloadAudience == null || gcsBucketName == null) { - System.err.println( - "Error: GCP_WORKLOAD_AUDIENCE and GCS_BUCKET_NAME environment variables are required."); - return; - } + // (Optional) The service account impersonation URL. + String saImpersonationUrl = System.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"); - System.out.println("Getting metadata for bucket: " + gcsBucketName + "..."); - Bucket bucket = - authenticateWithAwsCredentials(gcpWorkloadAudience, saImpersonationUrl, gcsBucketName); + if (gcpWorkloadAudience == null || gcsBucketName == null) { + System.err.println( + "Error: GCP_WORKLOAD_AUDIENCE and GCS_BUCKET_NAME environment variables are required."); + return; + } - System.out.println(" --- SUCCESS! ---"); - System.out.printf("Bucket Name: %s%n", bucket.getName()); - System.out.printf("Bucket Location: %s%n", bucket.getLocation()); + 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); } - /** - * 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(); - GoogleCredentials credentials = credentialsBuilder.build(); + // 3. Use the credentials to make an authenticated request. + Storage storage = StorageOptions.newBuilder().setCredentials(credentials).build().getService(); - // 3. Use the credentials to make an authenticated request. - Storage storage = StorageOptions.newBuilder().setCredentials(credentials).build().getService(); + return storage.get(gcsBucketName); + } - 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; - /** - * 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(); - } + 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 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(); + @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."); - } + if (credentials == null) { + throw new IllegalStateException("Unable to resolve AWS credentials."); + } - String sessionToken = null; - if (credentials instanceof AwsSessionCredentials) { - sessionToken = ((AwsSessionCredentials) credentials).sessionToken(); - } + String sessionToken = null; + if (credentials instanceof AwsSessionCredentials) { + sessionToken = ((AwsSessionCredentials) credentials).sessionToken(); + } - return new AwsSecurityCredentials( - credentials.accessKeyId(), credentials.secretAccessKey(), sessionToken); - } + return new AwsSecurityCredentials( + credentials.accessKeyId(), credentials.secretAccessKey(), sessionToken); } - // [END auth_custom_credential_supplier_aws] -} \ No newline at end of file + } + // [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 index 601d81b9f26..cb720b2805f 100644 --- a/auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierOktaWorkload.java +++ b/auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierOktaWorkload.java @@ -1,5 +1,3 @@ -package com.google.cloud.auth.samples; - /* * Copyright 2025 Google LLC * @@ -16,6 +14,8 @@ * 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; @@ -35,6 +35,7 @@ import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Base64; + // [END auth_custom_credential_supplier_okta] /** @@ -43,188 +44,187 @@ */ 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; - } + 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()); + 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); } - /** - * 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(); - GoogleCredentials credentials = credentialsBuilder.build(); + // 3. Use the credentials to make an authenticated request. + Storage storage = StorageOptions.newBuilder().setCredentials(credentials).build().getService(); - // 3. Use the credentials to make an authenticated request. - Storage storage = StorageOptions.newBuilder().setCredentials(credentials).build().getService(); + return storage.get(gcsBucketName); + } - 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; } /** - * A custom SubjectTokenSupplier that authenticates with Okta using the Client Credentials grant - * flow. + * Main method called by the auth library. It will fetch a new token if one is not already + * cached. */ - 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; - } + @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); - } + /** + * 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] -} \ No newline at end of file + } + // [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 index 1d64ca1fc2e..36bdd9b536d 100644 --- a/auth/src/test/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkloadTest.java +++ b/auth/src/test/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkloadTest.java @@ -1,5 +1,3 @@ -package com.google.cloud.auth.samples; - /* * Copyright 2025 Google LLC * @@ -16,6 +14,8 @@ * limitations under the License. */ +package com.google.cloud.auth.samples; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assume.assumeTrue; @@ -25,49 +25,49 @@ 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"; + 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"; + // 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); + @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); - } + // 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()); - } + 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); + @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); + // 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); + // 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(); - } -} \ No newline at end of file + // 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 index 1848115fd45..be6eda6cad1 100644 --- a/auth/src/test/java/com/google/cloud/auth/samples/CustomCredentialSupplierOktaWorkloadTest.java +++ b/auth/src/test/java/com/google/cloud/auth/samples/CustomCredentialSupplierOktaWorkloadTest.java @@ -1,5 +1,3 @@ -package com.google.cloud.auth.samples; - /* * Copyright 2025 Google LLC * @@ -16,6 +14,8 @@ * limitations under the License. */ +package com.google.cloud.auth.samples; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assume.assumeTrue; @@ -25,60 +25,55 @@ 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 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"; + 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); - } + @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()); - } + 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); + /** + * 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); + 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); + // 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); + // 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(); - } -} \ No newline at end of file + // Verify we can actually access metadata (proving auth worked) + assertThat(bucket.getLocation()).isNotNull(); + } +} From 6522a0572783cfbaaf7dfba02e1dc57840bebd81 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Sun, 14 Dec 2025 18:48:57 -0800 Subject: [PATCH 3/5] Changed the custom credential scripts to use the secrets.json file to read properties. --- auth/.gitignore | 3 + auth/README.md | 45 ------- .../CustomCredentialSupplierAwsWorkload.java | 107 +++++++++++++-- .../samples/customcredentials/aws/Dockerfile | 35 +++++ .../samples/customcredentials/aws/README.md | 124 ++++++++++++++++++ ...ustom-credentials-aws-secrets.json.example | 8 ++ .../samples/customcredentials/aws/pod.yaml | 44 +++++++ .../CustomCredentialSupplierOktaWorkload.java | 122 +++++++++++++---- .../samples/customcredentials/okta/README.md | 93 +++++++++++++ ...stom-credentials-okta-secrets.json.example | 8 ++ ...stomCredentialSupplierAwsWorkloadTest.java | 73 ----------- ...tomCredentialSupplierOktaWorkloadTest.java | 79 ----------- ...stomCredentialSupplierAwsWorkloadTest.java | 72 ++++++++++ ...tomCredentialSupplierOktaWorkloadTest.java | 86 ++++++++++++ 14 files changed, 668 insertions(+), 231 deletions(-) create mode 100644 auth/.gitignore rename auth/src/main/java/com/google/cloud/auth/samples/{ => customcredentials/aws}/CustomCredentialSupplierAwsWorkload.java (58%) create mode 100644 auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/Dockerfile create mode 100644 auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/README.md create mode 100644 auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/custom-credentials-aws-secrets.json.example create mode 100644 auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/pod.yaml rename auth/src/main/java/com/google/cloud/auth/samples/{ => customcredentials/okta}/CustomCredentialSupplierOktaWorkload.java (67%) create mode 100644 auth/src/main/java/com/google/cloud/auth/samples/customcredentials/okta/README.md create mode 100644 auth/src/main/java/com/google/cloud/auth/samples/customcredentials/okta/custom-credentials-okta-secrets.json.example delete mode 100644 auth/src/test/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkloadTest.java delete mode 100644 auth/src/test/java/com/google/cloud/auth/samples/CustomCredentialSupplierOktaWorkloadTest.java create mode 100644 auth/src/test/java/com/google/cloud/auth/samples/customcredentials/aws/CustomCredentialSupplierAwsWorkloadTest.java create mode 100644 auth/src/test/java/com/google/cloud/auth/samples/customcredentials/okta/CustomCredentialSupplierOktaWorkloadTest.java diff --git a/auth/.gitignore b/auth/.gitignore new file mode 100644 index 00000000000..305842cb8a4 --- /dev/null +++ b/auth/.gitignore @@ -0,0 +1,3 @@ +# Ignore GCP and IdP secret files +src/main/java/com/google/cloud/auth/samples/customcredentials/aws/custom-credentials-aws-secrets.json +src/main/java/com/google/cloud/auth/samples/customcredentials/okta/custom-credentials-okta-secrets.json diff --git a/auth/README.md b/auth/README.md index fba4ca01abe..186970b53dd 100644 --- a/auth/README.md +++ b/auth/README.md @@ -67,51 +67,6 @@ You can then run `DownscopingExample` via: mvn exec:java -Dexec.mainClass=com.google.cloud.auth.samples.DownscopingExample -## Custom Credential Suppliers - -If you want to use external credentials (like AWS or Okta) that require custom retrieval logic not supported natively by the library, you can provide a custom supplier implementation. - -### Authenticate with Okta (Custom Supplier) - -This sample demonstrates how to use a custom `IdentityPoolSubjectTokenSupplier` to fetch an OIDC token from Okta using the Client Credentials flow and exchange it for Google Cloud credentials. - -1. **Set required environment variables:** - ```bash - export OKTA_DOMAIN="https://your-domain.okta.com" - export OKTA_CLIENT_ID="your-client-id" - export OKTA_CLIENT_SECRET="your-client-secret" - export GCP_WORKLOAD_AUDIENCE="//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/my-pool/providers/my-provider" - export GCS_BUCKET_NAME="your-bucket-name" - # Optional: - # export GCP_SERVICE_ACCOUNT_IMPERSONATION_URL="..." - ``` - -2. **Run the sample:** - ```bash - mvn exec:java -Dexec.mainClass=com.google.cloud.auth.samples.CustomCredentialSupplierOktaWorkload - ``` - -### Authenticate with AWS (Custom Supplier) - -This sample demonstrates how to use the **AWS SDK for Java (v2)** as a custom `AwsSecurityCredentialsSupplier` to bridge AWS credentials (from environment, `~/.aws/credentials`, or EKS/ECS metadata) to Google Cloud Workload Identity. - -1. **Set required environment variables:** - ```bash - # Google Cloud Config - export GCP_WORKLOAD_AUDIENCE="//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/my-pool/providers/my-aws-provider" - export GCS_BUCKET_NAME="your-bucket-name" - - # AWS Credentials (or use ~/.aws/credentials) - export AWS_ACCESS_KEY_ID="your-aws-key" - export AWS_SECRET_ACCESS_KEY="your-aws-secret" - export AWS_REGION="us-east-1" - ``` - -2. **Run the sample:** - ```bash - mvn exec:java -Dexec.mainClass=com.google.cloud.auth.samples.CustomCredentialSupplierAwsWorkload - ``` - ## Tests Run all tests: ``` diff --git a/auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkload.java b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/CustomCredentialSupplierAwsWorkload.java similarity index 58% rename from auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkload.java rename to auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/CustomCredentialSupplierAwsWorkload.java index dd85cc6322a..fb36fde7b90 100644 --- a/auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkload.java +++ b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/CustomCredentialSupplierAwsWorkload.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.auth.samples; +package com.google.cloud.auth.samples.customcredentials.aws; // [START auth_custom_credential_supplier_aws] import com.google.auth.oauth2.AwsCredentials; @@ -25,7 +25,14 @@ import com.google.cloud.storage.Bucket; import com.google.cloud.storage.Storage; import com.google.cloud.storage.StorageOptions; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Map; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; @@ -41,30 +48,106 @@ public class CustomCredentialSupplierAwsWorkload { public static void main(String[] args) throws IOException { + + // Reads the custom-credentials-aws-secrets.json if running locally. + loadConfigFromFile(); + // The audience for the workload identity federation. // Format: //iam.googleapis.com/projects//locations/global/ // workloadIdentityPools//providers/ - String gcpWorkloadAudience = System.getenv("GCP_WORKLOAD_AUDIENCE"); + String gcpWorkloadAudience = getConfiguration("GCP_WORKLOAD_AUDIENCE"); // The bucket to fetch data from. - String gcsBucketName = System.getenv("GCS_BUCKET_NAME"); + String gcsBucketName = getConfiguration("GCS_BUCKET_NAME"); // (Optional) The service account impersonation URL. - String saImpersonationUrl = System.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"); + String saImpersonationUrl = getConfiguration("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."); + "Required configuration missing. Please provide it in a " + + "custom-credentials-aws-secrets.json file or as environment variables: " + + "GCP_WORKLOAD_AUDIENCE, GCS_BUCKET_NAME"); + return; + } + + try { + System.out.println("Retrieving metadata for bucket: " + gcsBucketName + "..."); + Bucket bucket = + authenticateWithAwsCredentials(gcpWorkloadAudience, saImpersonationUrl, gcsBucketName); + + System.out.println(" --- SUCCESS! ---"); + System.out.println("Bucket details:"); + System.out.printf(" Name: %s%n", bucket.getName()); + System.out.printf(" Location: %s%n", bucket.getLocation()); + System.out.printf(" Storage Class: %s%n", bucket.getStorageClass()); + System.out.printf(" Metageneration: %s%n", bucket.getMetageneration()); + } catch (Exception e) { + System.err.println("Authentication or Request failed: " + e.getMessage()); + } + } + + /** + * Helper method to retrieve configuration. It checks Environment variables first, then System + * properties (populated by loadConfigFromFile). + */ + static String getConfiguration(String key) { + String value = System.getenv(key); + if (value == null) { + value = System.getProperty(key); + } + return value; + } + + /** + * If a local secrets file is present, load it into the System Properties. This is a + * "just-in-time" configuration for local development. These variables are only set for the + * current process. + */ + static void loadConfigFromFile() { + String secretsFile = "custom-credentials-aws-secrets.json"; + if (!Files.exists(Paths.get(secretsFile))) { return; } - System.out.println("Getting metadata for bucket: " + gcsBucketName + "..."); - Bucket bucket = - authenticateWithAwsCredentials(gcpWorkloadAudience, saImpersonationUrl, gcsBucketName); + try (Reader reader = Files.newBufferedReader(Paths.get(secretsFile))) { + // Use Gson to parse the JSON file into a Map + Gson gson = new Gson(); + Type type = new TypeToken>() {}.getType(); + Map secrets = gson.fromJson(reader, type); + + if (secrets == null) { + return; + } - System.out.println(" --- SUCCESS! ---"); - System.out.printf("Bucket Name: %s%n", bucket.getName()); - System.out.printf("Bucket Location: %s%n", bucket.getLocation()); + // AWS SDK for Java looks for System Properties with specific names (camelCase) + // if environment variables are missing. + if (secrets.containsKey("aws_access_key_id")) { + System.setProperty("aws.accessKeyId", secrets.get("aws_access_key_id")); + } + if (secrets.containsKey("aws_secret_access_key")) { + System.setProperty("aws.secretAccessKey", secrets.get("aws_secret_access_key")); + } + if (secrets.containsKey("aws_region")) { + System.setProperty("aws.region", secrets.get("aws_region")); + } + + // Set custom GCP variables as System Properties so getConfiguration() can find them. + if (secrets.containsKey("gcp_workload_audience")) { + System.setProperty("GCP_WORKLOAD_AUDIENCE", secrets.get("gcp_workload_audience")); + } + if (secrets.containsKey("gcs_bucket_name")) { + System.setProperty("GCS_BUCKET_NAME", secrets.get("gcs_bucket_name")); + } + if (secrets.containsKey("gcp_service_account_impersonation_url")) { + System.setProperty( + "GCP_SERVICE_ACCOUNT_IMPERSONATION_URL", + secrets.get("gcp_service_account_impersonation_url")); + } + + } catch (IOException e) { + System.err.println("Error reading secrets file: " + e.getMessage()); + } } /** @@ -117,7 +200,7 @@ private static class CustomAwsSupplier implements AwsSecurityCredentialsSupplier private String region; public CustomAwsSupplier() { - // The AWS SDK handles memoization and refreshing internally. + // The AWS SDK handles caching internally. this.awsCredentialsProvider = DefaultCredentialsProvider.create(); } diff --git a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/Dockerfile b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/Dockerfile new file mode 100644 index 00000000000..5d572ddc309 --- /dev/null +++ b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/Dockerfile @@ -0,0 +1,35 @@ +# Build the application +FROM maven:3.9-eclipse-temurin-17 AS builder + +WORKDIR /app + +# Copy only the build config first (for better layer caching) +COPY pom.xml . +COPY src ./src + +# 'clean package': Compiles the code and creates the thin jar in /app/target +# 'dependency:copy-dependencies': Copies all JARs to /app/target/libs +# We explicitly set -DoutputDirectory so we know EXACTLY where they are. +RUN mvn clean package dependency:copy-dependencies \ + -DoutputDirectory=target/libs \ + -DskipTests + +# Run the application +FROM eclipse-temurin:17-jre-focal + +# Security: Create a non-root user +RUN useradd -m appuser +USER appuser +WORKDIR /app + +# Copy the Thin Jar (Your application code) +# Ensure the name 'auth-1.0.jar' matches your pom.xml artifactId/version +COPY --from=builder --chown=appuser:appuser /app/target/auth-1.0.jar app.jar + +# Copy the Dependencies (The libraries) +COPY --from=builder --chown=appuser:appuser /app/target/libs lib/ + +# Run with Classpath +# We add 'app.jar' and everything in 'lib/' to the classpath. +# We explicitly specify the Main Class here, so your shared pom.xml doesn't need to change. +CMD ["java", "-cp", "app.jar:lib/*", "com.google.cloud.auth.samples.customcredentials.aws.CustomCredentialSupplierAwsWorkload"] \ No newline at end of file diff --git a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/README.md b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/README.md new file mode 100644 index 00000000000..30f86397398 --- /dev/null +++ b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/README.md @@ -0,0 +1,124 @@ +# Running the Custom AWS Credential Supplier Sample (Java) + +This sample demonstrates how to use a custom AWS security credential supplier to authenticate with Google Cloud using AWS as an external identity provider. It uses the **AWS SDK for Java (v2)** to fetch credentials from sources like Amazon Elastic Kubernetes Service (EKS) with IAM Roles for Service Accounts (IRSA), Elastic Container Service (ECS), or Fargate. + +## Prerequisites + +* An AWS account. +* A Google Cloud project with the IAM API enabled. +* A GCS bucket. +* **Java 11** or later installed. +* **Maven** installed. + +If you want to use AWS security credentials that cannot be retrieved using methods supported natively by the Google Auth library, a custom `AwsSecurityCredentialsSupplier` implementation may be specified. The supplier must return valid, unexpired AWS security credentials when called by the Google Cloud Auth library. + +## Running Locally + +For local development, you can provide credentials and configuration in a JSON file. + +### Build the Project + +Ensure you have Java and Maven installed, then build the project to download dependencies and create an executable JAR: + +```bash +mvn clean package +``` + +### Configure Credentials for Local Development + +1. Copy the example secrets file to a new file named `custom-credentials-aws-secrets.json` in the project root: + ```bash + cp custom-credentials-aws-secrets.json.example custom-credentials-aws-secrets.json + ``` +2. Open `custom-credentials-aws-secrets.json` and fill in the required values for your AWS and Google Cloud configuration. Do not check your `custom-credentials-aws-secrets.json` file into version control. + +**Note:** This file is only used for local development and is not needed when running in a containerized environment like EKS with IRSA. + +### Run the Application + +Execute the JAR file generated in the `target` directory: + +```bash +java -jar target/custom-credential-aws-1.0-SNAPSHOT.jar +``` + +*Note: Adjust the JAR filename version if you modified it in your `pom.xml`.* + +When run locally, the application will detect the `custom-credentials-aws-secrets.json` file and use it to configure the necessary system properties for the AWS SDK. + +## Running in a Containerized Environment (EKS) + +This section provides a brief overview of how to run the sample in an Amazon EKS cluster. + +### EKS Cluster Setup + +First, you need an EKS cluster. You can create one using `eksctl` or the AWS Management Console. For detailed instructions, refer to the [Amazon EKS documentation](https://docs.aws.amazon.com/eks/latest/userguide/create-cluster.html). + +### Configure IAM Roles for Service Accounts (IRSA) + +IRSA enables you to associate an IAM role with a Kubernetes service account. This provides a secure way for your pods to access AWS services without hardcoding long-lived credentials. + +Run the following command to create the IAM role and bind it to a Kubernetes Service Account: + +```bash +eksctl create iamserviceaccount \ + --name your-k8s-service-account \ + --namespace default \ + --cluster your-cluster-name \ + --region your-aws-region \ + --role-name your-role-name \ + --attach-policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \ + --approve +``` + +> **Note**: The `--attach-policy-arn` flag is used here to demonstrate attaching permissions. Update this with the specific AWS policy ARN your application requires. + +For a deep dive into how this works without using `eksctl`, refer to the [IAM Roles for Service Accounts](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) documentation. + +### Configure Google Cloud to Trust the AWS Role + +To allow your AWS role to authenticate as a Google Cloud service account, you need to configure Workload Identity Federation. This process involves these key steps: + +1. **Create a Workload Identity Pool and an AWS Provider:** The pool holds the configuration, and the provider is set up to trust your AWS account. + +2. **Create or select a Google Cloud Service Account:** This service account will be impersonated by your AWS role. + +3. **Bind the AWS Role to the Google Cloud Service Account:** Create an IAM policy binding that gives your AWS role the `Workload Identity User` (`roles/iam.workloadIdentityUser`) role on the Google Cloud service account. + +For more detailed information, see the documentation on [Configuring Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds). + +### Containerize and Package the Application + +Create a `Dockerfile` for the Java application and push the image to a container registry (for example Amazon ECR) that your EKS cluster can access. + +**Note:** The provided [`Dockerfile`](Dockerfile) uses a multi-stage build to compile the Java code. It is an example that may need modification for your specific needs. + +Build and push the image: +```bash +docker build -t your-container-image:latest . +docker push your-container-image:latest +``` + +### Deploy to EKS + +Create a Kubernetes deployment manifest to deploy your application to the EKS cluster. See the [`pod.yaml`](pod.yaml) file for an example. + +**Note:** The provided [`pod.yaml`](pod.yaml) is an example and may need to be modified for your specific needs. + +Deploy the pod: + +```bash +kubectl apply -f pod.yaml +``` + +### Clean Up + +To clean up the resources, delete the EKS cluster and any other AWS and Google Cloud resources you created. + +```bash +eksctl delete cluster --name your-cluster-name +``` + +## Testing + +This sample is not continuously tested. It is provided for instructional purposes and may require modifications to work in your environment. diff --git a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/custom-credentials-aws-secrets.json.example b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/custom-credentials-aws-secrets.json.example new file mode 100644 index 00000000000..e1fc447c78e --- /dev/null +++ b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/custom-credentials-aws-secrets.json.example @@ -0,0 +1,8 @@ +{ + "aws_access_key_id": "YOUR_AWS_ACCESS_KEY_ID", + "aws_secret_access_key": "YOUR_AWS_SECRET_ACCESS_KEY", + "aws_region": "YOUR_AWS_REGION", + "gcp_workload_audience": "YOUR_GCP_WORKLOAD_AUDIENCE", + "gcs_bucket_name": "YOUR_GCS_BUCKET_NAME", + "gcp_service_account_impersonation_url": "YOUR_GCP_SERVICE_ACCOUNT_IMPERSONATION_URL" +} \ No newline at end of file diff --git a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/pod.yaml b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/pod.yaml new file mode 100644 index 00000000000..1a90b58a5a9 --- /dev/null +++ b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/pod.yaml @@ -0,0 +1,44 @@ +# 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. + +apiVersion: v1 +kind: Pod +metadata: + name: custom-credential-pod-java +spec: + # The Kubernetes Service Account that is annotated with the corresponding + # AWS IAM role ARN. See the README for instructions on setting up IAM + # Roles for Service Accounts (IRSA). + serviceAccountName: your-k8s-service-account + containers: + - name: gcp-auth-sample-java + # The container image pushed to the container registry + # For example, Amazon Elastic Container Registry + image: your-container-image:latest + env: + # REQUIRED: The AWS region. The AWS SDK for Java requires this + # to be set explicitly in containers. + - name: AWS_REGION + value: "your-aws-region" + + # REQUIRED: The full identifier of the Workload Identity Pool provider + - name: GCP_WORKLOAD_AUDIENCE + value: "your-gcp-workload-audience" + + # OPTIONAL: Enable Google Cloud service account impersonation + # - name: GCP_SERVICE_ACCOUNT_IMPERSONATION_URL + # value: "your-gcp-service-account-impersonation-url" + + # REQUIRED: The bucket to list + - name: GCS_BUCKET_NAME + value: "your-gcs-bucket-name" \ No newline at end of file diff --git a/auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierOktaWorkload.java b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/okta/CustomCredentialSupplierOktaWorkload.java similarity index 67% rename from auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierOktaWorkload.java rename to auth/src/main/java/com/google/cloud/auth/samples/customcredentials/okta/CustomCredentialSupplierOktaWorkload.java index cb720b2805f..4ce51d90439 100644 --- a/auth/src/main/java/com/google/cloud/auth/samples/CustomCredentialSupplierOktaWorkload.java +++ b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/okta/CustomCredentialSupplierOktaWorkload.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.auth.samples; +package com.google.cloud.auth.samples.customcredentials.okta; // [START auth_custom_credential_supplier_okta] import com.google.api.client.json.GenericJson; @@ -26,15 +26,22 @@ import com.google.cloud.storage.Bucket; import com.google.cloud.storage.Storage; import com.google.cloud.storage.StorageOptions; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStreamReader; +import java.io.Reader; +import java.lang.reflect.Type; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; import java.time.Instant; import java.util.Base64; +import java.util.Map; // [END auth_custom_credential_supplier_okta] @@ -45,21 +52,25 @@ public class CustomCredentialSupplierOktaWorkload { public static void main(String[] args) throws IOException { + + // Reads the custom-credentials-okta-secrets.json if running locally. + loadConfigFromFile(); + // The audience for the workload identity federation. // Format: //iam.googleapis.com/projects//locations/global/ // workloadIdentityPools//providers/ - String gcpWorkloadAudience = System.getenv("GCP_WORKLOAD_AUDIENCE"); + String gcpWorkloadAudience = getConfiguration("GCP_WORKLOAD_AUDIENCE"); // The bucket to fetch data from. - String gcsBucketName = System.getenv("GCS_BUCKET_NAME"); + String gcsBucketName = getConfiguration("GCS_BUCKET_NAME"); // (Optional) The service account impersonation URL. - String saImpersonationUrl = System.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"); + String saImpersonationUrl = getConfiguration("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"); + String oktaDomain = getConfiguration("OKTA_DOMAIN"); + String oktaClientId = getConfiguration("OKTA_CLIENT_ID"); + String oktaClientSecret = getConfiguration("OKTA_CLIENT_SECRET"); if (gcpWorkloadAudience == null || gcsBucketName == null @@ -67,25 +78,92 @@ public static void main(String[] args) throws IOException { || oktaClientId == null || oktaClientSecret == null) { System.err.println( - "Error: Missing required environment variables. " - + "Required: GCP_WORKLOAD_AUDIENCE, GCS_BUCKET_NAME, " + "Error: Missing required configuration. " + + "Please provide it in a custom-credentials-okta-secrets.json file or as " + + "environment variables: 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()); + try { + 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()); + } catch (Exception e) { + System.err.println("Authentication or Request failed: " + e.getMessage()); + } + } + + /** + * Helper method to retrieve configuration. It checks Environment variables first, then System + * properties (populated by loadConfigFromFile). + */ + static String getConfiguration(String key) { + String value = System.getenv(key); + if (value == null) { + value = System.getProperty(key); + } + return value; + } + + /** + * If a local secrets file is present, load it into the System Properties. This is a + * "just-in-time" configuration for local development. These variables are only set for the + * current process. + */ + static void loadConfigFromFile() { + String secretsFile = + "custom-credentials-okta-secrets.json"; + if (!Files.exists(Paths.get(secretsFile))) { + return; + } + + try (Reader reader = Files.newBufferedReader(Paths.get(secretsFile))) { + Gson gson = new Gson(); + Type type = new TypeToken>() {}.getType(); + Map secrets = gson.fromJson(reader, type); + + if (secrets == null) { + return; + } + + // Map JSON keys (snake_case) to System Properties (UPPER_UNDERSCORE) + if (secrets.containsKey("gcp_workload_audience")) { + System.setProperty("GCP_WORKLOAD_AUDIENCE", secrets.get("gcp_workload_audience")); + } + if (secrets.containsKey("gcs_bucket_name")) { + System.setProperty("GCS_BUCKET_NAME", secrets.get("gcs_bucket_name")); + } + if (secrets.containsKey("gcp_service_account_impersonation_url")) { + System.setProperty( + "GCP_SERVICE_ACCOUNT_IMPERSONATION_URL", + secrets.get("gcp_service_account_impersonation_url")); + } + if (secrets.containsKey("okta_domain")) { + System.setProperty("OKTA_DOMAIN", secrets.get("okta_domain")); + } + if (secrets.containsKey("okta_client_id")) { + System.setProperty("OKTA_CLIENT_ID", secrets.get("okta_client_id")); + } + if (secrets.containsKey("okta_client_secret")) { + System.setProperty("OKTA_CLIENT_SECRET", secrets.get("okta_client_secret")); + } + + } catch (IOException e) { + System.err.println("Error reading secrets file: " + e.getMessage()); + } catch (com.google.gson.JsonSyntaxException e) { + System.err.println("Error: File is not valid JSON."); + } } /** diff --git a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/okta/README.md b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/okta/README.md new file mode 100644 index 00000000000..a5c820051d0 --- /dev/null +++ b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/okta/README.md @@ -0,0 +1,93 @@ +# Running the Custom Okta Credential Supplier Sample + +This sample demonstrates how to use a custom subject token supplier to authenticate with Google Cloud using Okta as an external identity provider. It uses the Client Credentials flow for machine-to-machine (M2M) authentication. + +## Prerequisites + +* An Okta developer account. +* A Google Cloud project with the IAM API enabled. +* A Google Cloud Storage bucket. Ensure that the authenticated user has access to this bucket. +* Java 11 or later installed. +* Maven installed. + +## Okta Configuration + +Before running the sample, you need to configure an Okta application for Machine-to-Machine (M2M) communication. + +### Create an M2M Application in Okta + +1. Log in to your Okta developer console. +2. Navigate to **Applications** > **Applications** and click **Create App Integration**. +3. Select **API Services** as the sign-on method and click **Next**. +4. Give your application a name and click **Save**. + +### Obtain Okta Credentials + +Once the application is created, you will find the following information in the **General** tab: + +* **Okta Domain**: Your Okta developer domain (e.g., `https://dev-123456.okta.com`). +* **Client ID**: The client ID for your application. +* **Client Secret**: The client secret for your application. + +You will need these values to configure the sample. + +## Google Cloud Configuration + +You need to configure a Workload Identity Pool in Google Cloud to trust the Okta application. + +### Set up Workload Identity Federation + +1. In the Google Cloud Console, navigate to **IAM & Admin** > **Workload Identity Federation**. +2. Click **Create Pool** to create a new Workload Identity Pool. +3. Add a new **OIDC provider** to the pool. +4. Configure the provider with your Okta domain as the issuer URL. +5. Map the Okta `sub` (subject) assertion to a GCP principal. + +For detailed instructions, refer to the [Workload Identity Federation documentation](https://cloud.google.com/iam/docs/workload-identity-federation). + +## Running the Sample + +To run the sample on your local system, you need to build the project and configure your credentials as environment variables. + +### 1. Build the Project + +This command compiles your code and downloads all dependencies. +```bash +mvn clean package +``` + +### 2. Configure Credentials + +This sample reads its configuration from environment variables. Set the following variables in your shell: + +* `OKTA_DOMAIN`: Your Okta developer domain (for example `https://dev-123456.okta.com`). +* `OKTA_CLIENT_ID`: The client ID for your application. +* `OKTA_CLIENT_SECRET`: The client secret for your application. +* `GCP_WORKLOAD_AUDIENCE`: The audience for the Google Cloud Workload Identity Pool. This is the full identifier of the Workload Identity Pool provider. +* `GCS_BUCKET_NAME`: The name of the Google Cloud Storage bucket to access. +* `GCP_SERVICE_ACCOUNT_IMPERSONATION_URL`: (Optional) The URL for service account impersonation. + +Example: +```bash +export OKTA_DOMAIN="https://dev-123456.okta.com" +export OKTA_CLIENT_ID="your-client-id" +# ... and so on for the other variables +``` + +### 3. Run the Application + +First, generate the classpath file: +```bash +mvn dependency:build-classpath -Dmdep.outputFile=cp.txt +``` + +Now, run the application, providing the generated classpath. +```bash +java -cp "target/auth-1.0.jar:$(cat cp.txt)" com.google.cloud.auth.samples.customcredentials.okta.CustomCredentialSupplierOktaWorkload +``` + +The script authenticates with Okta to get an OIDC token, exchanges that token for a Google Cloud federated token, and uses it to list metadata for the specified Google Cloud Storage bucket. + +## Testing + +This sample is not continuously tested. It is provided for instructional purposes and may require modifications to work in your environment. diff --git a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/okta/custom-credentials-okta-secrets.json.example b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/okta/custom-credentials-okta-secrets.json.example new file mode 100644 index 00000000000..5564fab6333 --- /dev/null +++ b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/okta/custom-credentials-okta-secrets.json.example @@ -0,0 +1,8 @@ +{ + "okta_domain": "https://your-okta-domain.okta.com", + "okta_client_id": "your-okta-client-id", + "okta_client_secret": "your-okta-client-secret", + "gcp_workload_audience": "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider", + "gcs_bucket_name": "your-gcs-bucket-name", + "gcp_service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/my-service-account@my-project.iam.gserviceaccount.com:generateAccessToken" +} \ No newline at end of file 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 deleted file mode 100644 index 36bdd9b536d..00000000000 --- a/auth/src/test/java/com/google/cloud/auth/samples/CustomCredentialSupplierAwsWorkloadTest.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 deleted file mode 100644 index be6eda6cad1..00000000000 --- a/auth/src/test/java/com/google/cloud/auth/samples/CustomCredentialSupplierOktaWorkloadTest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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(); - } -} diff --git a/auth/src/test/java/com/google/cloud/auth/samples/customcredentials/aws/CustomCredentialSupplierAwsWorkloadTest.java b/auth/src/test/java/com/google/cloud/auth/samples/customcredentials/aws/CustomCredentialSupplierAwsWorkloadTest.java new file mode 100644 index 00000000000..7dd63657302 --- /dev/null +++ b/auth/src/test/java/com/google/cloud/auth/samples/customcredentials/aws/CustomCredentialSupplierAwsWorkloadTest.java @@ -0,0 +1,72 @@ +/* + * 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.customcredentials.aws; + +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_KEY = "GCP_WORKLOAD_AUDIENCE"; + private static final String BUCKET_KEY = "GCS_BUCKET_NAME"; + private static final String IMPERSONATION_KEY = "GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"; + + @BeforeClass + public static void setupConfiguration() { + // This will load secrets.json into System Properties if the file exists. + CustomCredentialSupplierAwsWorkload.loadConfigFromFile(); + + String audience = CustomCredentialSupplierAwsWorkload.getConfiguration(AUDIENCE_KEY); + String bucket = CustomCredentialSupplierAwsWorkload.getConfiguration(BUCKET_KEY); + + // Check for AWS specific requirements (needed by the SDK) + String awsKey = System.getProperty("aws.accessKeyId"); // Set by loadConfigFromFile + if (awsKey == null) { + awsKey = System.getenv("AWS_ACCESS_KEY_ID"); + } + + // Skip the test if configuration is missing (mirrors pytest.skip). + assumeTrue("Skipping test: " + AUDIENCE_KEY + " is missing.", audience != null); + assumeTrue("Skipping test: " + BUCKET_KEY + " is missing.", bucket != null); + assumeTrue("Skipping test: AWS Credentials not found.", awsKey != null); + } + + @Test + public void testAuthenticateWithAwsCredentials_system() throws Exception { + // Retrieve values using the helper + String audience = CustomCredentialSupplierAwsWorkload.getConfiguration(AUDIENCE_KEY); + String bucketName = CustomCredentialSupplierAwsWorkload.getConfiguration(BUCKET_KEY); + String impersonationUrl = + CustomCredentialSupplierAwsWorkload.getConfiguration(IMPERSONATION_KEY); + + // Act: Run the authentication sample + Bucket bucket = + CustomCredentialSupplierAwsWorkload.authenticateWithAwsCredentials( + audience, impersonationUrl, bucketName); + + // 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/customcredentials/okta/CustomCredentialSupplierOktaWorkloadTest.java b/auth/src/test/java/com/google/cloud/auth/samples/customcredentials/okta/CustomCredentialSupplierOktaWorkloadTest.java new file mode 100644 index 00000000000..83aefdf3793 --- /dev/null +++ b/auth/src/test/java/com/google/cloud/auth/samples/customcredentials/okta/CustomCredentialSupplierOktaWorkloadTest.java @@ -0,0 +1,86 @@ +/* + * 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.customcredentials.okta; + +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_KEY = "GCP_WORKLOAD_AUDIENCE"; + private static final String BUCKET_KEY = "GCS_BUCKET_NAME"; + private static final String IMPERSONATION_KEY = "GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"; + + private static final String OKTA_DOMAIN_KEY = "OKTA_DOMAIN"; + private static final String OKTA_CLIENT_ID_KEY = "OKTA_CLIENT_ID"; + private static final String OKTA_CLIENT_SECRET_KEY = "OKTA_CLIENT_SECRET"; + + @BeforeClass + public static void setupConfiguration() { + // 1. Call the loader from the main class. + // This will load custom-credentials-okta-secrets.json into System Properties if the file + // exists. + CustomCredentialSupplierOktaWorkload.loadConfigFromFile(); + + // 2. Validate requirements using the main class's getConfiguration helper. + // This checks both Environment Variables and the System Properties we just loaded. + requireConfig(AUDIENCE_KEY); + requireConfig(BUCKET_KEY); + requireConfig(OKTA_DOMAIN_KEY); + requireConfig(OKTA_CLIENT_ID_KEY); + requireConfig(OKTA_CLIENT_SECRET_KEY); + } + + private static void requireConfig(String key) { + String value = CustomCredentialSupplierOktaWorkload.getConfiguration(key); + assumeTrue("Skipping test: " + key + " is missing.", value != null && !value.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 { + // Retrieve values using the helper from the main class + String audience = CustomCredentialSupplierOktaWorkload.getConfiguration(AUDIENCE_KEY); + String bucketName = CustomCredentialSupplierOktaWorkload.getConfiguration(BUCKET_KEY); + String impersonationUrl = + CustomCredentialSupplierOktaWorkload.getConfiguration(IMPERSONATION_KEY); + + String oktaDomain = CustomCredentialSupplierOktaWorkload.getConfiguration(OKTA_DOMAIN_KEY); + String oktaClientId = CustomCredentialSupplierOktaWorkload.getConfiguration(OKTA_CLIENT_ID_KEY); + String oktaSecret = + CustomCredentialSupplierOktaWorkload.getConfiguration(OKTA_CLIENT_SECRET_KEY); + + // 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(); + } +} From c71280cde1dcd71213274f0398789a6068ca4593 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Sun, 14 Dec 2025 19:01:47 -0800 Subject: [PATCH 4/5] Added newlines at the end of files that were missing it. --- .../google/cloud/auth/samples/customcredentials/aws/Dockerfile | 2 +- .../aws/custom-credentials-aws-secrets.json.example | 2 +- .../google/cloud/auth/samples/customcredentials/aws/pod.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/Dockerfile b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/Dockerfile index 5d572ddc309..73efa152fb7 100644 --- a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/Dockerfile +++ b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/Dockerfile @@ -32,4 +32,4 @@ COPY --from=builder --chown=appuser:appuser /app/target/libs lib/ # Run with Classpath # We add 'app.jar' and everything in 'lib/' to the classpath. # We explicitly specify the Main Class here, so your shared pom.xml doesn't need to change. -CMD ["java", "-cp", "app.jar:lib/*", "com.google.cloud.auth.samples.customcredentials.aws.CustomCredentialSupplierAwsWorkload"] \ No newline at end of file +CMD ["java", "-cp", "app.jar:lib/*", "com.google.cloud.auth.samples.customcredentials.aws.CustomCredentialSupplierAwsWorkload"] diff --git a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/custom-credentials-aws-secrets.json.example b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/custom-credentials-aws-secrets.json.example index e1fc447c78e..300dc70c138 100644 --- a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/custom-credentials-aws-secrets.json.example +++ b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/custom-credentials-aws-secrets.json.example @@ -5,4 +5,4 @@ "gcp_workload_audience": "YOUR_GCP_WORKLOAD_AUDIENCE", "gcs_bucket_name": "YOUR_GCS_BUCKET_NAME", "gcp_service_account_impersonation_url": "YOUR_GCP_SERVICE_ACCOUNT_IMPERSONATION_URL" -} \ No newline at end of file +} diff --git a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/pod.yaml b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/pod.yaml index 1a90b58a5a9..7cc35c293ca 100644 --- a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/pod.yaml +++ b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/pod.yaml @@ -41,4 +41,4 @@ spec: # REQUIRED: The bucket to list - name: GCS_BUCKET_NAME - value: "your-gcs-bucket-name" \ No newline at end of file + value: "your-gcs-bucket-name" From db3c757c5ae15badf109d230fb04c5ed3e511abe Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Mon, 15 Dec 2025 15:32:00 -0800 Subject: [PATCH 5/5] ReadMe changes for Okta script. other vanity changes. --- .../CustomCredentialSupplierAwsWorkload.java | 10 +++---- .../samples/customcredentials/aws/Dockerfile | 4 +-- .../samples/customcredentials/aws/README.md | 2 +- .../CustomCredentialSupplierOktaWorkload.java | 14 ++++----- .../samples/customcredentials/okta/README.md | 30 +++++++------------ 5 files changed, 22 insertions(+), 38 deletions(-) diff --git a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/CustomCredentialSupplierAwsWorkload.java b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/CustomCredentialSupplierAwsWorkload.java index fb36fde7b90..82f4836da58 100644 --- a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/CustomCredentialSupplierAwsWorkload.java +++ b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/CustomCredentialSupplierAwsWorkload.java @@ -105,12 +105,13 @@ static String getConfiguration(String key) { * current process. */ static void loadConfigFromFile() { - String secretsFile = "custom-credentials-aws-secrets.json"; - if (!Files.exists(Paths.get(secretsFile))) { + // By default, this expects the file to be in the project root. + String secretsFilePath = "custom-credentials-aws-secrets.json"; + if (!Files.exists(Paths.get(secretsFilePath))) { return; } - try (Reader reader = Files.newBufferedReader(Paths.get(secretsFile))) { + try (Reader reader = Files.newBufferedReader(Paths.get(secretsFilePath))) { // Use Gson to parse the JSON file into a Map Gson gson = new Gson(); Type type = new TypeToken>() {}.getType(); @@ -164,10 +165,8 @@ 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) @@ -182,7 +181,6 @@ public static Bucket authenticateWithAwsCredentials( 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); diff --git a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/Dockerfile b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/Dockerfile index 73efa152fb7..49069fc6410 100644 --- a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/Dockerfile +++ b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/Dockerfile @@ -22,8 +22,7 @@ RUN useradd -m appuser USER appuser WORKDIR /app -# Copy the Thin Jar (Your application code) -# Ensure the name 'auth-1.0.jar' matches your pom.xml artifactId/version +# Copy the Thin Jar COPY --from=builder --chown=appuser:appuser /app/target/auth-1.0.jar app.jar # Copy the Dependencies (The libraries) @@ -31,5 +30,4 @@ COPY --from=builder --chown=appuser:appuser /app/target/libs lib/ # Run with Classpath # We add 'app.jar' and everything in 'lib/' to the classpath. -# We explicitly specify the Main Class here, so your shared pom.xml doesn't need to change. CMD ["java", "-cp", "app.jar:lib/*", "com.google.cloud.auth.samples.customcredentials.aws.CustomCredentialSupplierAwsWorkload"] diff --git a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/README.md b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/README.md index 30f86397398..6dd7d5428c8 100644 --- a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/README.md +++ b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/aws/README.md @@ -91,7 +91,7 @@ For more detailed information, see the documentation on [Configuring Workload Id Create a `Dockerfile` for the Java application and push the image to a container registry (for example Amazon ECR) that your EKS cluster can access. -**Note:** The provided [`Dockerfile`](Dockerfile) uses a multi-stage build to compile the Java code. It is an example that may need modification for your specific needs. +**Note:** The provided [`Dockerfile`](Dockerfile) is an example that may need modification for your specific needs. Build and push the image: ```bash diff --git a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/okta/CustomCredentialSupplierOktaWorkload.java b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/okta/CustomCredentialSupplierOktaWorkload.java index 4ce51d90439..9bcbd42bef2 100644 --- a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/okta/CustomCredentialSupplierOktaWorkload.java +++ b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/okta/CustomCredentialSupplierOktaWorkload.java @@ -27,6 +27,7 @@ import com.google.cloud.storage.Storage; import com.google.cloud.storage.StorageOptions; import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; import java.io.BufferedReader; import java.io.DataOutputStream; @@ -122,13 +123,13 @@ static String getConfiguration(String key) { * current process. */ static void loadConfigFromFile() { - String secretsFile = - "custom-credentials-okta-secrets.json"; - if (!Files.exists(Paths.get(secretsFile))) { + // By default, this expects the file to be in the project root. + String secretsFilePath = "custom-credentials-okta-secrets.json"; + if (!Files.exists(Paths.get(secretsFilePath))) { return; } - try (Reader reader = Files.newBufferedReader(Paths.get(secretsFile))) { + try (Reader reader = Files.newBufferedReader(Paths.get(secretsFilePath))) { Gson gson = new Gson(); Type type = new TypeToken>() {}.getType(); Map secrets = gson.fromJson(reader, type); @@ -161,7 +162,7 @@ static void loadConfigFromFile() { } catch (IOException e) { System.err.println("Error reading secrets file: " + e.getMessage()); - } catch (com.google.gson.JsonSyntaxException e) { + } catch (JsonSyntaxException e) { System.err.println("Error: File is not valid JSON."); } } @@ -188,11 +189,9 @@ public static Bucket authenticateWithOktaCredentials( 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) @@ -208,7 +207,6 @@ public static Bucket authenticateWithOktaCredentials( 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); diff --git a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/okta/README.md b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/okta/README.md index a5c820051d0..f2992fc16ed 100644 --- a/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/okta/README.md +++ b/auth/src/main/java/com/google/cloud/auth/samples/customcredentials/okta/README.md @@ -1,4 +1,4 @@ -# Running the Custom Okta Credential Supplier Sample +# Running the Custom Okta Credential Supplier Sample (Java) This sample demonstrates how to use a custom subject token supplier to authenticate with Google Cloud using Okta as an external identity provider. It uses the Client Credentials flow for machine-to-machine (M2M) authentication. @@ -47,7 +47,7 @@ For detailed instructions, refer to the [Workload Identity Federation documentat ## Running the Sample -To run the sample on your local system, you need to build the project and configure your credentials as environment variables. +To run the sample on your local system, you need to build the project and provide configuration via a secrets file. ### 1. Build the Project @@ -56,23 +56,13 @@ This command compiles your code and downloads all dependencies. mvn clean package ``` -### 2. Configure Credentials +### Configure Credentials for Local Development -This sample reads its configuration from environment variables. Set the following variables in your shell: - -* `OKTA_DOMAIN`: Your Okta developer domain (for example `https://dev-123456.okta.com`). -* `OKTA_CLIENT_ID`: The client ID for your application. -* `OKTA_CLIENT_SECRET`: The client secret for your application. -* `GCP_WORKLOAD_AUDIENCE`: The audience for the Google Cloud Workload Identity Pool. This is the full identifier of the Workload Identity Pool provider. -* `GCS_BUCKET_NAME`: The name of the Google Cloud Storage bucket to access. -* `GCP_SERVICE_ACCOUNT_IMPERSONATION_URL`: (Optional) The URL for service account impersonation. - -Example: -```bash -export OKTA_DOMAIN="https://dev-123456.okta.com" -export OKTA_CLIENT_ID="your-client-id" -# ... and so on for the other variables -``` +1. Copy the example secrets file to a new file named `custom-credentials-okta-secrets.json` in the project root: + ```bash + cp custom-credentials-okta-secrets.json.example custom-credentials-okta-secrets.json + ``` +2. Open `custom-credentials-okta-secrets.json` and fill in the required values for your AWS and Google Cloud configuration. Do not check your `custom-credentials-okta-secrets.json` file into version control. ### 3. Run the Application @@ -81,12 +71,12 @@ First, generate the classpath file: mvn dependency:build-classpath -Dmdep.outputFile=cp.txt ``` -Now, run the application, providing the generated classpath. +Now, run the application, providing the generated classpath: ```bash java -cp "target/auth-1.0.jar:$(cat cp.txt)" com.google.cloud.auth.samples.customcredentials.okta.CustomCredentialSupplierOktaWorkload ``` -The script authenticates with Okta to get an OIDC token, exchanges that token for a Google Cloud federated token, and uses it to list metadata for the specified Google Cloud Storage bucket. +The script will detect the `custom-credentials-okta-secrets.json` file, authenticate with Okta to get an OIDC token, exchange it for a Google Cloud federated token, and retrieve metadata for your GCS bucket. ## Testing