From 49d17f5aefbced8b9fffaa1cb08f3b1fccb6f178 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Fri, 10 Oct 2025 17:34:49 -0700 Subject: [PATCH 1/8] Included changes for Documenting Custom Credential Suppliers for AWS Workloads and Okta Workload. --- samples/snippets/pom.xml | 19 ++ .../CustomCredentialSupplierAwsWorkload.java | 142 +++++++++++++ .../CustomCredentialSupplierOktaWorkload.java | 197 ++++++++++++++++++ 3 files changed, 358 insertions(+) create mode 100644 samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java create mode 100644 samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index dbf7630e3..4d82218dc 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -63,6 +63,25 @@ google-cloud-storage + + + software.amazon.awssdk + auth + 2.20.27 + + + software.amazon.awssdk + regions + 2.20.27 + + + + + com.google.code.gson + gson + 2.10.1 + + junit diff --git a/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java b/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java new file mode 100644 index 000000000..5fcb9531d --- /dev/null +++ b/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java @@ -0,0 +1,142 @@ +/* + * 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 com.google.auth.oauth2.AwsCredentials; +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; + +/** + * This sample demonstrates how to use a custom AWS security credentials supplier to authenticate + * with Google Cloud. + */ +public class CustomCredentialSupplierAwsWorkload { + + public static void main(String[] args) throws IOException { + // TODO(Developer): Replace these variables with your actual values. + String gcpWorkloadAudience = System.getenv("GCP_WORKLOAD_AUDIENCE"); + String saImpersonationUrl = System.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"); + String gcsBucketName = System.getenv("GCS_BUCKET_NAME"); + + if (gcpWorkloadAudience == null + || saImpersonationUrl == null + || gcsBucketName == null) { + System.out.println( + "Missing required environment variables. Please check your environment settings. " + + "Required: GCP_WORKLOAD_AUDIENCE, GCP_SERVICE_ACCOUNT_IMPERSONATION_URL, GCS_BUCKET_NAME"); + return; + } + + customCredentialSupplierAwsWorkload(gcpWorkloadAudience, saImpersonationUrl, gcsBucketName); + } + + public static void customCredentialSupplierAwsWorkload( + String gcpWorkloadAudience, String saImpersonationUrl, String gcsBucketName) + throws IOException { + // 1. Instantiate the custom supplier. + CustomAwsSupplier customSupplier = new CustomAwsSupplier(); + + // 2. Configure the AwsCredentials options. + GoogleCredentials credentials = + AwsCredentials.newBuilder() + .setAudience(gcpWorkloadAudience) + .setSubjectTokenType("urn:ietf:params:aws:token-type:aws4_request") + .setServiceAccountImpersonationUrl(saImpersonationUrl) + .setAwsSecurityCredentialsSupplier(customSupplier) + .build(); + + // 3. Use the credentials to make an authenticated request. + Storage storage = StorageOptions.newBuilder().setCredentials(credentials).build().getService(); + + System.out.println("[Test] Getting metadata for bucket: " + gcsBucketName + "..."); + Bucket bucket = storage.get(gcsBucketName); + System.out.println(" --- SUCCESS! ---"); + System.out.println("Successfully authenticated and retrieved bucket data:"); + System.out.println(bucket.toString()); + } + + /** + * Custom AWS Security Credentials Supplier. + * + *

This implementation resolves AWS credentials using the default provider chain from the AWS + * SDK. This allows fetching credentials from environment variables, shared credential files + * (~/.aws/credentials), or IAM roles for service accounts (IRSA) in EKS, etc. + */ + private static class CustomAwsSupplier implements AwsSecurityCredentialsSupplier { + + private final AwsCredentialsProvider awsCredentialsProvider; + private String region; + + public CustomAwsSupplier() { + // The AWS SDK handles memoization (caching) and proactive refreshing internally. + this.awsCredentialsProvider = DefaultCredentialsProvider.create(); + } + + /** + * Returns the AWS region. This is required for signing the AWS request. It resolves the region + * automatically by using the default AWS region provider chain, which searches for the region + * in the standard locations (environment variables, AWS config file, etc.). + */ + @Override + public String getRegion(ExternalAccountSupplierContext context) { + if (this.region == null) { + Region awsRegion = new DefaultAwsRegionProviderChain().getRegion(); + if (awsRegion != null) { + this.region = awsRegion.id(); + } + } + if (this.region == null) { + throw new IllegalStateException( + "CustomAwsSupplier: Unable to resolve AWS region. Please set the AWS_REGION " + + "environment variable or configure it in your ~/.aws/config file."); + } + return this.region; + } + + /** Retrieves AWS security credentials using the AWS SDK's default provider chain. */ + @Override + public com.google.auth.oauth2.AwsSecurityCredentials getCredentials( + ExternalAccountSupplierContext context) { + software.amazon.awssdk.auth.credentials.AwsCredentials credentials = + this.awsCredentialsProvider.resolveCredentials(); + if (credentials == null + || credentials.accessKeyId() == null + || credentials.secretAccessKey() == null) { + throw new IllegalStateException( + "Unable to resolve AWS credentials from the default provider chain. " + + "Ensure your AWS CLI is configured, or AWS environment variables " + + "(like AWS_ACCESS_KEY_ID) are set."); + } + + String sessionToken = null; + if (credentials instanceof AwsSessionCredentials) { + sessionToken = ((AwsSessionCredentials) credentials).sessionToken(); + } + + return new com.google.auth.oauth2.AwsSecurityCredentials( + credentials.accessKeyId(), credentials.secretAccessKey(), sessionToken); + } + } +} \ No newline at end of file diff --git a/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java b/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java new file mode 100644 index 000000000..d19248543 --- /dev/null +++ b/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java @@ -0,0 +1,197 @@ +/* + * 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 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 com.google.gson.Gson; +import com.google.gson.JsonObject; +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.util.Base64; + +/** + * This sample demonstrates how to use a custom subject token supplier to authenticate with Google + * Cloud, using Okta as the identity provider. + */ +public class CustomCredentialSupplierOktaWorkload { + + public static void main(String[] args) throws IOException { + // TODO(Developer): Replace these variables with your actual values. + String gcpWorkloadAudience = System.getenv("GCP_WORKLOAD_AUDIENCE"); + String serviceAccountImpersonationUrl = + System.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"); + String gcsBucketName = System.getenv("GCS_BUCKET_NAME"); + String oktaDomain = System.getenv("OKTA_DOMAIN"); + String oktaClientId = System.getenv("OKTA_CLIENT_ID"); + String oktaClientSecret = System.getenv("OKTA_CLIENT_SECRET"); + + if (gcpWorkloadAudience == null + || serviceAccountImpersonationUrl == null + || gcsBucketName == null + || oktaDomain == null + || oktaClientId == null + || oktaClientSecret == null) { + System.out.println( + "Missing required environment variables. Please check your environment settings. " + + "Required: GCP_WORKLOAD_AUDIENCE, GCP_SERVICE_ACCOUNT_IMPERSONATION_URL, " + + "GCS_BUCKET_NAME, OKTA_DOMAIN, OKTA_CLIENT_ID, OKTA_CLIENT_SECRET"); + return; + } + + customCredentialSupplierOktaWorkload( + gcpWorkloadAudience, + serviceAccountImpersonationUrl, + gcsBucketName, + oktaDomain, + oktaClientId, + oktaClientSecret); + } + + public static void customCredentialSupplierOktaWorkload( + String gcpWorkloadAudience, + String serviceAccountImpersonationUrl, + 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. + GoogleCredentials credentials = + IdentityPoolCredentials.newBuilder() + .setAudience(gcpWorkloadAudience) + .setSubjectTokenType("urn:ietf:params:oauth:token-type:jwt") + .setTokenUrl("https://sts.googleapis.com/v1/token") + .setSubjectTokenSupplier(oktaSupplier) + .setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl) + .build(); + + // 3. Use the credentials to make an authenticated request. + Storage storage = StorageOptions.newBuilder().setCredentials(credentials).build().getService(); + + System.out.println("[Test] Getting metadata for bucket: " + gcsBucketName + "..."); + Bucket bucket = storage.get(gcsBucketName); + System.out.println(" --- SUCCESS! ---"); + System.out.println("Successfully authenticated and retrieved bucket data:"); + System.out.println(bucket.toString()); + } + + /** + * A custom SubjectTokenSupplier that authenticates with Okta using the Client Credentials grant + * flow. + */ + private static class OktaClientCredentialsSupplier implements IdentityPoolSubjectTokenSupplier { + + private final String oktaTokenUrl; + private final String clientId; + private final String clientSecret; + private String accessToken; + private long expiryTime; + + public OktaClientCredentialsSupplier(String domain, String clientId, String clientSecret) { + this.oktaTokenUrl = domain + "/oauth2/default/v1/token"; + this.clientId = clientId; + this.clientSecret = clientSecret; + System.out.println("OktaClientCredentialsSupplier initialized."); + } + + /** + * 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 && System.currentTimeMillis() < this.expiryTime - 60 * 1000; + + if (isTokenValid) { + System.out.println("[Supplier] Returning cached Okta Access token."); + return this.accessToken; + } + + System.out.println( + "[Supplier] Token is missing or expired. Fetching new Okta Access token via Client " + + "Credentials grant..."); + 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"); + + 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())) { + 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()))) { + StringBuilder response = new StringBuilder(); + String line; + while ((line = in.readLine()) != null) { + response.append(line); + } + + Gson gson = new Gson(); + JsonObject jsonObject = gson.fromJson(response.toString(), JsonObject.class); + + if (jsonObject.has("access_token") && jsonObject.has("expires_in")) { + this.accessToken = jsonObject.get("access_token").getAsString(); + int expiresIn = jsonObject.get("expires_in").getAsInt(); + this.expiryTime = System.currentTimeMillis() + expiresIn * 1000; + System.out.println( + "[Supplier] Successfully received Access Token from Okta. Expires in " + + expiresIn + + " seconds."); + } else { + throw new IOException("Access token or expires_in not found in Okta response."); + } + } + } else { + throw new IOException( + "Failed to authenticate with Okta using Client Credentials grant. Response code: " + + responseCode); + } + } + } +} \ No newline at end of file From 20f2a5b580bd7c6150a00e299624e8a737885f12 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Mon, 13 Oct 2025 12:38:28 -0700 Subject: [PATCH 2/8] Added documentation and formatting changes. --- .../CustomCredentialSupplierAwsWorkload.java | 8 ++---- .../CustomCredentialSupplierOktaWorkload.java | 28 +++++++++++++++---- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java b/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java index 5fcb9531d..01a4c294d 100644 --- a/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java +++ b/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java @@ -21,7 +21,6 @@ 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; @@ -34,7 +33,7 @@ */ public class CustomCredentialSupplierAwsWorkload { - public static void main(String[] args) throws IOException { + public static void main(String[] args) { // TODO(Developer): Replace these variables with your actual values. String gcpWorkloadAudience = System.getenv("GCP_WORKLOAD_AUDIENCE"); String saImpersonationUrl = System.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"); @@ -53,8 +52,7 @@ public static void main(String[] args) throws IOException { } public static void customCredentialSupplierAwsWorkload( - String gcpWorkloadAudience, String saImpersonationUrl, String gcsBucketName) - throws IOException { + String gcpWorkloadAudience, String saImpersonationUrl, String gcsBucketName) { // 1. Instantiate the custom supplier. CustomAwsSupplier customSupplier = new CustomAwsSupplier(); @@ -139,4 +137,4 @@ public com.google.auth.oauth2.AwsSecurityCredentials getCredentials( credentials.accessKeyId(), credentials.secretAccessKey(), sessionToken); } } -} \ No newline at end of file +} diff --git a/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java b/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java index d19248543..0ef4975fb 100644 --- a/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java +++ b/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java @@ -38,7 +38,7 @@ */ public class CustomCredentialSupplierOktaWorkload { - public static void main(String[] args) throws IOException { + public static void main(String[] args) { // TODO(Developer): Replace these variables with your actual values. String gcpWorkloadAudience = System.getenv("GCP_WORKLOAD_AUDIENCE"); String serviceAccountImpersonationUrl = @@ -76,8 +76,7 @@ public static void customCredentialSupplierOktaWorkload( String gcsBucketName, String oktaDomain, String oktaClientId, - String oktaClientSecret) - throws IOException { + String oktaClientSecret) { // 1. Instantiate our custom supplier with Okta credentials. OktaClientCredentialsSupplier oktaSupplier = new OktaClientCredentialsSupplier(oktaDomain, oktaClientId, oktaClientSecret); @@ -143,8 +142,19 @@ public String getSubjectToken(ExternalAccountSupplierContext context) throws IOE } /** - * Performs the Client Credentials grant flow by making a POST request to Okta's token - * endpoint. + * Performs the Client Credentials grant flow by making a POST request to Okta's token endpoint. + * + *

To set up the Okta application for this flow: + * + *

    + *
  1. In your Okta developer console, create a new Application of type "Machine-to-Machine + * (M2M)". + *
  2. Under the "General" tab, ensure that "Client Credentials" is an allowed grant type. + *
  3. Note the "Client ID" and "Client Secret" for your application. + *
  4. Navigate to "Security" > "API" and select your authorization server (e.g., "default"). + *
  5. Under the "Scopes" tab, add a custom scope (e.g., "gcp.test.read"). + *
  6. Ensure your M2M application is granted this scope. + *
*/ private void fetchOktaAccessToken() throws IOException { URL url = new URL(this.oktaTokenUrl); @@ -152,12 +162,18 @@ private void fetchOktaAccessToken() throws IOException { conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + // The client_id and client_secret are sent in a Basic Auth header, as required by the + // OAuth 2.0 Client Credentials grant specification. The credentials are Base64 encoded. 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())) { + // For the Client Credentials grant, scopes are optional and define the permissions + // the access token will have. Replace "gcp.test.read" with the scopes defined in your + // Okta authorization server. Multiple scopes can be requested by space-separating them + // (e.g., "scope1 scope2"). String params = "grant_type=client_credentials&scope=gcp.test.read"; out.writeBytes(params); out.flush(); @@ -194,4 +210,4 @@ private void fetchOktaAccessToken() throws IOException { } } } -} \ No newline at end of file +} From 6215db5a0c527307914a8e958b261061956926a2 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Tue, 14 Oct 2025 16:13:39 -0700 Subject: [PATCH 3/8] nit fixes. --- .../src/main/java/CustomCredentialSupplierAwsWorkload.java | 2 +- .../src/main/java/CustomCredentialSupplierOktaWorkload.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java b/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java index 01a4c294d..e70df8c2f 100644 --- a/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java +++ b/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java @@ -29,7 +29,7 @@ /** * This sample demonstrates how to use a custom AWS security credentials supplier to authenticate - * with Google Cloud. + * to Google Cloud Storage. */ public class CustomCredentialSupplierAwsWorkload { diff --git a/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java b/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java index 0ef4975fb..871df8ba6 100644 --- a/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java +++ b/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java @@ -33,8 +33,8 @@ import java.util.Base64; /** - * This sample demonstrates how to use a custom subject token supplier to authenticate with Google - * Cloud, using Okta as the identity provider. + * 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 { From f17cd9164d6c40095ceaf31ff395cf88a379b790 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Wed, 22 Oct 2025 12:49:40 -0700 Subject: [PATCH 4/8] Addressed PR comments. --- samples/snippets/pom.xml | 6 ------ .../CustomCredentialSupplierAwsWorkload.java | 9 +++------ .../CustomCredentialSupplierOktaWorkload.java | 19 +++++++++++-------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index 4d82218dc..a96e40667 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -75,12 +75,6 @@ 2.20.27
- - - com.google.code.gson - gson - 2.10.1 - diff --git a/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java b/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java index e70df8c2f..25294b7ed 100644 --- a/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java +++ b/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java @@ -14,10 +14,7 @@ * limitations under the License. */ -import com.google.auth.oauth2.AwsCredentials; -import com.google.auth.oauth2.AwsSecurityCredentialsSupplier; -import com.google.auth.oauth2.ExternalAccountSupplierContext; -import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.*; import com.google.cloud.storage.Bucket; import com.google.cloud.storage.Storage; import com.google.cloud.storage.StorageOptions; @@ -34,7 +31,7 @@ public class CustomCredentialSupplierAwsWorkload { public static void main(String[] args) { - // TODO(Developer): Replace these variables with your actual values. + // TODO(Developer): Set these environment variable values. String gcpWorkloadAudience = System.getenv("GCP_WORKLOAD_AUDIENCE"); String saImpersonationUrl = System.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"); String gcsBucketName = System.getenv("GCS_BUCKET_NAME"); @@ -115,7 +112,7 @@ public String getRegion(ExternalAccountSupplierContext context) { /** Retrieves AWS security credentials using the AWS SDK's default provider chain. */ @Override - public com.google.auth.oauth2.AwsSecurityCredentials getCredentials( + public AwsSecurityCredentials getCredentials( ExternalAccountSupplierContext context) { software.amazon.awssdk.auth.credentials.AwsCredentials credentials = this.awsCredentialsProvider.resolveCredentials(); diff --git a/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java b/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java index 871df8ba6..3c2d33d83 100644 --- a/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java +++ b/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java @@ -14,6 +14,8 @@ * limitations under the License. */ +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; @@ -21,8 +23,6 @@ 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.JsonObject; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; @@ -188,13 +188,16 @@ private void fetchOktaAccessToken() throws IOException { response.append(line); } - Gson gson = new Gson(); - JsonObject jsonObject = gson.fromJson(response.toString(), JsonObject.class); + GenericJson jsonObject = + GsonFactory.getDefaultInstance() + .createJsonParser(response.toString()) + .parse(GenericJson.class); - if (jsonObject.has("access_token") && jsonObject.has("expires_in")) { - this.accessToken = jsonObject.get("access_token").getAsString(); - int expiresIn = jsonObject.get("expires_in").getAsInt(); - this.expiryTime = System.currentTimeMillis() + expiresIn * 1000; + if (jsonObject.containsKey("access_token") && jsonObject.containsKey("expires_in")) { + this.accessToken = (String) jsonObject.get("access_token"); + Number expiresInNumber = (Number) jsonObject.get("expires_in"); + int expiresIn = expiresInNumber.intValue(); + this.expiryTime = System.currentTimeMillis() + expiresIn * 1000L; System.out.println( "[Supplier] Successfully received Access Token from Okta. Expires in " + expiresIn From d6d4d4c04c6583b75225e7006af31f686e271ae0 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Wed, 22 Oct 2025 16:23:51 -0700 Subject: [PATCH 5/8] Listed out imports. Added more user friendly documentation. --- .../CustomCredentialSupplierAwsWorkload.java | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java b/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java index 25294b7ed..a03314ac0 100644 --- a/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java +++ b/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java @@ -14,7 +14,11 @@ * limitations under the License. */ -import com.google.auth.oauth2.*; +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; @@ -32,8 +36,26 @@ public class CustomCredentialSupplierAwsWorkload { public static void main(String[] args) { // TODO(Developer): Set these environment variable values. + + // 1. AWS Credentials: + // If running on a local system, the user must set AWS_REGION, AWS_ACCESS_KEY_ID, and + // AWS_SECRET_ACCESS_KEY as environment variables. If running in an AWS environment (e.g., + // ECS, EKS), these variables will be auto-detected. + + // 2. GCP_WORKLOAD_AUDIENCE: + // The audience for the workload identity federation. This is the full resource name of the + // Workload Identity Pool Provider, in the following format: + // //iam.googleapis.com/projects//locations/global/workloadIdentityPools//providers/ String gcpWorkloadAudience = System.getenv("GCP_WORKLOAD_AUDIENCE"); + + // 3. GCP_SERVICE_ACCOUNT_IMPERSONATION_URL: + // The service account impersonation URL. This is the URL for impersonating a service account, + // in the following format: + // https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/:generateAccessToken String saImpersonationUrl = System.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"); + + // 4. GCS_BUCKET_NAME: + // The name of the bucket that you wish to fetch data for. String gcsBucketName = System.getenv("GCS_BUCKET_NAME"); if (gcpWorkloadAudience == null @@ -57,6 +79,8 @@ public static void customCredentialSupplierAwsWorkload( GoogleCredentials credentials = 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") .setServiceAccountImpersonationUrl(saImpersonationUrl) .setAwsSecurityCredentialsSupplier(customSupplier) @@ -65,7 +89,7 @@ public static void customCredentialSupplierAwsWorkload( // 3. Use the credentials to make an authenticated request. Storage storage = StorageOptions.newBuilder().setCredentials(credentials).build().getService(); - System.out.println("[Test] Getting metadata for bucket: " + gcsBucketName + "..."); + System.out.println("Getting metadata for bucket: " + gcsBucketName + "..."); Bucket bucket = storage.get(gcsBucketName); System.out.println(" --- SUCCESS! ---"); System.out.println("Successfully authenticated and retrieved bucket data:"); From faa55a796520f0d9f597febc7439086a26455065 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Wed, 22 Oct 2025 17:17:32 -0700 Subject: [PATCH 6/8] Added documentation for env variables. --- .../CustomCredentialSupplierAwsWorkload.java | 11 ++--- .../CustomCredentialSupplierOktaWorkload.java | 48 ++++++++++++------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java b/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java index a03314ac0..ab5d474d5 100644 --- a/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java +++ b/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java @@ -29,8 +29,8 @@ import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain; /** - * This sample demonstrates how to use a custom AWS security credentials supplier to authenticate - * to Google Cloud Storage. + * This sample demonstrates how to use a custom AWS security credentials supplier to authenticate to + * Google Cloud Storage. */ public class CustomCredentialSupplierAwsWorkload { @@ -58,9 +58,7 @@ public static void main(String[] args) { // The name of the bucket that you wish to fetch data for. String gcsBucketName = System.getenv("GCS_BUCKET_NAME"); - if (gcpWorkloadAudience == null - || saImpersonationUrl == null - || gcsBucketName == null) { + if (gcpWorkloadAudience == null || saImpersonationUrl == null || gcsBucketName == null) { System.out.println( "Missing required environment variables. Please check your environment settings. " + "Required: GCP_WORKLOAD_AUDIENCE, GCP_SERVICE_ACCOUNT_IMPERSONATION_URL, GCS_BUCKET_NAME"); @@ -136,8 +134,7 @@ public String getRegion(ExternalAccountSupplierContext context) { /** Retrieves AWS security credentials using the AWS SDK's default provider chain. */ @Override - public AwsSecurityCredentials getCredentials( - ExternalAccountSupplierContext context) { + public AwsSecurityCredentials getCredentials(ExternalAccountSupplierContext context) { software.amazon.awssdk.auth.credentials.AwsCredentials credentials = this.awsCredentialsProvider.resolveCredentials(); if (credentials == null diff --git a/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java b/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java index 3c2d33d83..e8ed97fb5 100644 --- a/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java +++ b/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java @@ -40,12 +40,38 @@ public class CustomCredentialSupplierOktaWorkload { public static void main(String[] args) { // TODO(Developer): Replace these variables with your actual values. + + // 1. GCP_WORKLOAD_AUDIENCE: + // The audience for the workload identity federation. This is the full resource name of the + // Workload Identity Pool Provider, in the following format: + // //iam.googleapis.com/projects//locations/global/workloadIdentityPools//providers/ String gcpWorkloadAudience = System.getenv("GCP_WORKLOAD_AUDIENCE"); - String serviceAccountImpersonationUrl = - System.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"); + + // 2. GCP_SERVICE_ACCOUNT_IMPERSONATION_URL: + // The service account impersonation URL. This is the URL for impersonating a service account, + // in the following format: + // https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/:generateAccessToken + String serviceAccountImpersonationUrl = System.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"); + + // 3. GCS_BUCKET_NAME: + // The name of the bucket that you wish to fetch data for. String gcsBucketName = System.getenv("GCS_BUCKET_NAME"); + + // 4. Okta Configuration: + // To set up the Okta application for this flow: + // a. In your Okta developer console, create a new Application of type "Machine-to-Machine + // (M2M)". + // b. Under the "General" tab, ensure that "Client Credentials" is an allowed grant type. + // c. Note the "Client ID" and "Client Secret" for your application. + // d. Navigate to "Security" > "API" and select your authorization server (e.g., "default"). + // e. Under the "Scopes" tab, add a custom scope (e.g., "gcp.test.read"). + // f. Ensure your M2M application is granted this scope. + // + // OKTA_DOMAIN: Your Okta organization URL (e.g., https://{yourOktaDomain}.okta.com) String oktaDomain = System.getenv("OKTA_DOMAIN"); + // OKTA_CLIENT_ID: The Client ID of your Okta M2M application. String oktaClientId = System.getenv("OKTA_CLIENT_ID"); + // OKTA_CLIENT_SECRET: The Client Secret of your Okta M2M application. String oktaClientSecret = System.getenv("OKTA_CLIENT_SECRET"); if (gcpWorkloadAudience == null @@ -127,7 +153,8 @@ public OktaClientCredentialsSupplier(String domain, String clientId, String clie @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 && System.currentTimeMillis() < this.expiryTime - 60 * 1000; + boolean isTokenValid = + this.accessToken != null && System.currentTimeMillis() < this.expiryTime - 60 * 1000; if (isTokenValid) { System.out.println("[Supplier] Returning cached Okta Access token."); @@ -143,18 +170,6 @@ public String getSubjectToken(ExternalAccountSupplierContext context) throws IOE /** * Performs the Client Credentials grant flow by making a POST request to Okta's token endpoint. - * - *

To set up the Okta application for this flow: - * - *

    - *
  1. In your Okta developer console, create a new Application of type "Machine-to-Machine - * (M2M)". - *
  2. Under the "General" tab, ensure that "Client Credentials" is an allowed grant type. - *
  3. Note the "Client ID" and "Client Secret" for your application. - *
  4. Navigate to "Security" > "API" and select your authorization server (e.g., "default"). - *
  5. Under the "Scopes" tab, add a custom scope (e.g., "gcp.test.read"). - *
  6. Ensure your M2M application is granted this scope. - *
*/ private void fetchOktaAccessToken() throws IOException { URL url = new URL(this.oktaTokenUrl); @@ -165,7 +180,8 @@ private void fetchOktaAccessToken() throws IOException { // The client_id and client_secret are sent in a Basic Auth header, as required by the // OAuth 2.0 Client Credentials grant specification. The credentials are Base64 encoded. String auth = this.clientId + ":" + this.clientSecret; - String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); + String encodedAuth = + Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); conn.setRequestProperty("Authorization", "Basic " + encodedAuth); conn.setDoOutput(true); From 78cc9279ce405a0d210266d2ad4f68e53ac9be3f Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Wed, 22 Oct 2025 17:39:50 -0700 Subject: [PATCH 7/8] Added documentation for env variables. --- .../CustomCredentialSupplierAwsWorkload.java | 22 ++++++++++-------- .../CustomCredentialSupplierOktaWorkload.java | 23 +++++++++++-------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java b/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java index ab5d474d5..e99fcefd9 100644 --- a/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java +++ b/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java @@ -48,20 +48,20 @@ public static void main(String[] args) { // //iam.googleapis.com/projects//locations/global/workloadIdentityPools//providers/ String gcpWorkloadAudience = System.getenv("GCP_WORKLOAD_AUDIENCE"); - // 3. GCP_SERVICE_ACCOUNT_IMPERSONATION_URL: - // The service account impersonation URL. This is the URL for impersonating a service account, - // in the following format: + // 3. GCP_SERVICE_ACCOUNT_IMPERSONATION_URL (optional): + // The service account impersonation URL. It should follow the format: // https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/:generateAccessToken + // If not provided, you should grant access to the GCP bucket to the principal directly. String saImpersonationUrl = System.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"); // 4. GCS_BUCKET_NAME: // The name of the bucket that you wish to fetch data for. String gcsBucketName = System.getenv("GCS_BUCKET_NAME"); - if (gcpWorkloadAudience == null || saImpersonationUrl == null || gcsBucketName == null) { + if (gcpWorkloadAudience == null || gcsBucketName == null) { System.out.println( "Missing required environment variables. Please check your environment settings. " - + "Required: GCP_WORKLOAD_AUDIENCE, GCP_SERVICE_ACCOUNT_IMPERSONATION_URL, GCS_BUCKET_NAME"); + + "Required: GCP_WORKLOAD_AUDIENCE, GCS_BUCKET_NAME"); return; } @@ -74,15 +74,19 @@ public static void customCredentialSupplierAwsWorkload( CustomAwsSupplier customSupplier = new CustomAwsSupplier(); // 2. Configure the AwsCredentials options. - GoogleCredentials credentials = + 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") - .setServiceAccountImpersonationUrl(saImpersonationUrl) - .setAwsSecurityCredentialsSupplier(customSupplier) - .build(); + .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(); diff --git a/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java b/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java index e8ed97fb5..64a0a7f92 100644 --- a/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java +++ b/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java @@ -47,10 +47,10 @@ public static void main(String[] args) { // //iam.googleapis.com/projects//locations/global/workloadIdentityPools//providers/ String gcpWorkloadAudience = System.getenv("GCP_WORKLOAD_AUDIENCE"); - // 2. GCP_SERVICE_ACCOUNT_IMPERSONATION_URL: - // The service account impersonation URL. This is the URL for impersonating a service account, - // in the following format: + // 2. GCP_SERVICE_ACCOUNT_IMPERSONATION_URL (optional): + // The service account impersonation URL. In the following format: // https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/:generateAccessToken + // If not provided, you should grant access to the GCP bucket to the principal directly. String serviceAccountImpersonationUrl = System.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"); // 3. GCS_BUCKET_NAME: @@ -75,14 +75,13 @@ public static void main(String[] args) { String oktaClientSecret = System.getenv("OKTA_CLIENT_SECRET"); if (gcpWorkloadAudience == null - || serviceAccountImpersonationUrl == null || gcsBucketName == null || oktaDomain == null || oktaClientId == null || oktaClientSecret == null) { System.out.println( "Missing required environment variables. Please check your environment settings. " - + "Required: GCP_WORKLOAD_AUDIENCE, GCP_SERVICE_ACCOUNT_IMPERSONATION_URL, " + + "Required: GCP_WORKLOAD_AUDIENCE, " + "GCS_BUCKET_NAME, OKTA_DOMAIN, OKTA_CLIENT_ID, OKTA_CLIENT_SECRET"); return; } @@ -108,14 +107,20 @@ public static void customCredentialSupplierOktaWorkload( new OktaClientCredentialsSupplier(oktaDomain, oktaClientId, oktaClientSecret); // 2. Instantiate an IdentityPoolCredentials with the required configuration. - GoogleCredentials credentials = + 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) - .setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl) - .build(); + .setSubjectTokenSupplier(oktaSupplier); + + if (serviceAccountImpersonationUrl != null) { + credentialsBuilder.setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl); + } + + GoogleCredentials credentials = credentialsBuilder.build(); // 3. Use the credentials to make an authenticated request. Storage storage = StorageOptions.newBuilder().setCredentials(credentials).build().getService(); From 8bbaacdc180f8693ce9909d1054a67831883b035 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Thu, 23 Oct 2025 18:06:17 -0700 Subject: [PATCH 8/8] BOM manages AWS Sdk dependencies. Multiple scopes demonstrated for Okta. Doc update. --- samples/snippets/pom.xml | 27 +--- .../CustomCredentialSupplierAwsWorkload.java | 2 +- .../CustomCredentialSupplierOktaWorkload.java | 124 ++++++++++-------- 3 files changed, 78 insertions(+), 75 deletions(-) diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index a96e40667..5b7217972 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -34,6 +34,13 @@ pom import
+ + software.amazon.awssdk + bom + 2.20.27 + pom + import + @@ -62,34 +69,14 @@ com.google.cloud google-cloud-storage - - software.amazon.awssdk auth - 2.20.27 software.amazon.awssdk regions - 2.20.27 - - - - - - junit - junit - 4.13.2 - test - - truth - com.google.truth - test - 1.4.4 - - diff --git a/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java b/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java index e99fcefd9..5da2a2628 100644 --- a/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java +++ b/samples/snippets/src/main/java/CustomCredentialSupplierAwsWorkload.java @@ -45,7 +45,7 @@ public static void main(String[] args) { // 2. GCP_WORKLOAD_AUDIENCE: // The audience for the workload identity federation. This is the full resource name of the // Workload Identity Pool Provider, in the following format: - // //iam.googleapis.com/projects//locations/global/workloadIdentityPools//providers/ + // `//iam.googleapis.com/projects//locations/global/workloadIdentityPools//providers/` String gcpWorkloadAudience = System.getenv("GCP_WORKLOAD_AUDIENCE"); // 3. GCP_SERVICE_ACCOUNT_IMPERSONATION_URL (optional): diff --git a/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java b/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java index 64a0a7f92..4c53f2546 100644 --- a/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java +++ b/samples/snippets/src/main/java/CustomCredentialSupplierOktaWorkload.java @@ -23,6 +23,7 @@ import com.google.cloud.storage.Bucket; import com.google.cloud.storage.Storage; import com.google.cloud.storage.StorageOptions; +import java.time.Instant; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; @@ -44,7 +45,7 @@ public static void main(String[] args) { // 1. GCP_WORKLOAD_AUDIENCE: // The audience for the workload identity federation. This is the full resource name of the // Workload Identity Pool Provider, in the following format: - // //iam.googleapis.com/projects//locations/global/workloadIdentityPools//providers/ + // `//iam.googleapis.com/projects//locations/global/workloadIdentityPools//providers/` String gcpWorkloadAudience = System.getenv("GCP_WORKLOAD_AUDIENCE"); // 2. GCP_SERVICE_ACCOUNT_IMPERSONATION_URL (optional): @@ -58,7 +59,11 @@ public static void main(String[] args) { String gcsBucketName = System.getenv("GCS_BUCKET_NAME"); // 4. Okta Configuration: - // To set up the Okta application for this flow: + // To set up the Okta application for this flow, refer: + // https://developer.okta.com/docs/guides/implement-grant-type/clientcreds/main/ + // https://developer.okta.com/docs/guides/customize-authz-server/main/ + // + // Steps: // a. In your Okta developer console, create a new Application of type "Machine-to-Machine // (M2M)". // b. Under the "General" tab, ensure that "Client Credentials" is an allowed grant type. @@ -138,11 +143,13 @@ public static void customCredentialSupplierOktaWorkload( */ 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 long expiryTime; + private Instant expiryTime; public OktaClientCredentialsSupplier(String domain, String clientId, String clientSecret) { this.oktaTokenUrl = domain + "/oauth2/default/v1/token"; @@ -159,7 +166,8 @@ public OktaClientCredentialsSupplier(String domain, String clientId, String clie 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 && System.currentTimeMillis() < this.expiryTime - 60 * 1000; + this.accessToken != null + && Instant.now().isBefore(this.expiryTime.minusSeconds(TOKEN_REFRESH_BUFFER_SECONDS)); if (isTokenValid) { System.out.println("[Supplier] Returning cached Okta Access token."); @@ -178,59 +186,67 @@ public String getSubjectToken(ExternalAccountSupplierContext context) throws IOE */ 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"); - - // The client_id and client_secret are sent in a Basic Auth header, as required by the - // OAuth 2.0 Client Credentials grant specification. The credentials are Base64 encoded. - 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())) { - // For the Client Credentials grant, scopes are optional and define the permissions - // the access token will have. Replace "gcp.test.read" with the scopes defined in your - // Okta authorization server. Multiple scopes can be requested by space-separating them - // (e.g., "scope1 scope2"). - 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()))) { - StringBuilder response = new StringBuilder(); - String line; - while ((line = in.readLine()) != null) { - response.append(line); - } + HttpURLConnection conn = null; + try { + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + + // The client_id and client_secret are sent in a Basic Auth header, as required by the + // OAuth 2.0 Client Credentials grant specification. The credentials are Base64 encoded. + 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())) { + // For the Client Credentials grant, scopes are optional and define the permissions + // the access token will have. Replace "gcp.test.read" with the scopes defined in your + // Okta authorization server. Multiple scopes can be requested by space-separating them. + // In application/x-www-form-urlencoded, a space is represented by '+' or '%20'. + // e.g., "scope1%20scope2" or "scope1+scope2". + String params = "grant_type=client_credentials&scope=gcp.test.read%20gcp.bucket.read"; + out.writeBytes(params); + out.flush(); + } - GenericJson jsonObject = - GsonFactory.getDefaultInstance() - .createJsonParser(response.toString()) - .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"); - int expiresIn = expiresInNumber.intValue(); - this.expiryTime = System.currentTimeMillis() + expiresIn * 1000L; - System.out.println( - "[Supplier] Successfully received Access Token from Okta. Expires in " - + expiresIn - + " seconds."); - } else { - throw new IOException("Access token or expires_in not found in Okta response."); + int responseCode = conn.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + try (BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { + StringBuilder response = new StringBuilder(); + String line; + while ((line = in.readLine()) != null) { + response.append(line); + } + + GenericJson jsonObject = + GsonFactory.getDefaultInstance() + .createJsonParser(response.toString()) + .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"); + int expiresIn = expiresInNumber.intValue(); + this.expiryTime = Instant.now().plusSeconds(expiresIn); + System.out.println( + "[Supplier] Successfully received Access Token from Okta. Expires in " + + expiresIn + + " seconds."); + } else { + throw new IOException("Access token or expires_in not found in Okta response."); + } } + } else { + throw new IOException( + "Failed to authenticate with Okta using Client Credentials grant. Response code: " + + responseCode); + } + } finally { + if (conn != null) { + conn.disconnect(); } - } else { - throw new IOException( - "Failed to authenticate with Okta using Client Credentials grant. Response code: " - + responseCode); } } }