Skip to content

Commit fe5e6f2

Browse files
committed
Added initial changes for customCredentialSuppliers in AWS and Okta with tests and readme
1 parent 62dbb27 commit fe5e6f2

File tree

5 files changed

+497
-1
lines changed

5 files changed

+497
-1
lines changed

auth/README.md

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,79 @@ information](https://developers.google.com/identity/protocols/application-defaul
6060

6161
$ npm run test:downscoping
6262

63+
## Custom Credential Suppliers
64+
65+
If you want to use external credentials (like AWS or Okta) that require custom retrieval logic not supported natively by the library, you can provide a custom supplier implementation.
66+
67+
### Custom AWS Credential Supplier
68+
69+
This sample demonstrates how to use the AWS SDK for Node.js as a custom `AwsSecurityCredentialsSupplier` to bridge AWS credentials—from sources like EKS IRSA, ECS, or local profiles—to Google Cloud Workload Identity.
70+
71+
#### 1. Set Environment Variables
72+
73+
```bash
74+
# AWS Credentials (or use ~/.aws/credentials)
75+
export AWS_ACCESS_KEY_ID="YOUR_AWS_ACCESS_KEY_ID"
76+
export AWS_SECRET_ACCESS_KEY="YOUR_AWS_SECRET_ACCESS_KEY"
77+
export AWS_REGION="us-east-1"
78+
79+
# Google Cloud Config
80+
# Format: //iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<POOL_ID>/providers/<PROVIDER_ID>
81+
export GCP_WORKLOAD_AUDIENCE="//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/my-pool/providers/my-aws-provider"
82+
export GCS_BUCKET_NAME="your-bucket-name"
83+
84+
# Optional: Service Account Impersonation
85+
# export GCP_SERVICE_ACCOUNT_IMPERSONATION_URL="https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/[email protected]:generateAccessToken"
86+
```
87+
88+
#### 2. Run the Sample
89+
90+
```bash
91+
node custom-credential-supplier-aws.js
92+
```
93+
94+
#### Running in Kubernetes (EKS)
95+
96+
To run this in an EKS cluster using IAM Roles for Service Accounts (IRSA):
97+
98+
1. **Configure IRSA:** Associate an AWS IAM Role with your Kubernetes Service Account.
99+
2. **Configure GCP:** Allow the AWS IAM Role ARN to impersonate your Workload Identity Pool.
100+
3. **Deploy:** When deploying your Node.js application, ensure the Pod uses the annotated Service Account. The AWS SDK in the sample will automatically detect the credentials injected by the EKS OIDC webhook.
101+
102+
---
103+
104+
### Custom Okta Credential Supplier
105+
106+
This sample demonstrates how to use a custom `SubjectTokenSupplier` to fetch an OIDC token from **Okta** using the Client Credentials flow and exchange it for Google Cloud credentials via Workload Identity Federation.
107+
108+
#### 1. Okta Configuration
109+
110+
Ensure you have an Okta Machine-to-Machine (M2M) application set up with "Client Credentials" grant type enabled. You will need the Domain, Client ID, and Client Secret.
111+
112+
#### 2. Set Environment Variables
113+
114+
```bash
115+
# Okta Configuration
116+
export OKTA_DOMAIN="https://your-okta-domain.okta.com"
117+
export OKTA_CLIENT_ID="your-okta-client-id"
118+
export OKTA_CLIENT_SECRET="your-okta-client-secret"
119+
120+
# Google Cloud Config
121+
export GCP_WORKLOAD_AUDIENCE="//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/my-pool/providers/my-oidc-provider"
122+
export GCS_BUCKET_NAME="your-bucket-name"
123+
124+
# Optional: Service Account Impersonation
125+
# export GCP_SERVICE_ACCOUNT_IMPERSONATION_URL="https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/[email protected]:generateAccessToken"
126+
```
127+
128+
#### 3. Run the Sample
129+
130+
```bash
131+
node custom-credential-supplier-okta.js
132+
```
133+
63134
### Additional resources
64135

65136
For more information on downscoped credentials you can visit:
66137

67-
> https://github.com/googleapis/google-auth-library-nodejs
138+
> https://github.com/googleapis/google-auth-library-nodejs
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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 {STSClient} = require('@aws-sdk/client-sts');
19+
20+
/**
21+
* Custom AWS Security Credentials Supplier.
22+
*
23+
* This implementation resolves AWS credentials using the default Node provider
24+
* chain from the AWS SDK. This allows fetching credentials from environment
25+
* variables, shared credential files (~/.aws/credentials), or IAM roles
26+
* for service accounts (IRSA) in EKS, etc.
27+
*/
28+
class CustomAwsSupplier {
29+
constructor() {
30+
// Will be cached upon first resolution.
31+
this.region = null;
32+
33+
// Initialize the AWS credential provider.
34+
// The AWS SDK handles memoization (caching) and proactive refreshing internally.
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+
// Call the initialized provider. It will return cached creds or refresh if needed.
66+
const awsCredentials = await this.awsCredentialsProvider();
67+
68+
if (!awsCredentials.accessKeyId || !awsCredentials.secretAccessKey) {
69+
throw new Error(
70+
'Unable to resolve AWS credentials from the node provider chain. ' +
71+
'Ensure your AWS CLI is configured, or AWS environment variables (like AWS_ACCESS_KEY_ID) are set.'
72+
);
73+
}
74+
75+
// Map the AWS SDK format to the google-auth-library format.
76+
return {
77+
accessKeyId: awsCredentials.accessKeyId,
78+
secretAccessKey: awsCredentials.secretAccessKey,
79+
token: awsCredentials.sessionToken,
80+
};
81+
}
82+
}
83+
84+
/**
85+
* Authenticates with Google Cloud using AWS credentials and retrieves bucket metadata.
86+
*
87+
* @param {string} bucketName The name of the bucket to retrieve.
88+
* @param {string} audience The Workload Identity Pool audience.
89+
* @param {string} [impersonationUrl] Optional Service Account impersonation URL.
90+
*/
91+
async function authenticateWithAwsCredentials(
92+
bucketName,
93+
audience,
94+
impersonationUrl
95+
) {
96+
// 1. Instantiate the custom supplier.
97+
const customSupplier = new CustomAwsSupplier();
98+
99+
// 2. Configure the AwsClient options.
100+
const clientOptions = {
101+
audience: audience,
102+
subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request',
103+
service_account_impersonation_url: impersonationUrl,
104+
aws_security_credentials_supplier: customSupplier,
105+
};
106+
107+
// 3. Create the auth client
108+
const client = new AwsClient(clientOptions);
109+
110+
// 4. Make an authenticated request to GCS.
111+
const bucketUrl = `https://storage.googleapis.com/storage/v1/b/${bucketName}`;
112+
const res = await client.request({url: bucketUrl});
113+
return res.data;
114+
}
115+
// [END auth_custom_credential_supplier_aws]
116+
117+
async function main() {
118+
require('dotenv').config();
119+
const gcpAudience = process.env.GCP_WORKLOAD_AUDIENCE;
120+
const saImpersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL;
121+
const gcsBucketName = process.env.GCS_BUCKET_NAME;
122+
123+
if (!gcpAudience || !gcsBucketName) {
124+
throw new Error(
125+
'Missing required environment variables: GCP_WORKLOAD_AUDIENCE, GCS_BUCKET_NAME'
126+
);
127+
}
128+
129+
try {
130+
console.log(`Retrieving metadata for bucket: ${gcsBucketName}...`);
131+
const bucketMetadata = await authenticateWithAwsCredentials(
132+
gcsBucketName,
133+
gcpAudience,
134+
saImpersonationUrl
135+
);
136+
console.log('\n--- SUCCESS! ---');
137+
console.log('Bucket Name:', bucketMetadata.name);
138+
console.log('Bucket Location:', bucketMetadata.location);
139+
} catch (error) {
140+
console.error('\n--- FAILED ---');
141+
console.error(error.response?.data || error);
142+
process.exitCode = 1;
143+
}
144+
}
145+
146+
if (require.main === module) {
147+
main();
148+
}
149+
150+
exports.authenticateWithAwsCredentials = authenticateWithAwsCredentials;

0 commit comments

Comments
 (0)