Skip to content

Commit 49d17f5

Browse files
committed
Included changes for Documenting Custom Credential Suppliers for AWS Workloads and Okta Workload.
1 parent 98b62ba commit 49d17f5

File tree

3 files changed

+358
-0
lines changed

3 files changed

+358
-0
lines changed

samples/snippets/pom.xml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,25 @@
6363
<artifactId>google-cloud-storage</artifactId>
6464
</dependency>
6565

66+
<!-- AWS SDK for Java V2 -->
67+
<dependency>
68+
<groupId>software.amazon.awssdk</groupId>
69+
<artifactId>auth</artifactId>
70+
<version>2.20.27</version>
71+
</dependency>
72+
<dependency>
73+
<groupId>software.amazon.awssdk</groupId>
74+
<artifactId>regions</artifactId>
75+
<version>2.20.27</version>
76+
</dependency>
77+
78+
<!-- Gson -->
79+
<dependency>
80+
<groupId>com.google.code.gson</groupId>
81+
<artifactId>gson</artifactId>
82+
<version>2.10.1</version>
83+
</dependency>
84+
6685
<!-- Test dependencies-->
6786
<dependency>
6887
<groupId>junit</groupId>
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import com.google.auth.oauth2.AwsCredentials;
18+
import com.google.auth.oauth2.AwsSecurityCredentialsSupplier;
19+
import com.google.auth.oauth2.ExternalAccountSupplierContext;
20+
import com.google.auth.oauth2.GoogleCredentials;
21+
import com.google.cloud.storage.Bucket;
22+
import com.google.cloud.storage.Storage;
23+
import com.google.cloud.storage.StorageOptions;
24+
import java.io.IOException;
25+
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
26+
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
27+
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
28+
import software.amazon.awssdk.regions.Region;
29+
import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain;
30+
31+
/**
32+
* This sample demonstrates how to use a custom AWS security credentials supplier to authenticate
33+
* with Google Cloud.
34+
*/
35+
public class CustomCredentialSupplierAwsWorkload {
36+
37+
public static void main(String[] args) throws IOException {
38+
// TODO(Developer): Replace these variables with your actual values.
39+
String gcpWorkloadAudience = System.getenv("GCP_WORKLOAD_AUDIENCE");
40+
String saImpersonationUrl = System.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL");
41+
String gcsBucketName = System.getenv("GCS_BUCKET_NAME");
42+
43+
if (gcpWorkloadAudience == null
44+
|| saImpersonationUrl == null
45+
|| gcsBucketName == null) {
46+
System.out.println(
47+
"Missing required environment variables. Please check your environment settings. "
48+
+ "Required: GCP_WORKLOAD_AUDIENCE, GCP_SERVICE_ACCOUNT_IMPERSONATION_URL, GCS_BUCKET_NAME");
49+
return;
50+
}
51+
52+
customCredentialSupplierAwsWorkload(gcpWorkloadAudience, saImpersonationUrl, gcsBucketName);
53+
}
54+
55+
public static void customCredentialSupplierAwsWorkload(
56+
String gcpWorkloadAudience, String saImpersonationUrl, String gcsBucketName)
57+
throws IOException {
58+
// 1. Instantiate the custom supplier.
59+
CustomAwsSupplier customSupplier = new CustomAwsSupplier();
60+
61+
// 2. Configure the AwsCredentials options.
62+
GoogleCredentials credentials =
63+
AwsCredentials.newBuilder()
64+
.setAudience(gcpWorkloadAudience)
65+
.setSubjectTokenType("urn:ietf:params:aws:token-type:aws4_request")
66+
.setServiceAccountImpersonationUrl(saImpersonationUrl)
67+
.setAwsSecurityCredentialsSupplier(customSupplier)
68+
.build();
69+
70+
// 3. Use the credentials to make an authenticated request.
71+
Storage storage = StorageOptions.newBuilder().setCredentials(credentials).build().getService();
72+
73+
System.out.println("[Test] Getting metadata for bucket: " + gcsBucketName + "...");
74+
Bucket bucket = storage.get(gcsBucketName);
75+
System.out.println(" --- SUCCESS! ---");
76+
System.out.println("Successfully authenticated and retrieved bucket data:");
77+
System.out.println(bucket.toString());
78+
}
79+
80+
/**
81+
* Custom AWS Security Credentials Supplier.
82+
*
83+
* <p>This implementation resolves AWS credentials using the default provider chain from the AWS
84+
* SDK. This allows fetching credentials from environment variables, shared credential files
85+
* (~/.aws/credentials), or IAM roles for service accounts (IRSA) in EKS, etc.
86+
*/
87+
private static class CustomAwsSupplier implements AwsSecurityCredentialsSupplier {
88+
89+
private final AwsCredentialsProvider awsCredentialsProvider;
90+
private String region;
91+
92+
public CustomAwsSupplier() {
93+
// The AWS SDK handles memoization (caching) and proactive refreshing internally.
94+
this.awsCredentialsProvider = DefaultCredentialsProvider.create();
95+
}
96+
97+
/**
98+
* Returns the AWS region. This is required for signing the AWS request. It resolves the region
99+
* automatically by using the default AWS region provider chain, which searches for the region
100+
* in the standard locations (environment variables, AWS config file, etc.).
101+
*/
102+
@Override
103+
public String getRegion(ExternalAccountSupplierContext context) {
104+
if (this.region == null) {
105+
Region awsRegion = new DefaultAwsRegionProviderChain().getRegion();
106+
if (awsRegion != null) {
107+
this.region = awsRegion.id();
108+
}
109+
}
110+
if (this.region == null) {
111+
throw new IllegalStateException(
112+
"CustomAwsSupplier: Unable to resolve AWS region. Please set the AWS_REGION "
113+
+ "environment variable or configure it in your ~/.aws/config file.");
114+
}
115+
return this.region;
116+
}
117+
118+
/** Retrieves AWS security credentials using the AWS SDK's default provider chain. */
119+
@Override
120+
public com.google.auth.oauth2.AwsSecurityCredentials getCredentials(
121+
ExternalAccountSupplierContext context) {
122+
software.amazon.awssdk.auth.credentials.AwsCredentials credentials =
123+
this.awsCredentialsProvider.resolveCredentials();
124+
if (credentials == null
125+
|| credentials.accessKeyId() == null
126+
|| credentials.secretAccessKey() == null) {
127+
throw new IllegalStateException(
128+
"Unable to resolve AWS credentials from the default provider chain. "
129+
+ "Ensure your AWS CLI is configured, or AWS environment variables "
130+
+ "(like AWS_ACCESS_KEY_ID) are set.");
131+
}
132+
133+
String sessionToken = null;
134+
if (credentials instanceof AwsSessionCredentials) {
135+
sessionToken = ((AwsSessionCredentials) credentials).sessionToken();
136+
}
137+
138+
return new com.google.auth.oauth2.AwsSecurityCredentials(
139+
credentials.accessKeyId(), credentials.secretAccessKey(), sessionToken);
140+
}
141+
}
142+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import com.google.auth.oauth2.ExternalAccountSupplierContext;
18+
import com.google.auth.oauth2.GoogleCredentials;
19+
import com.google.auth.oauth2.IdentityPoolCredentials;
20+
import com.google.auth.oauth2.IdentityPoolSubjectTokenSupplier;
21+
import com.google.cloud.storage.Bucket;
22+
import com.google.cloud.storage.Storage;
23+
import com.google.cloud.storage.StorageOptions;
24+
import com.google.gson.Gson;
25+
import com.google.gson.JsonObject;
26+
import java.io.BufferedReader;
27+
import java.io.DataOutputStream;
28+
import java.io.IOException;
29+
import java.io.InputStreamReader;
30+
import java.net.HttpURLConnection;
31+
import java.net.URL;
32+
import java.nio.charset.StandardCharsets;
33+
import java.util.Base64;
34+
35+
/**
36+
* This sample demonstrates how to use a custom subject token supplier to authenticate with Google
37+
* Cloud, using Okta as the identity provider.
38+
*/
39+
public class CustomCredentialSupplierOktaWorkload {
40+
41+
public static void main(String[] args) throws IOException {
42+
// TODO(Developer): Replace these variables with your actual values.
43+
String gcpWorkloadAudience = System.getenv("GCP_WORKLOAD_AUDIENCE");
44+
String serviceAccountImpersonationUrl =
45+
System.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL");
46+
String gcsBucketName = System.getenv("GCS_BUCKET_NAME");
47+
String oktaDomain = System.getenv("OKTA_DOMAIN");
48+
String oktaClientId = System.getenv("OKTA_CLIENT_ID");
49+
String oktaClientSecret = System.getenv("OKTA_CLIENT_SECRET");
50+
51+
if (gcpWorkloadAudience == null
52+
|| serviceAccountImpersonationUrl == null
53+
|| gcsBucketName == null
54+
|| oktaDomain == null
55+
|| oktaClientId == null
56+
|| oktaClientSecret == null) {
57+
System.out.println(
58+
"Missing required environment variables. Please check your environment settings. "
59+
+ "Required: GCP_WORKLOAD_AUDIENCE, GCP_SERVICE_ACCOUNT_IMPERSONATION_URL, "
60+
+ "GCS_BUCKET_NAME, OKTA_DOMAIN, OKTA_CLIENT_ID, OKTA_CLIENT_SECRET");
61+
return;
62+
}
63+
64+
customCredentialSupplierOktaWorkload(
65+
gcpWorkloadAudience,
66+
serviceAccountImpersonationUrl,
67+
gcsBucketName,
68+
oktaDomain,
69+
oktaClientId,
70+
oktaClientSecret);
71+
}
72+
73+
public static void customCredentialSupplierOktaWorkload(
74+
String gcpWorkloadAudience,
75+
String serviceAccountImpersonationUrl,
76+
String gcsBucketName,
77+
String oktaDomain,
78+
String oktaClientId,
79+
String oktaClientSecret)
80+
throws IOException {
81+
// 1. Instantiate our custom supplier with Okta credentials.
82+
OktaClientCredentialsSupplier oktaSupplier =
83+
new OktaClientCredentialsSupplier(oktaDomain, oktaClientId, oktaClientSecret);
84+
85+
// 2. Instantiate an IdentityPoolCredentials with the required configuration.
86+
GoogleCredentials credentials =
87+
IdentityPoolCredentials.newBuilder()
88+
.setAudience(gcpWorkloadAudience)
89+
.setSubjectTokenType("urn:ietf:params:oauth:token-type:jwt")
90+
.setTokenUrl("https://sts.googleapis.com/v1/token")
91+
.setSubjectTokenSupplier(oktaSupplier)
92+
.setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl)
93+
.build();
94+
95+
// 3. Use the credentials to make an authenticated request.
96+
Storage storage = StorageOptions.newBuilder().setCredentials(credentials).build().getService();
97+
98+
System.out.println("[Test] Getting metadata for bucket: " + gcsBucketName + "...");
99+
Bucket bucket = storage.get(gcsBucketName);
100+
System.out.println(" --- SUCCESS! ---");
101+
System.out.println("Successfully authenticated and retrieved bucket data:");
102+
System.out.println(bucket.toString());
103+
}
104+
105+
/**
106+
* A custom SubjectTokenSupplier that authenticates with Okta using the Client Credentials grant
107+
* flow.
108+
*/
109+
private static class OktaClientCredentialsSupplier implements IdentityPoolSubjectTokenSupplier {
110+
111+
private final String oktaTokenUrl;
112+
private final String clientId;
113+
private final String clientSecret;
114+
private String accessToken;
115+
private long expiryTime;
116+
117+
public OktaClientCredentialsSupplier(String domain, String clientId, String clientSecret) {
118+
this.oktaTokenUrl = domain + "/oauth2/default/v1/token";
119+
this.clientId = clientId;
120+
this.clientSecret = clientSecret;
121+
System.out.println("OktaClientCredentialsSupplier initialized.");
122+
}
123+
124+
/**
125+
* Main method called by the auth library. It will fetch a new token if one is not already
126+
* cached.
127+
*/
128+
@Override
129+
public String getSubjectToken(ExternalAccountSupplierContext context) throws IOException {
130+
// Check if the current token is still valid (with a 60-second buffer).
131+
boolean isTokenValid = this.accessToken != null && System.currentTimeMillis() < this.expiryTime - 60 * 1000;
132+
133+
if (isTokenValid) {
134+
System.out.println("[Supplier] Returning cached Okta Access token.");
135+
return this.accessToken;
136+
}
137+
138+
System.out.println(
139+
"[Supplier] Token is missing or expired. Fetching new Okta Access token via Client "
140+
+ "Credentials grant...");
141+
fetchOktaAccessToken();
142+
return this.accessToken;
143+
}
144+
145+
/**
146+
* Performs the Client Credentials grant flow by making a POST request to Okta's token
147+
* endpoint.
148+
*/
149+
private void fetchOktaAccessToken() throws IOException {
150+
URL url = new URL(this.oktaTokenUrl);
151+
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
152+
conn.setRequestMethod("POST");
153+
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
154+
155+
String auth = this.clientId + ":" + this.clientSecret;
156+
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
157+
conn.setRequestProperty("Authorization", "Basic " + encodedAuth);
158+
159+
conn.setDoOutput(true);
160+
try (DataOutputStream out = new DataOutputStream(conn.getOutputStream())) {
161+
String params = "grant_type=client_credentials&scope=gcp.test.read";
162+
out.writeBytes(params);
163+
out.flush();
164+
}
165+
166+
int responseCode = conn.getResponseCode();
167+
if (responseCode == HttpURLConnection.HTTP_OK) {
168+
try (BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
169+
StringBuilder response = new StringBuilder();
170+
String line;
171+
while ((line = in.readLine()) != null) {
172+
response.append(line);
173+
}
174+
175+
Gson gson = new Gson();
176+
JsonObject jsonObject = gson.fromJson(response.toString(), JsonObject.class);
177+
178+
if (jsonObject.has("access_token") && jsonObject.has("expires_in")) {
179+
this.accessToken = jsonObject.get("access_token").getAsString();
180+
int expiresIn = jsonObject.get("expires_in").getAsInt();
181+
this.expiryTime = System.currentTimeMillis() + expiresIn * 1000;
182+
System.out.println(
183+
"[Supplier] Successfully received Access Token from Okta. Expires in "
184+
+ expiresIn
185+
+ " seconds.");
186+
} else {
187+
throw new IOException("Access token or expires_in not found in Okta response.");
188+
}
189+
}
190+
} else {
191+
throw new IOException(
192+
"Failed to authenticate with Okta using Client Credentials grant. Response code: "
193+
+ responseCode);
194+
}
195+
}
196+
}
197+
}

0 commit comments

Comments
 (0)