Skip to content

Commit b8c845d

Browse files
vvermaniennae
andauthored
feat: Documenting custom credential suppliers (#4194)
* Added initial changes for customCredentialSuppliers in AWS and Okta with tests and readme * Fixed tests and added some lint corrections. * Changed tests formatting. * Deleted old test files. * Custom Credentials now read from secrets.json as opposed to env variables. * package.json changes. * Added copyright headers. * fix: simplify configuration of secrets rather than a bunch of if statements also remove some extraneous comments Signed-off-by: Jennifer Davis <sigje@google.com> * Crisper load config from file. --------- Signed-off-by: Jennifer Davis <sigje@google.com> Co-authored-by: Jennifer Davis <sigje@google.com>
1 parent f6a44c5 commit b8c845d

14 files changed

+925
-3
lines changed

.eslintrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"node/no-unsupported-features/es-syntax": ["off"]
1313
},
1414
"parserOptions": {
15+
"ecmaVersion": 2020,
1516
"sourceType": "module"
1617
}
1718
}

auth/.eslintrc.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"extends": "../.eslintrc.json",
3+
"rules": {
4+
"no-unused-vars": "off"
5+
}
6+
}

auth/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,4 @@ information](https://developers.google.com/identity/protocols/application-defaul
6464

6565
For more information on downscoped credentials you can visit:
6666

67-
> https://github.com/googleapis/google-auth-library-nodejs
67+
> https://github.com/googleapis/google-auth-library-nodejs
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
FROM node:20-slim
2+
3+
WORKDIR /app
4+
5+
COPY package*.json ./
6+
7+
RUN npm install --omit=dev
8+
9+
RUN useradd -m appuser
10+
11+
COPY --chown=appuser:appuser . .
12+
13+
USER appuser
14+
15+
CMD [ "node", "customCredentialSupplierAws.js" ]
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Running the Custom AWS Credential Supplier Sample (Node.js)
2+
3+
This sample demonstrates how to use a custom AWS security credential supplier to authenticate with Google Cloud using AWS as an external identity provider. It uses the **AWS SDK for JavaScript (v3)** to fetch credentials from sources like Amazon Elastic Kubernetes Service (EKS) with IAM Roles for Service Accounts (IRSA), Elastic Container Service (ECS), or Fargate.
4+
5+
## Prerequisites
6+
7+
* An AWS account.
8+
* A Google Cloud project with the IAM API enabled.
9+
* A GCS bucket.
10+
* **Node.js 16** or later installed.
11+
* **npm** installed.
12+
13+
If you want to use AWS security credentials that cannot be retrieved using methods supported natively by the Google Auth library, a custom `AwsSecurityCredentialsSupplier` implementation may be specified. The supplier must return valid, unexpired AWS security credentials when called by the Google Cloud Auth library.
14+
15+
## Running Locally
16+
17+
For local development, you can provide credentials and configuration in a JSON file.
18+
19+
### Install Dependencies
20+
21+
Ensure you have Node.js installed, then install the required libraries:
22+
23+
```bash
24+
npm install
25+
```
26+
27+
### Configure Credentials for Local Development
28+
29+
1. Copy the example secrets file to a new file named `custom-credentials-aws-secrets.json` in the project root:
30+
```bash
31+
cp custom-credentials-aws-secrets.json.example custom-credentials-aws-secrets.json
32+
```
33+
2. Open `custom-credentials-aws-secrets.json` and fill in the required values for your AWS and Google Cloud configuration. Do not check your `custom-credentials-aws-secrets.json` file into version control.
34+
35+
36+
### Run the Application
37+
38+
Execute the script using node:
39+
40+
```bash
41+
node customCredentialSupplierAws.js
42+
```
43+
44+
When run locally, the application will detect the `custom-credentials-aws-secrets.json` file and use it to configure the necessary environment variables for the AWS SDK.
45+
46+
## Running in a Containerized Environment (EKS)
47+
48+
This section provides a brief overview of how to run the sample in an Amazon EKS cluster.
49+
50+
### EKS Cluster Setup
51+
52+
First, you need an EKS cluster. You can create one using `eksctl` or the AWS Management Console. For detailed instructions, refer to the [Amazon EKS documentation](https://docs.aws.amazon.com/eks/latest/userguide/create-cluster.html).
53+
54+
### Configure IAM Roles for Service Accounts (IRSA)
55+
56+
IRSA enables you to associate an IAM role with a Kubernetes service account. This provides a secure way for your pods to access AWS services without hardcoding long-lived credentials.
57+
58+
Run the following command to create the IAM role and bind it to a Kubernetes Service Account:
59+
60+
```bash
61+
eksctl create iamserviceaccount \
62+
--name your-k8s-service-account \
63+
--namespace default \
64+
--cluster your-cluster-name \
65+
--region your-aws-region \
66+
--role-name your-role-name \
67+
--attach-policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \
68+
--approve
69+
```
70+
71+
> **Note**: The `--attach-policy-arn` flag is used here to demonstrate attaching permissions. Update this with the specific AWS policy ARN your application requires.
72+
73+
For a deep dive into how this works without using `eksctl`, refer to the [IAM Roles for Service Accounts](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) documentation.
74+
75+
### Configure Google Cloud to Trust the AWS Role
76+
77+
To allow your AWS role to authenticate as a Google Cloud service account, you need to configure Workload Identity Federation. This process involves these key steps:
78+
79+
1. **Create a Workload Identity Pool and an AWS Provider:** The pool holds the configuration, and the provider is set up to trust your AWS account.
80+
81+
2. **Create or select a Google Cloud Service Account:** This service account will be impersonated by your AWS role.
82+
83+
3. **Bind the AWS Role to the Google Cloud Service Account:** Create an IAM policy binding that gives your AWS role the `Workload Identity User` (`roles/iam.workloadIdentityUser`) role on the Google Cloud service account.
84+
85+
For more detailed information, see the documentation on [Configuring Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds).
86+
87+
### Containerize and Package the Application
88+
89+
Create a `Dockerfile` for the Node.js application and push the image to a container registry (for example Amazon ECR) that your EKS cluster can access.
90+
91+
**Note:** The provided [`Dockerfile`](Dockerfile) is an example and may need modification for your specific needs.
92+
93+
Build and push the image:
94+
```bash
95+
docker build -t your-container-image:latest .
96+
docker push your-container-image:latest
97+
```
98+
99+
### Deploy to EKS
100+
101+
Create a Kubernetes deployment manifest to deploy your application to the EKS cluster. See the [`pod.yaml`](pod.yaml) file for an example.
102+
103+
**Note:** The provided [`pod.yaml`](pod.yaml) is an example and may need to be modified for your specific needs.
104+
105+
Deploy the pod:
106+
107+
```bash
108+
kubectl apply -f pod.yaml
109+
```
110+
111+
### Clean Up
112+
113+
To clean up the resources, delete the EKS cluster and any other AWS and Google Cloud resources you created.
114+
115+
```bash
116+
eksctl delete cluster --name your-cluster-name
117+
```
118+
119+
## Testing
120+
121+
This sample is not continuously tested. It is provided for instructional purposes and may require modifications to work in your environment.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"aws_access_key_id": "YOUR_AWS_ACCESS_KEY_ID",
3+
"aws_secret_access_key": "YOUR_AWS_SECRET_ACCESS_KEY",
4+
"aws_region": "YOUR_AWS_REGION",
5+
"gcp_workload_audience": "YOUR_GCP_WORKLOAD_AUDIENCE",
6+
"gcs_bucket_name": "YOUR_GCS_BUCKET_NAME",
7+
"gcp_service_account_impersonation_url": "YOUR_GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"
8+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// [START auth_custom_credential_supplier_aws]
16+
const {AwsClient} = require('google-auth-library');
17+
const {fromNodeProviderChain} = require('@aws-sdk/credential-providers');
18+
const fs = require('fs');
19+
const path = require('path');
20+
const {STSClient} = require('@aws-sdk/client-sts');
21+
const {Storage} = require('@google-cloud/storage');
22+
23+
/**
24+
* Custom AWS Security Credentials Supplier.
25+
*
26+
* This implementation resolves AWS credentials using the default Node provider
27+
* chain from the AWS SDK. This allows fetching credentials from environment
28+
* variables, shared credential files (~/.aws/credentials), or IAM roles
29+
* for service accounts (IRSA) in EKS, etc.
30+
*/
31+
class CustomAwsSupplier {
32+
constructor() {
33+
this.region = null;
34+
35+
this.awsCredentialsProvider = fromNodeProviderChain();
36+
}
37+
38+
/**
39+
* Returns the AWS region. This is required for signing the AWS request.
40+
* It resolves the region automatically by using the default AWS region
41+
* provider chain, which searches for the region in the standard locations
42+
* (environment variables, AWS config file, etc.).
43+
*/
44+
async getAwsRegion(_context) {
45+
if (this.region) {
46+
return this.region;
47+
}
48+
49+
const client = new STSClient({});
50+
this.region = await client.config.region();
51+
52+
if (!this.region) {
53+
throw new Error(
54+
'CustomAwsSupplier: Unable to resolve AWS region. Please set the AWS_REGION environment variable or configure it in your ~/.aws/config file.'
55+
);
56+
}
57+
58+
return this.region;
59+
}
60+
61+
/**
62+
* Retrieves AWS security credentials using the AWS SDK's default provider chain.
63+
*/
64+
async getAwsSecurityCredentials(_context) {
65+
const awsCredentials = await this.awsCredentialsProvider();
66+
67+
if (!awsCredentials.accessKeyId || !awsCredentials.secretAccessKey) {
68+
throw new Error(
69+
'Unable to resolve AWS credentials from the node provider chain. ' +
70+
'Ensure your AWS CLI is configured, or AWS environment variables (like AWS_ACCESS_KEY_ID) are set.'
71+
);
72+
}
73+
74+
return {
75+
accessKeyId: awsCredentials.accessKeyId,
76+
secretAccessKey: awsCredentials.secretAccessKey,
77+
token: awsCredentials.sessionToken,
78+
};
79+
}
80+
}
81+
82+
/**
83+
* Authenticates with Google Cloud using AWS credentials and retrieves bucket metadata.
84+
*
85+
* @param {string} bucketName The name of the bucket to retrieve.
86+
* @param {string} audience The Workload Identity Pool audience.
87+
* @param {string} [impersonationUrl] Optional Service Account impersonation URL.
88+
*/
89+
async function authenticateWithAwsCredentials(
90+
bucketName,
91+
audience,
92+
impersonationUrl
93+
) {
94+
const customSupplier = new CustomAwsSupplier();
95+
96+
const clientOptions = {
97+
audience: audience,
98+
subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request',
99+
service_account_impersonation_url: impersonationUrl,
100+
aws_security_credentials_supplier: customSupplier,
101+
};
102+
103+
const authClient = new AwsClient(clientOptions);
104+
105+
const storage = new Storage({
106+
authClient: authClient,
107+
});
108+
109+
const [metadata] = await storage.bucket(bucketName).getMetadata();
110+
return metadata;
111+
}
112+
// [END auth_custom_credential_supplier_aws]
113+
114+
/**
115+
* If a local secrets file is present, load it into the process environment.
116+
* This is a "just-in-time" configuration for local development. These
117+
* variables are only set for the current process.
118+
*/
119+
function loadConfigFromFile() {
120+
const secretsPath = path.resolve(
121+
__dirname,
122+
'custom-credentials-aws-secrets.json'
123+
);
124+
if (!fs.existsSync(secretsPath)) return;
125+
126+
try {
127+
const secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8'));
128+
129+
const envMap = {
130+
aws_access_key_id: 'AWS_ACCESS_KEY_ID',
131+
aws_secret_access_key: 'AWS_SECRET_ACCESS_KEY',
132+
aws_region: 'AWS_REGION',
133+
gcp_workload_audience: 'GCP_WORKLOAD_AUDIENCE',
134+
gcs_bucket_name: 'GCS_BUCKET_NAME',
135+
gcp_service_account_impersonation_url:
136+
'GCP_SERVICE_ACCOUNT_IMPERSONATION_URL',
137+
};
138+
139+
for (const [jsonKey, envKey] of Object.entries(envMap)) {
140+
if (secrets[jsonKey]) {
141+
process.env[envKey] = secrets[jsonKey];
142+
}
143+
}
144+
} catch (error) {
145+
console.error(`Error reading secrets file: ${error.message}`);
146+
}
147+
}
148+
149+
async function main() {
150+
loadConfigFromFile();
151+
152+
const gcpAudience = process.env.GCP_WORKLOAD_AUDIENCE;
153+
const saImpersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL;
154+
const gcsBucketName = process.env.GCS_BUCKET_NAME;
155+
156+
if (!gcpAudience || !gcsBucketName) {
157+
throw new Error(
158+
'Missing required configuration. Please provide it in a ' +
159+
'secrets.json file or as environment variables: ' +
160+
'GCP_WORKLOAD_AUDIENCE, GCS_BUCKET_NAME'
161+
);
162+
}
163+
164+
try {
165+
console.log(`Retrieving metadata for bucket: ${gcsBucketName}...`);
166+
const bucketMetadata = await authenticateWithAwsCredentials(
167+
gcsBucketName,
168+
gcpAudience,
169+
saImpersonationUrl
170+
);
171+
console.log('\n--- SUCCESS! ---');
172+
console.log('Bucket Metadata:', JSON.stringify(bucketMetadata, null, 2));
173+
} catch (error) {
174+
console.error('\n--- FAILED ---');
175+
console.error(error.message || error);
176+
process.exitCode = 1;
177+
}
178+
}
179+
180+
if (require.main === module) {
181+
main();
182+
}
183+
184+
exports.authenticateWithAwsCredentials = authenticateWithAwsCredentials;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright 2025 Google LLC
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
apiVersion: v1
15+
kind: Pod
16+
metadata:
17+
name: custom-credential-pod-node
18+
spec:
19+
# The Kubernetes Service Account that is annotated with the corresponding
20+
# AWS IAM role ARN. See the README for instructions on setting up IAM
21+
# Roles for Service Accounts (IRSA).
22+
serviceAccountName: your-k8s-service-account
23+
containers:
24+
- name: gcp-auth-sample-node
25+
# The container image pushed to the container registry
26+
# For example, Amazon Elastic Container Registry
27+
image: your-container-image:latest
28+
env:
29+
# REQUIRED: The AWS region. The AWS SDK for Node.js requires this
30+
# to be set explicitly in containers.
31+
- name: AWS_REGION
32+
value: "your-aws-region"
33+
34+
# REQUIRED: The full identifier of the Workload Identity Pool provider
35+
- name: GCP_WORKLOAD_AUDIENCE
36+
value: "your-gcp-workload-audience"
37+
38+
# OPTIONAL: Enable Google Cloud service account impersonation
39+
# - name: GCP_SERVICE_ACCOUNT_IMPERSONATION_URL
40+
# value: "your-gcp-service-account-impersonation-url"
41+
42+
# REQUIRED: The bucket to list
43+
- name: GCS_BUCKET_NAME
44+
value: "your-gcs-bucket-name"

0 commit comments

Comments
 (0)