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