Skip to content

Commit 3ec8dfb

Browse files
vvermaniennaegemini-code-assist[bot]
authored
feat: Custom Credential Supplier Documentation (#10203)
* Added some samples for custom credentials. Including tests. * Newlines at the end of files and formatting fixes. * Changed the custom credential scripts to use the secrets.json file to read properties. * Added newlines at the end of files that were missing it. * ReadMe changes for Okta script. other vanity changes. * fix: address issues with non-ASCII characters Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: Jennifer Davis <iennae@gmail.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent c8a3d3c commit 3ec8dfb

File tree

12 files changed

+1020
-1
lines changed

12 files changed

+1020
-1
lines changed

auth/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Ignore GCP and IdP secret files
2+
src/main/java/com/google/cloud/auth/samples/customcredentials/aws/custom-credentials-aws-secrets.json
3+
src/main/java/com/google/cloud/auth/samples/customcredentials/okta/custom-credentials-okta-secrets.json

auth/pom.xml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ limitations under the License.
3030
<parent>
3131
<groupId>com.google.cloud.samples</groupId>
3232
<artifactId>shared-configuration</artifactId>
33-
<version>1.2.0</version>
33+
<version>1.2.2</version>
3434
</parent>
3535

3636
<properties>
@@ -52,6 +52,13 @@ limitations under the License.
5252
<type>pom</type>
5353
<scope>import</scope>
5454
</dependency>
55+
<dependency>
56+
<groupId>software.amazon.awssdk</groupId>
57+
<artifactId>bom</artifactId>
58+
<version>2.25.41</version>
59+
<type>pom</type>
60+
<scope>import</scope>
61+
</dependency>
5562
</dependencies>
5663
</dependencyManagement>
5764

@@ -82,6 +89,14 @@ limitations under the License.
8289
<groupId>com.google.cloud</groupId>
8390
<artifactId>google-cloud-language</artifactId>
8491
</dependency>
92+
<dependency>
93+
<groupId>software.amazon.awssdk</groupId>
94+
<artifactId>auth</artifactId>
95+
</dependency>
96+
<dependency>
97+
<groupId>software.amazon.awssdk</groupId>
98+
<artifactId>regions</artifactId>
99+
</dependency>
85100
<!-- END dependencies -->
86101
<dependency>
87102
<groupId>junit</groupId>
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
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+
package com.google.cloud.auth.samples.customcredentials.aws;
18+
19+
// [START auth_custom_credential_supplier_aws]
20+
import com.google.auth.oauth2.AwsCredentials;
21+
import com.google.auth.oauth2.AwsSecurityCredentials;
22+
import com.google.auth.oauth2.AwsSecurityCredentialsSupplier;
23+
import com.google.auth.oauth2.ExternalAccountSupplierContext;
24+
import com.google.auth.oauth2.GoogleCredentials;
25+
import com.google.cloud.storage.Bucket;
26+
import com.google.cloud.storage.Storage;
27+
import com.google.cloud.storage.StorageOptions;
28+
import com.google.gson.Gson;
29+
import com.google.gson.reflect.TypeToken;
30+
import java.io.IOException;
31+
import java.io.Reader;
32+
import java.lang.reflect.Type;
33+
import java.nio.file.Files;
34+
import java.nio.file.Paths;
35+
import java.util.Map;
36+
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
37+
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
38+
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
39+
import software.amazon.awssdk.regions.Region;
40+
import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain;
41+
42+
// [END auth_custom_credential_supplier_aws]
43+
44+
/**
45+
* This sample demonstrates how to use a custom AWS security credentials supplier to authenticate to
46+
* Google Cloud Storage using AWS Workload Identity Federation.
47+
*/
48+
public class CustomCredentialSupplierAwsWorkload {
49+
50+
public static void main(String[] args) throws IOException {
51+
52+
// Reads the custom-credentials-aws-secrets.json if running locally.
53+
loadConfigFromFile();
54+
55+
// The audience for the workload identity federation.
56+
// Format: //iam.googleapis.com/projects/<project-number>/locations/global/
57+
// workloadIdentityPools/<pool-id>/providers/<provider-id>
58+
String gcpWorkloadAudience = getConfiguration("GCP_WORKLOAD_AUDIENCE");
59+
60+
// The bucket to fetch data from.
61+
String gcsBucketName = getConfiguration("GCS_BUCKET_NAME");
62+
63+
// (Optional) The service account impersonation URL.
64+
String saImpersonationUrl = getConfiguration("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL");
65+
66+
if (gcpWorkloadAudience == null || gcsBucketName == null) {
67+
System.err.println(
68+
"Required configuration missing. Please provide it in a "
69+
+ "custom-credentials-aws-secrets.json file or as environment variables: "
70+
+ "GCP_WORKLOAD_AUDIENCE, GCS_BUCKET_NAME");
71+
return;
72+
}
73+
74+
try {
75+
System.out.println("Retrieving metadata for bucket: " + gcsBucketName + "...");
76+
Bucket bucket =
77+
authenticateWithAwsCredentials(gcpWorkloadAudience, saImpersonationUrl, gcsBucketName);
78+
79+
System.out.println(" --- SUCCESS! ---");
80+
System.out.println("Bucket details:");
81+
System.out.printf(" Name: %s%n", bucket.getName());
82+
System.out.printf(" Location: %s%n", bucket.getLocation());
83+
System.out.printf(" Storage Class: %s%n", bucket.getStorageClass());
84+
System.out.printf(" Metageneration: %s%n", bucket.getMetageneration());
85+
} catch (Exception e) {
86+
System.err.println("Authentication or Request failed: " + e.getMessage());
87+
}
88+
}
89+
90+
/**
91+
* Helper method to retrieve configuration. It checks Environment variables first, then System
92+
* properties (populated by loadConfigFromFile).
93+
*/
94+
static String getConfiguration(String key) {
95+
String value = System.getenv(key);
96+
if (value == null) {
97+
value = System.getProperty(key);
98+
}
99+
return value;
100+
}
101+
102+
/**
103+
* If a local secrets file is present, load it into the System Properties. This is a
104+
* "just-in-time" configuration for local development. These variables are only set for the
105+
* current process.
106+
*/
107+
static void loadConfigFromFile() {
108+
// By default, this expects the file to be in the project root.
109+
String secretsFilePath = "custom-credentials-aws-secrets.json";
110+
if (!Files.exists(Paths.get(secretsFilePath))) {
111+
return;
112+
}
113+
114+
try (Reader reader = Files.newBufferedReader(Paths.get(secretsFilePath))) {
115+
// Use Gson to parse the JSON file into a Map
116+
Gson gson = new Gson();
117+
Type type = new TypeToken<Map<String, String>>() {}.getType();
118+
Map<String, String> secrets = gson.fromJson(reader, type);
119+
120+
if (secrets == null) {
121+
return;
122+
}
123+
124+
// AWS SDK for Java looks for System Properties with specific names (camelCase)
125+
// if environment variables are missing.
126+
if (secrets.containsKey("aws_access_key_id")) {
127+
System.setProperty("aws.accessKeyId", secrets.get("aws_access_key_id"));
128+
}
129+
if (secrets.containsKey("aws_secret_access_key")) {
130+
System.setProperty("aws.secretAccessKey", secrets.get("aws_secret_access_key"));
131+
}
132+
if (secrets.containsKey("aws_region")) {
133+
System.setProperty("aws.region", secrets.get("aws_region"));
134+
}
135+
136+
// Set custom GCP variables as System Properties so getConfiguration() can find them.
137+
if (secrets.containsKey("gcp_workload_audience")) {
138+
System.setProperty("GCP_WORKLOAD_AUDIENCE", secrets.get("gcp_workload_audience"));
139+
}
140+
if (secrets.containsKey("gcs_bucket_name")) {
141+
System.setProperty("GCS_BUCKET_NAME", secrets.get("gcs_bucket_name"));
142+
}
143+
if (secrets.containsKey("gcp_service_account_impersonation_url")) {
144+
System.setProperty(
145+
"GCP_SERVICE_ACCOUNT_IMPERSONATION_URL",
146+
secrets.get("gcp_service_account_impersonation_url"));
147+
}
148+
149+
} catch (IOException e) {
150+
System.err.println("Error reading secrets file: " + e.getMessage());
151+
}
152+
}
153+
154+
/**
155+
* Authenticates using a custom AWS credential supplier and retrieves bucket metadata.
156+
*
157+
* @param gcpWorkloadAudience The WIF provider audience.
158+
* @param saImpersonationUrl Optional service account impersonation URL.
159+
* @param gcsBucketName The GCS bucket name.
160+
* @return The Bucket object containing metadata.
161+
* @throws IOException If authentication fails.
162+
*/
163+
// [START auth_custom_credential_supplier_aws]
164+
public static Bucket authenticateWithAwsCredentials(
165+
String gcpWorkloadAudience, String saImpersonationUrl, String gcsBucketName)
166+
throws IOException {
167+
168+
CustomAwsSupplier customSupplier = new CustomAwsSupplier();
169+
170+
AwsCredentials.Builder credentialsBuilder =
171+
AwsCredentials.newBuilder()
172+
.setAudience(gcpWorkloadAudience)
173+
// This token type indicates that the subject token is an AWS Signature Version 4 signed
174+
// request. This is required for AWS Workload Identity Federation.
175+
.setSubjectTokenType("urn:ietf:params:aws:token-type:aws4_request")
176+
.setAwsSecurityCredentialsSupplier(customSupplier);
177+
178+
if (saImpersonationUrl != null) {
179+
credentialsBuilder.setServiceAccountImpersonationUrl(saImpersonationUrl);
180+
}
181+
182+
GoogleCredentials credentials = credentialsBuilder.build();
183+
184+
Storage storage = StorageOptions.newBuilder().setCredentials(credentials).build().getService();
185+
186+
return storage.get(gcsBucketName);
187+
}
188+
189+
/**
190+
* Custom AWS Security Credentials Supplier.
191+
*
192+
* <p>This implementation resolves AWS credentials and regions using the default provider chains
193+
* from the AWS SDK (v2). This supports environment variables, ~/.aws/credentials, and EC2/EKS
194+
* metadata.
195+
*/
196+
private static class CustomAwsSupplier implements AwsSecurityCredentialsSupplier {
197+
private final AwsCredentialsProvider awsCredentialsProvider;
198+
private String region;
199+
200+
public CustomAwsSupplier() {
201+
// The AWS SDK handles caching internally.
202+
this.awsCredentialsProvider = DefaultCredentialsProvider.create();
203+
}
204+
205+
@Override
206+
public String getRegion(ExternalAccountSupplierContext context) {
207+
if (this.region == null) {
208+
Region awsRegion = new DefaultAwsRegionProviderChain().getRegion();
209+
if (awsRegion == null) {
210+
throw new IllegalStateException(
211+
"Unable to resolve AWS region. Ensure AWS_REGION is set or configured.");
212+
}
213+
this.region = awsRegion.id();
214+
}
215+
return this.region;
216+
}
217+
218+
@Override
219+
public AwsSecurityCredentials getCredentials(ExternalAccountSupplierContext context) {
220+
software.amazon.awssdk.auth.credentials.AwsCredentials credentials =
221+
this.awsCredentialsProvider.resolveCredentials();
222+
223+
if (credentials == null) {
224+
throw new IllegalStateException("Unable to resolve AWS credentials.");
225+
}
226+
227+
String sessionToken = null;
228+
if (credentials instanceof AwsSessionCredentials) {
229+
sessionToken = ((AwsSessionCredentials) credentials).sessionToken();
230+
}
231+
232+
return new AwsSecurityCredentials(
233+
credentials.accessKeyId(), credentials.secretAccessKey(), sessionToken);
234+
}
235+
}
236+
// [END auth_custom_credential_supplier_aws]
237+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Build the application
2+
FROM maven:3.9-eclipse-temurin-17 AS builder
3+
4+
WORKDIR /app
5+
6+
# Copy only the build config first (for better layer caching)
7+
COPY pom.xml .
8+
COPY src ./src
9+
10+
# 'clean package': Compiles the code and creates the thin jar in /app/target
11+
# 'dependency:copy-dependencies': Copies all JARs to /app/target/libs
12+
# We explicitly set -DoutputDirectory so we know EXACTLY where they are.
13+
RUN mvn clean package dependency:copy-dependencies \
14+
-DoutputDirectory=target/libs \
15+
-DskipTests
16+
17+
# Run the application
18+
FROM eclipse-temurin:17-jre-focal
19+
20+
# Security: Create a non-root user
21+
RUN useradd -m appuser
22+
USER appuser
23+
WORKDIR /app
24+
25+
# Copy the Thin Jar
26+
COPY --from=builder --chown=appuser:appuser /app/target/auth-1.0.jar app.jar
27+
28+
# Copy the Dependencies (The libraries)
29+
COPY --from=builder --chown=appuser:appuser /app/target/libs lib/
30+
31+
# Run with Classpath
32+
# We add 'app.jar' and everything in 'lib/' to the classpath.
33+
CMD ["java", "-cp", "app.jar:lib/*", "com.google.cloud.auth.samples.customcredentials.aws.CustomCredentialSupplierAwsWorkload"]

0 commit comments

Comments
 (0)