From fe5e6f2f442ce39784e4a6f843586c496e0da19a Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Sun, 16 Nov 2025 17:53:58 -0800 Subject: [PATCH 1/9] Added initial changes for customCredentialSuppliers in AWS and Okta with tests and readme --- auth/README.md | 73 +++++++- auth/customCredentialSupplierAws.js | 150 +++++++++++++++ auth/customCredentialSupplierOkta.js | 176 ++++++++++++++++++ .../customCredentialSupplierAwsTest.js | 48 +++++ .../customCredentialSupplierOktaTest.js | 51 +++++ 5 files changed, 497 insertions(+), 1 deletion(-) create mode 100644 auth/customCredentialSupplierAws.js create mode 100644 auth/customCredentialSupplierOkta.js create mode 100644 auth/system-test/customCredentialSupplierAwsTest.js create mode 100644 auth/system-test/customCredentialSupplierOktaTest.js diff --git a/auth/README.md b/auth/README.md index 636015860f..a08eb49384 100644 --- a/auth/README.md +++ b/auth/README.md @@ -60,8 +60,79 @@ information](https://developers.google.com/identity/protocols/application-defaul $ npm run test:downscoping +## Custom Credential Suppliers + +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. + +### Custom AWS Credential Supplier + +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. + +#### 1. Set Environment Variables + +```bash +# AWS Credentials (or use ~/.aws/credentials) +export AWS_ACCESS_KEY_ID="YOUR_AWS_ACCESS_KEY_ID" +export AWS_SECRET_ACCESS_KEY="YOUR_AWS_SECRET_ACCESS_KEY" +export AWS_REGION="us-east-1" + +# Google Cloud Config +# Format: //iam.googleapis.com/projects//locations/global/workloadIdentityPools//providers/ +export GCP_WORKLOAD_AUDIENCE="//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/my-pool/providers/my-aws-provider" +export GCS_BUCKET_NAME="your-bucket-name" + +# Optional: Service Account Impersonation +# export GCP_SERVICE_ACCOUNT_IMPERSONATION_URL="https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/my-sa@my-project.iam.gserviceaccount.com:generateAccessToken" +``` + +#### 2. Run the Sample + +```bash +node custom-credential-supplier-aws.js +``` + +#### Running in Kubernetes (EKS) + +To run this in an EKS cluster using IAM Roles for Service Accounts (IRSA): + +1. **Configure IRSA:** Associate an AWS IAM Role with your Kubernetes Service Account. +2. **Configure GCP:** Allow the AWS IAM Role ARN to impersonate your Workload Identity Pool. +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. + +--- + +### Custom Okta Credential Supplier + +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. + +#### 1. Okta Configuration + +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. + +#### 2. Set Environment Variables + +```bash +# Okta Configuration +export OKTA_DOMAIN="https://your-okta-domain.okta.com" +export OKTA_CLIENT_ID="your-okta-client-id" +export OKTA_CLIENT_SECRET="your-okta-client-secret" + +# Google Cloud Config +export GCP_WORKLOAD_AUDIENCE="//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/my-pool/providers/my-oidc-provider" +export GCS_BUCKET_NAME="your-bucket-name" + +# Optional: Service Account Impersonation +# export GCP_SERVICE_ACCOUNT_IMPERSONATION_URL="https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/my-sa@my-project.iam.gserviceaccount.com:generateAccessToken" +``` + +#### 3. Run the Sample + +```bash +node custom-credential-supplier-okta.js +``` + ### Additional resources For more information on downscoped credentials you can visit: -> https://github.com/googleapis/google-auth-library-nodejs \ No newline at end of file +> https://github.com/googleapis/google-auth-library-nodejs diff --git a/auth/customCredentialSupplierAws.js b/auth/customCredentialSupplierAws.js new file mode 100644 index 0000000000..8662f16708 --- /dev/null +++ b/auth/customCredentialSupplierAws.js @@ -0,0 +1,150 @@ +// 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. + +// [START auth_custom_credential_supplier_aws] +const {AwsClient} = require('google-auth-library'); +const {fromNodeProviderChain} = require('@aws-sdk/credential-providers'); +const {STSClient} = require('@aws-sdk/client-sts'); + +/** + * Custom AWS Security Credentials Supplier. + * + * This implementation resolves AWS credentials using the default Node 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. + */ +class CustomAwsSupplier { + constructor() { + // Will be cached upon first resolution. + this.region = null; + + // Initialize the AWS credential provider. + // The AWS SDK handles memoization (caching) and proactive refreshing internally. + this.awsCredentialsProvider = fromNodeProviderChain(); + } + + /** + * 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.). + */ + async getAwsRegion(_context) { + if (this.region) { + return this.region; + } + + const client = new STSClient({}); + this.region = await client.config.region(); + + if (!this.region) { + throw new Error( + '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. + */ + async getAwsSecurityCredentials(_context) { + // Call the initialized provider. It will return cached creds or refresh if needed. + const awsCredentials = await this.awsCredentialsProvider(); + + if (!awsCredentials.accessKeyId || !awsCredentials.secretAccessKey) { + throw new Error( + 'Unable to resolve AWS credentials from the node provider chain. ' + + 'Ensure your AWS CLI is configured, or AWS environment variables (like AWS_ACCESS_KEY_ID) are set.' + ); + } + + // Map the AWS SDK format to the google-auth-library format. + return { + accessKeyId: awsCredentials.accessKeyId, + secretAccessKey: awsCredentials.secretAccessKey, + token: awsCredentials.sessionToken, + }; + } +} + +/** + * Authenticates with Google Cloud using AWS credentials and retrieves bucket metadata. + * + * @param {string} bucketName The name of the bucket to retrieve. + * @param {string} audience The Workload Identity Pool audience. + * @param {string} [impersonationUrl] Optional Service Account impersonation URL. + */ +async function authenticateWithAwsCredentials( + bucketName, + audience, + impersonationUrl +) { + // 1. Instantiate the custom supplier. + const customSupplier = new CustomAwsSupplier(); + + // 2. Configure the AwsClient options. + const clientOptions = { + audience: audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + service_account_impersonation_url: impersonationUrl, + aws_security_credentials_supplier: customSupplier, + }; + + // 3. Create the auth client + const client = new AwsClient(clientOptions); + + // 4. Make an authenticated request to GCS. + const bucketUrl = `https://storage.googleapis.com/storage/v1/b/${bucketName}`; + const res = await client.request({url: bucketUrl}); + return res.data; +} +// [END auth_custom_credential_supplier_aws] + +async function main() { + require('dotenv').config(); + const gcpAudience = process.env.GCP_WORKLOAD_AUDIENCE; + const saImpersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; + const gcsBucketName = process.env.GCS_BUCKET_NAME; + + if (!gcpAudience || !gcsBucketName) { + throw new Error( + 'Missing required environment variables: GCP_WORKLOAD_AUDIENCE, GCS_BUCKET_NAME' + ); + } + + try { + console.log(`Retrieving metadata for bucket: ${gcsBucketName}...`); + const bucketMetadata = await authenticateWithAwsCredentials( + gcsBucketName, + gcpAudience, + saImpersonationUrl + ); + console.log('\n--- SUCCESS! ---'); + console.log('Bucket Name:', bucketMetadata.name); + console.log('Bucket Location:', bucketMetadata.location); + } catch (error) { + console.error('\n--- FAILED ---'); + console.error(error.response?.data || error); + process.exitCode = 1; + } +} + +if (require.main === module) { + main(); +} + +exports.authenticateWithAwsCredentials = authenticateWithAwsCredentials; diff --git a/auth/customCredentialSupplierOkta.js b/auth/customCredentialSupplierOkta.js new file mode 100644 index 0000000000..17d4609b96 --- /dev/null +++ b/auth/customCredentialSupplierOkta.js @@ -0,0 +1,176 @@ +// 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. + +// [START auth_custom_credential_supplier_okta] +const {IdentityPoolClient} = require('google-auth-library'); +const {Gaxios} = require('gaxios'); + +/** + * A custom SubjectTokenSupplier that authenticates with Okta using the + * Client Credentials grant flow. + */ +class OktaClientCredentialsSupplier { + constructor(domain, clientId, clientSecret) { + this.oktaTokenUrl = `${domain}/oauth2/default/v1/token`; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.accessToken = null; + this.expiryTime = 0; + this.gaxios = new Gaxios(); + } + + /** + * Main method called by the auth library. It will fetch a new token if one + * is not already cached. + * @returns {Promise} A promise that resolves with the Okta Access token. + */ + async getSubjectToken() { + // Check if the current token is still valid (with a 60-second buffer). + const isTokenValid = + this.accessToken && Date.now() < this.expiryTime - 60 * 1000; + + if (isTokenValid) { + return this.accessToken; + } + + const {accessToken, expiresIn} = await this.fetchOktaAccessToken(); + this.accessToken = accessToken; + this.expiryTime = Date.now() + expiresIn * 1000; + return this.accessToken; + } + + /** + * Performs the Client Credentials grant flow with Okta. + */ + async fetchOktaAccessToken() { + const params = new URLSearchParams(); + params.append('grant_type', 'client_credentials'); + params.append('scope', 'gcp.test.read'); + + const authHeader = + 'Basic ' + + Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64'); + + try { + const response = await this.gaxios.request({ + url: this.oktaTokenUrl, + method: 'POST', + headers: { + Authorization: authHeader, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + data: params.toString(), + }); + + const {access_token, expires_in} = response.data; + if (access_token && expires_in) { + return {accessToken: access_token, expiresIn: expires_in}; + } else { + throw new Error( + 'Access token or expires_in not found in Okta response.' + ); + } + } catch (error) { + throw new Error( + `Failed to authenticate with Okta: ${error.response?.data || error.message}` + ); + } + } +} + +/** + * Authenticates with Google Cloud using Okta credentials and retrieves bucket metadata. + * + * @param {string} bucketName The name of the bucket to retrieve. + * @param {string} audience The Workload Identity Pool audience. + * @param {string} domain The Okta domain. + * @param {string} clientId The Okta client ID. + * @param {string} clientSecret The Okta client secret. + * @param {string} [impersonationUrl] Optional Service Account impersonation URL. + */ +async function authenticateWithOktaCredentials( + bucketName, + audience, + domain, + clientId, + clientSecret, + impersonationUrl +) { + // 1. Instantiate the custom supplier. + const oktaSupplier = new OktaClientCredentialsSupplier( + domain, + clientId, + clientSecret + ); + + // 2. Instantiate an IdentityPoolClient with the required configuration. + const client = new IdentityPoolClient({ + audience: audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: 'https://sts.googleapis.com/v1/token', + subject_token_supplier: oktaSupplier, + service_account_impersonation_url: impersonationUrl, + }); + + // 3. Make an authenticated request. + const bucketUrl = `https://storage.googleapis.com/storage/v1/b/${bucketName}`; + const res = await client.request({url: bucketUrl}); + return res.data; +} +// [END auth_custom_credential_supplier_okta] + +async function main() { + require('dotenv').config(); + const gcpAudience = process.env.GCP_WORKLOAD_AUDIENCE; + const saImpersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; + const gcsBucketName = process.env.GCS_BUCKET_NAME; + const oktaDomain = process.env.OKTA_DOMAIN; + const oktaClientId = process.env.OKTA_CLIENT_ID; + const oktaClientSecret = process.env.OKTA_CLIENT_SECRET; + + if ( + !gcpAudience || + !gcsBucketName || + !oktaDomain || + !oktaClientId || + !oktaClientSecret + ) { + throw new Error('Missing required environment variables for Okta/GCP.'); + } + + try { + console.log(`Retrieving metadata for bucket: ${gcsBucketName}...`); + const bucketMetadata = await authenticateWithOktaCredentials( + gcsBucketName, + gcpAudience, + oktaDomain, + oktaClientId, + oktaClientSecret, + saImpersonationUrl + ); + console.log('\n--- SUCCESS! ---'); + console.log('Bucket Name:', bucketMetadata.name); + console.log('Bucket Location:', bucketMetadata.location); + } catch (error) { + console.error('\n--- FAILED ---'); + console.error(error.response?.data || error); + process.exitCode = 1; + } +} + +if (require.main === module) { + main(); +} + +exports.authenticateWithOktaCredentials = authenticateWithOktaCredentials; diff --git a/auth/system-test/customCredentialSupplierAwsTest.js b/auth/system-test/customCredentialSupplierAwsTest.js new file mode 100644 index 0000000000..f26117b515 --- /dev/null +++ b/auth/system-test/customCredentialSupplierAwsTest.js @@ -0,0 +1,48 @@ +// 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. + +'use strict'; + +const assert = require('assert'); +const { + authenticateWithAwsCredentials, +} = require('../custom-credential-supplier-aws'); + +describe('Custom Credential Supplier AWS', () => { + const audience = process.env.GCP_WORKLOAD_AUDIENCE; + const bucketName = process.env.GCS_BUCKET_NAME; + const impersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; + + it('should authenticate using AWS credentials', async function () { + // Skip system tests if required environment variables are missing + if ( + !process.env.AWS_ACCESS_KEY_ID || + !process.env.AWS_SECRET_ACCESS_KEY || + !process.env.AWS_REGION || + !audience || + !bucketName + ) { + this.skip(); + } + + const metadata = await authenticateWithAwsCredentials( + bucketName, + audience, + impersonationUrl, + ); + + assert.strictEqual(metadata.name, bucketName); + assert.ok(metadata.location); + }); +}); diff --git a/auth/system-test/customCredentialSupplierOktaTest.js b/auth/system-test/customCredentialSupplierOktaTest.js new file mode 100644 index 0000000000..a314aa5573 --- /dev/null +++ b/auth/system-test/customCredentialSupplierOktaTest.js @@ -0,0 +1,51 @@ +// 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. + +'use strict'; + +const assert = require('assert'); +const { + authenticateWithOktaCredentials, +} = require('../custom-credential-supplier-okta'); + +describe('Custom Credential Supplier Okta', () => { + const audience = process.env.GCP_WORKLOAD_AUDIENCE; + const bucketName = process.env.GCS_BUCKET_NAME; + const impersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; + + it('should authenticate using Okta credentials', async function () { + // Skip system tests if required environment variables are missing + if ( + !process.env.OKTA_DOMAIN || + !process.env.OKTA_CLIENT_ID || + !process.env.OKTA_CLIENT_SECRET || + !audience || + !bucketName + ) { + this.skip(); + } + + const metadata = await authenticateWithOktaCredentials( + bucketName, + audience, + process.env.OKTA_DOMAIN, + process.env.OKTA_CLIENT_ID, + process.env.OKTA_CLIENT_SECRET, + impersonationUrl, + ); + + assert.strictEqual(metadata.name, bucketName); + assert.ok(metadata.location); + }); +}); From 65200e020913f33c890d71639640f41103267b39 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Mon, 17 Nov 2025 16:22:25 -0800 Subject: [PATCH 2/9] Fixed tests and added some lint corrections. --- .eslintrc.json | 1 + auth/.eslintrc.json | 6 ++++++ auth/package.json | 4 ++++ auth/system-test/customCredentialSupplierAwsTest.js | 4 ++-- auth/system-test/customCredentialSupplierOktaTest.js | 4 ++-- 5 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 auth/.eslintrc.json diff --git a/.eslintrc.json b/.eslintrc.json index 7e5a1dd078..44510807ab 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -12,6 +12,7 @@ "node/no-unsupported-features/es-syntax": ["off"] }, "parserOptions": { + "ecmaVersion": 2020, "sourceType": "module" } } diff --git a/auth/.eslintrc.json b/auth/.eslintrc.json new file mode 100644 index 0000000000..95f214a816 --- /dev/null +++ b/auth/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "../.eslintrc.json", + "rules": { + "no-unused-vars": "off" + } +} diff --git a/auth/package.json b/auth/package.json index 71988b2b5f..3826539aa9 100644 --- a/auth/package.json +++ b/auth/package.json @@ -19,6 +19,10 @@ "system-test": "c8 mocha -p -j 2 system-test/*.test.js --timeout=30000" }, "dependencies": { + "@aws-sdk/client-sts": "^3.58.0", + "@aws-sdk/credential-providers": "^3.0.0", + "dotenv": "^17.0.0", + "gaxios": "^6.0.0", "@google-cloud/storage": "^7.0.0", "fix": "0.0.6", "google-auth-library": "^9.0.0", diff --git a/auth/system-test/customCredentialSupplierAwsTest.js b/auth/system-test/customCredentialSupplierAwsTest.js index f26117b515..cba05aff03 100644 --- a/auth/system-test/customCredentialSupplierAwsTest.js +++ b/auth/system-test/customCredentialSupplierAwsTest.js @@ -17,7 +17,7 @@ const assert = require('assert'); const { authenticateWithAwsCredentials, -} = require('../custom-credential-supplier-aws'); +} = require('../customCredentialSupplierAws'); describe('Custom Credential Supplier AWS', () => { const audience = process.env.GCP_WORKLOAD_AUDIENCE; @@ -39,7 +39,7 @@ describe('Custom Credential Supplier AWS', () => { const metadata = await authenticateWithAwsCredentials( bucketName, audience, - impersonationUrl, + impersonationUrl ); assert.strictEqual(metadata.name, bucketName); diff --git a/auth/system-test/customCredentialSupplierOktaTest.js b/auth/system-test/customCredentialSupplierOktaTest.js index a314aa5573..244f5a87a5 100644 --- a/auth/system-test/customCredentialSupplierOktaTest.js +++ b/auth/system-test/customCredentialSupplierOktaTest.js @@ -17,7 +17,7 @@ const assert = require('assert'); const { authenticateWithOktaCredentials, -} = require('../custom-credential-supplier-okta'); +} = require('../customCredentialSupplierOkta'); describe('Custom Credential Supplier Okta', () => { const audience = process.env.GCP_WORKLOAD_AUDIENCE; @@ -42,7 +42,7 @@ describe('Custom Credential Supplier Okta', () => { process.env.OKTA_DOMAIN, process.env.OKTA_CLIENT_ID, process.env.OKTA_CLIENT_SECRET, - impersonationUrl, + impersonationUrl ); assert.strictEqual(metadata.name, bucketName); From aa306360a8bf902c3dd2cbc841f6f9814d16d773 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Mon, 17 Nov 2025 23:37:27 -0800 Subject: [PATCH 3/9] Changed tests formatting. --- .../customCredentialSupplierAws.test.js | 48 +++++++++++++++++ .../customCredentialSupplierOkta.test.js | 51 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 auth/system-test/customCredentialSupplierAws.test.js create mode 100644 auth/system-test/customCredentialSupplierOkta.test.js diff --git a/auth/system-test/customCredentialSupplierAws.test.js b/auth/system-test/customCredentialSupplierAws.test.js new file mode 100644 index 0000000000..cba05aff03 --- /dev/null +++ b/auth/system-test/customCredentialSupplierAws.test.js @@ -0,0 +1,48 @@ +// 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. + +'use strict'; + +const assert = require('assert'); +const { + authenticateWithAwsCredentials, +} = require('../customCredentialSupplierAws'); + +describe('Custom Credential Supplier AWS', () => { + const audience = process.env.GCP_WORKLOAD_AUDIENCE; + const bucketName = process.env.GCS_BUCKET_NAME; + const impersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; + + it('should authenticate using AWS credentials', async function () { + // Skip system tests if required environment variables are missing + if ( + !process.env.AWS_ACCESS_KEY_ID || + !process.env.AWS_SECRET_ACCESS_KEY || + !process.env.AWS_REGION || + !audience || + !bucketName + ) { + this.skip(); + } + + const metadata = await authenticateWithAwsCredentials( + bucketName, + audience, + impersonationUrl + ); + + assert.strictEqual(metadata.name, bucketName); + assert.ok(metadata.location); + }); +}); diff --git a/auth/system-test/customCredentialSupplierOkta.test.js b/auth/system-test/customCredentialSupplierOkta.test.js new file mode 100644 index 0000000000..244f5a87a5 --- /dev/null +++ b/auth/system-test/customCredentialSupplierOkta.test.js @@ -0,0 +1,51 @@ +// 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. + +'use strict'; + +const assert = require('assert'); +const { + authenticateWithOktaCredentials, +} = require('../customCredentialSupplierOkta'); + +describe('Custom Credential Supplier Okta', () => { + const audience = process.env.GCP_WORKLOAD_AUDIENCE; + const bucketName = process.env.GCS_BUCKET_NAME; + const impersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; + + it('should authenticate using Okta credentials', async function () { + // Skip system tests if required environment variables are missing + if ( + !process.env.OKTA_DOMAIN || + !process.env.OKTA_CLIENT_ID || + !process.env.OKTA_CLIENT_SECRET || + !audience || + !bucketName + ) { + this.skip(); + } + + const metadata = await authenticateWithOktaCredentials( + bucketName, + audience, + process.env.OKTA_DOMAIN, + process.env.OKTA_CLIENT_ID, + process.env.OKTA_CLIENT_SECRET, + impersonationUrl + ); + + assert.strictEqual(metadata.name, bucketName); + assert.ok(metadata.location); + }); +}); From a2759769eef3ed9b968c587f4f4ac06b36522b49 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Mon, 17 Nov 2025 23:38:25 -0800 Subject: [PATCH 4/9] Deleted old test files. --- .../customCredentialSupplierAwsTest.js | 48 ----------------- .../customCredentialSupplierOktaTest.js | 51 ------------------- 2 files changed, 99 deletions(-) delete mode 100644 auth/system-test/customCredentialSupplierAwsTest.js delete mode 100644 auth/system-test/customCredentialSupplierOktaTest.js diff --git a/auth/system-test/customCredentialSupplierAwsTest.js b/auth/system-test/customCredentialSupplierAwsTest.js deleted file mode 100644 index cba05aff03..0000000000 --- a/auth/system-test/customCredentialSupplierAwsTest.js +++ /dev/null @@ -1,48 +0,0 @@ -// 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. - -'use strict'; - -const assert = require('assert'); -const { - authenticateWithAwsCredentials, -} = require('../customCredentialSupplierAws'); - -describe('Custom Credential Supplier AWS', () => { - const audience = process.env.GCP_WORKLOAD_AUDIENCE; - const bucketName = process.env.GCS_BUCKET_NAME; - const impersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; - - it('should authenticate using AWS credentials', async function () { - // Skip system tests if required environment variables are missing - if ( - !process.env.AWS_ACCESS_KEY_ID || - !process.env.AWS_SECRET_ACCESS_KEY || - !process.env.AWS_REGION || - !audience || - !bucketName - ) { - this.skip(); - } - - const metadata = await authenticateWithAwsCredentials( - bucketName, - audience, - impersonationUrl - ); - - assert.strictEqual(metadata.name, bucketName); - assert.ok(metadata.location); - }); -}); diff --git a/auth/system-test/customCredentialSupplierOktaTest.js b/auth/system-test/customCredentialSupplierOktaTest.js deleted file mode 100644 index 244f5a87a5..0000000000 --- a/auth/system-test/customCredentialSupplierOktaTest.js +++ /dev/null @@ -1,51 +0,0 @@ -// 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. - -'use strict'; - -const assert = require('assert'); -const { - authenticateWithOktaCredentials, -} = require('../customCredentialSupplierOkta'); - -describe('Custom Credential Supplier Okta', () => { - const audience = process.env.GCP_WORKLOAD_AUDIENCE; - const bucketName = process.env.GCS_BUCKET_NAME; - const impersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; - - it('should authenticate using Okta credentials', async function () { - // Skip system tests if required environment variables are missing - if ( - !process.env.OKTA_DOMAIN || - !process.env.OKTA_CLIENT_ID || - !process.env.OKTA_CLIENT_SECRET || - !audience || - !bucketName - ) { - this.skip(); - } - - const metadata = await authenticateWithOktaCredentials( - bucketName, - audience, - process.env.OKTA_DOMAIN, - process.env.OKTA_CLIENT_ID, - process.env.OKTA_CLIENT_SECRET, - impersonationUrl - ); - - assert.strictEqual(metadata.name, bucketName); - assert.ok(metadata.location); - }); -}); From faaade6b43c6e651e8c19888e9fe2f873640579c Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Mon, 15 Dec 2025 13:05:26 -0800 Subject: [PATCH 5/9] Custom Credentials now read from secrets.json as opposed to env variables. --- auth/README.md | 71 ---------- auth/customcredentials/aws/Dockerfile | 25 ++++ auth/customcredentials/aws/README.md | 122 ++++++++++++++++++ ...ustom-credentials-aws-secrets.json.example | 8 ++ .../aws}/customCredentialSupplierAws.js | 78 +++++++++-- auth/customcredentials/aws/pod.yaml | 44 +++++++ auth/customcredentials/okta/README.md | 92 +++++++++++++ ...stom-credentials-okta-secrets.json.example | 8 ++ .../okta}/customCredentialSupplierOkta.js | 81 ++++++++++-- auth/package.json | 4 +- .../customCredentialSupplierAws.test.js | 48 ------- .../customCredentialSupplierOkta.test.js | 51 -------- .../aws/customCredentialSupplierAws.test.js | 99 ++++++++++++++ .../okta/customCredentialSupplierOkta.test.js | 102 +++++++++++++++ 14 files changed, 635 insertions(+), 198 deletions(-) create mode 100644 auth/customcredentials/aws/Dockerfile create mode 100644 auth/customcredentials/aws/README.md create mode 100644 auth/customcredentials/aws/custom-credentials-aws-secrets.json.example rename auth/{ => customcredentials/aws}/customCredentialSupplierAws.js (67%) create mode 100644 auth/customcredentials/aws/pod.yaml create mode 100644 auth/customcredentials/okta/README.md create mode 100644 auth/customcredentials/okta/custom-credentials-okta-secrets.json.example rename auth/{ => customcredentials/okta}/customCredentialSupplierOkta.js (68%) delete mode 100644 auth/system-test/customCredentialSupplierAws.test.js delete mode 100644 auth/system-test/customCredentialSupplierOkta.test.js create mode 100644 auth/system-test/customcredentials/aws/customCredentialSupplierAws.test.js create mode 100644 auth/system-test/customcredentials/okta/customCredentialSupplierOkta.test.js diff --git a/auth/README.md b/auth/README.md index a08eb49384..15d899867d 100644 --- a/auth/README.md +++ b/auth/README.md @@ -60,77 +60,6 @@ information](https://developers.google.com/identity/protocols/application-defaul $ npm run test:downscoping -## Custom Credential Suppliers - -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. - -### Custom AWS Credential Supplier - -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. - -#### 1. Set Environment Variables - -```bash -# AWS Credentials (or use ~/.aws/credentials) -export AWS_ACCESS_KEY_ID="YOUR_AWS_ACCESS_KEY_ID" -export AWS_SECRET_ACCESS_KEY="YOUR_AWS_SECRET_ACCESS_KEY" -export AWS_REGION="us-east-1" - -# Google Cloud Config -# Format: //iam.googleapis.com/projects//locations/global/workloadIdentityPools//providers/ -export GCP_WORKLOAD_AUDIENCE="//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/my-pool/providers/my-aws-provider" -export GCS_BUCKET_NAME="your-bucket-name" - -# Optional: Service Account Impersonation -# export GCP_SERVICE_ACCOUNT_IMPERSONATION_URL="https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/my-sa@my-project.iam.gserviceaccount.com:generateAccessToken" -``` - -#### 2. Run the Sample - -```bash -node custom-credential-supplier-aws.js -``` - -#### Running in Kubernetes (EKS) - -To run this in an EKS cluster using IAM Roles for Service Accounts (IRSA): - -1. **Configure IRSA:** Associate an AWS IAM Role with your Kubernetes Service Account. -2. **Configure GCP:** Allow the AWS IAM Role ARN to impersonate your Workload Identity Pool. -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. - ---- - -### Custom Okta Credential Supplier - -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. - -#### 1. Okta Configuration - -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. - -#### 2. Set Environment Variables - -```bash -# Okta Configuration -export OKTA_DOMAIN="https://your-okta-domain.okta.com" -export OKTA_CLIENT_ID="your-okta-client-id" -export OKTA_CLIENT_SECRET="your-okta-client-secret" - -# Google Cloud Config -export GCP_WORKLOAD_AUDIENCE="//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/my-pool/providers/my-oidc-provider" -export GCS_BUCKET_NAME="your-bucket-name" - -# Optional: Service Account Impersonation -# export GCP_SERVICE_ACCOUNT_IMPERSONATION_URL="https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/my-sa@my-project.iam.gserviceaccount.com:generateAccessToken" -``` - -#### 3. Run the Sample - -```bash -node custom-credential-supplier-okta.js -``` - ### Additional resources For more information on downscoped credentials you can visit: diff --git a/auth/customcredentials/aws/Dockerfile b/auth/customcredentials/aws/Dockerfile new file mode 100644 index 0000000000..23760295b3 --- /dev/null +++ b/auth/customcredentials/aws/Dockerfile @@ -0,0 +1,25 @@ +# Use the official Node.js image. +# https://hub.docker.com/_/node +FROM node:20-slim + +# Create and change to the app directory. +WORKDIR /app + +# Copy application dependency manifests to the container image. +# A wildcard is used to ensure both package.json AND package-lock.json are copied. +COPY package*.json ./ + +# Install production dependencies. +RUN npm install --omit=dev + +# Create a non-root user for security +RUN useradd -m appuser + +# Copy local code to the container image. +COPY --chown=appuser:appuser . . + +# Switch to non-root user +USER appuser + +# Run the web service on container startup. +CMD [ "node", "customCredentialSupplierAws.js" ] diff --git a/auth/customcredentials/aws/README.md b/auth/customcredentials/aws/README.md new file mode 100644 index 0000000000..00602f274d --- /dev/null +++ b/auth/customcredentials/aws/README.md @@ -0,0 +1,122 @@ +# Running the Custom AWS Credential Supplier Sample (Node.js) + +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. + +## Prerequisites + +* An AWS account. +* A Google Cloud project with the IAM API enabled. +* A GCS bucket. +* **Node.js 16** or later installed. +* **npm** installed. + +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. + +## Running Locally + +For local development, you can provide credentials and configuration in a JSON file. + +### Install Dependencies + +Ensure you have Node.js installed, then install the required libraries: + +```bash +npm install +``` + +### Configure Credentials for Local Development + +1. Copy the example secrets file to a new file named `custom-credentials-aws-secrets.json` in the project root: + ```bash + cp custom-credentials-aws-secrets.json.example custom-credentials-aws-secrets.json + ``` +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. + + +### Run the Application + +Execute the script using node: + +```bash +node customCredentialSupplierAws.js +``` + +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. + +## Running in a Containerized Environment (EKS) + +This section provides a brief overview of how to run the sample in an Amazon EKS cluster. + +### EKS Cluster Setup + +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). + +### Configure IAM Roles for Service Accounts (IRSA) + +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. + +Run the following command to create the IAM role and bind it to a Kubernetes Service Account: + +```bash +eksctl create iamserviceaccount \ + --name your-k8s-service-account \ + --namespace default \ + --cluster your-cluster-name \ + --region your-aws-region \ + --role-name your-role-name \ + --attach-policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \ + --approve +``` + +> **Note**: The `--attach-policy-arn` flag is used here to demonstrate attaching permissions. Update this with the specific AWS policy ARN your application requires. + +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. + +### Configure Google Cloud to Trust the AWS Role + +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: + +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. + +2. **Create or select a Google Cloud Service Account:** This service account will be impersonated by your AWS role. + +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. + +For more detailed information, see the documentation on [Configuring Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds). + +### Containerize and Package the Application + +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. + +**Note:** The provided [`Dockerfile`](Dockerfile) is an example and may need modification for your specific needs. + +Build and push the image: +```bash +docker build -t your-container-image:latest . +docker push your-container-image:latest +``` + +### Deploy to EKS + +Create a Kubernetes deployment manifest to deploy your application to the EKS cluster. See the [`pod.yaml`](pod.yaml) file for an example. + +**Note:** The provided [`pod.yaml`](pod.yaml) is an example and may need to be modified for your specific needs. + +Deploy the pod: + +```bash +kubectl apply -f pod.yaml +``` + +### Clean Up + +To clean up the resources, delete the EKS cluster and any other AWS and Google Cloud resources you created. + +```bash +eksctl delete cluster --name your-cluster-name +``` + +## Testing + +This sample is not continuously tested. It is provided for instructional purposes and may require modifications to work in your environment. +``` \ No newline at end of file diff --git a/auth/customcredentials/aws/custom-credentials-aws-secrets.json.example b/auth/customcredentials/aws/custom-credentials-aws-secrets.json.example new file mode 100644 index 0000000000..300dc70c13 --- /dev/null +++ b/auth/customcredentials/aws/custom-credentials-aws-secrets.json.example @@ -0,0 +1,8 @@ +{ + "aws_access_key_id": "YOUR_AWS_ACCESS_KEY_ID", + "aws_secret_access_key": "YOUR_AWS_SECRET_ACCESS_KEY", + "aws_region": "YOUR_AWS_REGION", + "gcp_workload_audience": "YOUR_GCP_WORKLOAD_AUDIENCE", + "gcs_bucket_name": "YOUR_GCS_BUCKET_NAME", + "gcp_service_account_impersonation_url": "YOUR_GCP_SERVICE_ACCOUNT_IMPERSONATION_URL" +} diff --git a/auth/customCredentialSupplierAws.js b/auth/customcredentials/aws/customCredentialSupplierAws.js similarity index 67% rename from auth/customCredentialSupplierAws.js rename to auth/customcredentials/aws/customCredentialSupplierAws.js index 8662f16708..17c253427c 100644 --- a/auth/customCredentialSupplierAws.js +++ b/auth/customcredentials/aws/customCredentialSupplierAws.js @@ -15,7 +15,10 @@ // [START auth_custom_credential_supplier_aws] const {AwsClient} = require('google-auth-library'); const {fromNodeProviderChain} = require('@aws-sdk/credential-providers'); +const fs = require('fs'); +const path = require('path'); const {STSClient} = require('@aws-sdk/client-sts'); +const {Storage} = require('@google-cloud/storage'); /** * Custom AWS Security Credentials Supplier. @@ -93,10 +96,8 @@ async function authenticateWithAwsCredentials( audience, impersonationUrl ) { - // 1. Instantiate the custom supplier. const customSupplier = new CustomAwsSupplier(); - // 2. Configure the AwsClient options. const clientOptions = { audience: audience, subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', @@ -104,25 +105,77 @@ async function authenticateWithAwsCredentials( aws_security_credentials_supplier: customSupplier, }; - // 3. Create the auth client - const client = new AwsClient(clientOptions); + const authClient = new AwsClient(clientOptions); - // 4. Make an authenticated request to GCS. - const bucketUrl = `https://storage.googleapis.com/storage/v1/b/${bucketName}`; - const res = await client.request({url: bucketUrl}); - return res.data; + const storage = new Storage({ + authClient: authClient, + }); + + const [metadata] = await storage.bucket(bucketName).getMetadata(); + return metadata; } // [END auth_custom_credential_supplier_aws] +/** + * If a local secrets file is present, load it into the process environment. + * This is a "just-in-time" configuration for local development. These + * variables are only set for the current process. + */ +function loadConfigFromFile() { + const secretsFile = 'custom-credentials-aws-secrets.json'; + const secretsPath = path.resolve(__dirname, secretsFile); + + if (!fs.existsSync(secretsPath)) { + return; + } + + try { + const secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8')); + + if (!secrets) { + return; + } + + // AWS SDK for Node.js looks for environment variables with specific names. + if (secrets.aws_access_key_id) { + process.env.AWS_ACCESS_KEY_ID = secrets.aws_access_key_id; + } + if (secrets.aws_secret_access_key) { + process.env.AWS_SECRET_ACCESS_KEY = secrets.aws_secret_access_key; + } + if (secrets.aws_region) { + process.env.AWS_REGION = secrets.aws_region; + } + + // Set custom GCP variables so they can be retrieved from process.env. + if (secrets.gcp_workload_audience) { + process.env.GCP_WORKLOAD_AUDIENCE = secrets.gcp_workload_audience; + } + if (secrets.gcs_bucket_name) { + process.env.GCS_BUCKET_NAME = secrets.gcs_bucket_name; + } + if (secrets.gcp_service_account_impersonation_url) { + process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL = + secrets.gcp_service_account_impersonation_url; + } + } catch (error) { + console.error(`Error reading secrets file: ${error.message}`); + } +} + async function main() { - require('dotenv').config(); + // Reads the secrets.json if running locally. + loadConfigFromFile(); + const gcpAudience = process.env.GCP_WORKLOAD_AUDIENCE; const saImpersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; const gcsBucketName = process.env.GCS_BUCKET_NAME; if (!gcpAudience || !gcsBucketName) { throw new Error( - 'Missing required environment variables: GCP_WORKLOAD_AUDIENCE, GCS_BUCKET_NAME' + 'Missing required configuration. Please provide it in a ' + + 'secrets.json file or as environment variables: ' + + 'GCP_WORKLOAD_AUDIENCE, GCS_BUCKET_NAME' ); } @@ -134,11 +187,10 @@ async function main() { saImpersonationUrl ); console.log('\n--- SUCCESS! ---'); - console.log('Bucket Name:', bucketMetadata.name); - console.log('Bucket Location:', bucketMetadata.location); + console.log('Bucket Metadata:', JSON.stringify(bucketMetadata, null, 2)); } catch (error) { console.error('\n--- FAILED ---'); - console.error(error.response?.data || error); + console.error(error.message || error); process.exitCode = 1; } } diff --git a/auth/customcredentials/aws/pod.yaml b/auth/customcredentials/aws/pod.yaml new file mode 100644 index 0000000000..20ca4bf710 --- /dev/null +++ b/auth/customcredentials/aws/pod.yaml @@ -0,0 +1,44 @@ +# 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. + +apiVersion: v1 +kind: Pod +metadata: + name: custom-credential-pod-node +spec: + # The Kubernetes Service Account that is annotated with the corresponding + # AWS IAM role ARN. See the README for instructions on setting up IAM + # Roles for Service Accounts (IRSA). + serviceAccountName: your-k8s-service-account + containers: + - name: gcp-auth-sample-node + # The container image pushed to the container registry + # For example, Amazon Elastic Container Registry + image: your-container-image:latest + env: + # REQUIRED: The AWS region. The AWS SDK for Node.js requires this + # to be set explicitly in containers. + - name: AWS_REGION + value: "your-aws-region" + + # REQUIRED: The full identifier of the Workload Identity Pool provider + - name: GCP_WORKLOAD_AUDIENCE + value: "your-gcp-workload-audience" + + # OPTIONAL: Enable Google Cloud service account impersonation + # - name: GCP_SERVICE_ACCOUNT_IMPERSONATION_URL + # value: "your-gcp-service-account-impersonation-url" + + # REQUIRED: The bucket to list + - name: GCS_BUCKET_NAME + value: "your-gcs-bucket-name" diff --git a/auth/customcredentials/okta/README.md b/auth/customcredentials/okta/README.md new file mode 100644 index 0000000000..7c2e99b1fb --- /dev/null +++ b/auth/customcredentials/okta/README.md @@ -0,0 +1,92 @@ +# Running the Custom Okta Credential Supplier Sample (Node.js) + +This sample demonstrates how to use a custom subject token supplier to authenticate with Google Cloud using Okta as an external identity provider. It uses the Client Credentials flow for machine-to-machine (M2M) authentication. + +## Prerequisites + +* An Okta developer account. +* A Google Cloud project with the IAM API enabled. +* A Google Cloud Storage bucket. Ensure that the authenticated user has access to this bucket. +* **Node.js 16** or later installed. +* **npm** installed. + +## Okta Configuration + +Before running the sample, you need to configure an Okta application for Machine-to-Machine (M2M) communication. + +### Create an M2M Application in Okta + +1. Log in to your Okta developer console. +2. Navigate to **Applications** > **Applications** and click **Create App Integration**. +3. Select **API Services** as the sign-on method and click **Next**. +4. Give your application a name and click **Save**. + +### Obtain Okta Credentials + +Once the application is created, you will find the following information in the **General** tab: + +* **Okta Domain**: Your Okta developer domain (e.g., `https://dev-123456.okta.com`). +* **Client ID**: The client ID for your application. +* **Client Secret**: The client secret for your application. + +You will need these values to configure the sample. + +## Google Cloud Configuration + +You need to configure a Workload Identity Pool in Google Cloud to trust the Okta application. + +### Set up Workload Identity Federation + +1. In the Google Cloud Console, navigate to **IAM & Admin** > **Workload Identity Federation**. +2. Click **Create Pool** to create a new Workload Identity Pool. +3. Add a new **OIDC provider** to the pool. +4. Configure the provider with your Okta domain as the issuer URL. +5. Map the Okta `sub` (subject) assertion to a GCP principal. + +For detailed instructions, refer to the [Workload Identity Federation documentation](https://cloud.google.com/iam/docs/workload-identity-federation). + +## Running the Sample + +To run the sample on your local system, you need to install dependencies and configure your credentials. + +### 1. Install Dependencies + +This command downloads all required Node.js libraries. + +```bash +npm install +``` + +### 2. Configure Credentials + +For local development, this sample reads configuration from a JSON file. + +1. Create a file named `custom-credentials-okta-secrets.json` in the project root. +2. Add the following content, replacing the placeholder values with your configuration: + +```json +{ + "gcp_workload_audience": "//iam.googleapis.com/projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/YOUR_POOL/providers/YOUR_PROVIDER", + "gcs_bucket_name": "your-bucket-name", + "okta_domain": "https://dev-123456.okta.com", + "okta_client_id": "your-okta-client-id", + "okta_client_secret": "your-okta-client-secret", + "gcp_service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/your-sa@your-project.iam.gserviceaccount.com:generateAccessToken" +} +``` + +**Note:** Do not check your secrets file into version control. + +### 3. Run the Application + +Execute the script using Node.js: + +```bash +node customCredentialSupplierOkta.js +``` + +The script authenticates with Okta to get an OIDC token, exchanges that token for a Google Cloud federated token, and uses it to list metadata for the specified Google Cloud Storage bucket. + +## Testing + +This sample is not continuously tested. It is provided for instructional purposes and may require modifications to work in your environment. diff --git a/auth/customcredentials/okta/custom-credentials-okta-secrets.json.example b/auth/customcredentials/okta/custom-credentials-okta-secrets.json.example new file mode 100644 index 0000000000..fa04fda7cb --- /dev/null +++ b/auth/customcredentials/okta/custom-credentials-okta-secrets.json.example @@ -0,0 +1,8 @@ +{ + "okta_domain": "https://your-okta-domain.okta.com", + "okta_client_id": "your-okta-client-id", + "okta_client_secret": "your-okta-client-secret", + "gcp_workload_audience": "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider", + "gcs_bucket_name": "your-gcs-bucket-name", + "gcp_service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/my-service-account@my-project.iam.gserviceaccount.com:generateAccessToken" +} diff --git a/auth/customCredentialSupplierOkta.js b/auth/customcredentials/okta/customCredentialSupplierOkta.js similarity index 68% rename from auth/customCredentialSupplierOkta.js rename to auth/customcredentials/okta/customCredentialSupplierOkta.js index 17d4609b96..42d787ca32 100644 --- a/auth/customCredentialSupplierOkta.js +++ b/auth/customcredentials/okta/customCredentialSupplierOkta.js @@ -14,7 +14,10 @@ // [START auth_custom_credential_supplier_okta] const {IdentityPoolClient} = require('google-auth-library'); +const {Storage} = require('@google-cloud/storage'); const {Gaxios} = require('gaxios'); +const fs = require('fs'); +const path = require('path'); /** * A custom SubjectTokenSupplier that authenticates with Okta using the @@ -22,7 +25,10 @@ const {Gaxios} = require('gaxios'); */ class OktaClientCredentialsSupplier { constructor(domain, clientId, clientSecret) { - this.oktaTokenUrl = `${domain}/oauth2/default/v1/token`; + // Ensure domain URL is clean + const cleanDomain = domain.endsWith('/') ? domain.slice(0, -1) : domain; + this.oktaTokenUrl = `${cleanDomain}/oauth2/default/v1/token`; + this.clientId = clientId; this.clientSecret = clientSecret; this.accessToken = null; @@ -107,15 +113,13 @@ async function authenticateWithOktaCredentials( clientSecret, impersonationUrl ) { - // 1. Instantiate the custom supplier. const oktaSupplier = new OktaClientCredentialsSupplier( domain, clientId, clientSecret ); - // 2. Instantiate an IdentityPoolClient with the required configuration. - const client = new IdentityPoolClient({ + const authClient = new IdentityPoolClient({ audience: audience, subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', token_url: 'https://sts.googleapis.com/v1/token', @@ -123,15 +127,64 @@ async function authenticateWithOktaCredentials( service_account_impersonation_url: impersonationUrl, }); - // 3. Make an authenticated request. - const bucketUrl = `https://storage.googleapis.com/storage/v1/b/${bucketName}`; - const res = await client.request({url: bucketUrl}); - return res.data; + const storage = new Storage({ + authClient: authClient, + }); + + const [metadata] = await storage.bucket(bucketName).getMetadata(); + return metadata; } // [END auth_custom_credential_supplier_okta] +/** + * If a local secrets file is present, load it into the process environment. + * This is a "just-in-time" configuration for local development. These + * variables are only set for the current process. + */ +function loadConfigFromFile() { + const secretsFile = 'custom-credentials-okta-secrets.json'; + const secretsPath = path.resolve(__dirname, secretsFile); + + if (!fs.existsSync(secretsPath)) { + return; + } + + try { + const secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8')); + + if (!secrets) { + return; + } + + // Map JSON keys (snake_case) to Environment Variables (UPPER_CASE) + if (secrets.gcp_workload_audience) { + process.env.GCP_WORKLOAD_AUDIENCE = secrets.gcp_workload_audience; + } + if (secrets.gcs_bucket_name) { + process.env.GCS_BUCKET_NAME = secrets.gcs_bucket_name; + } + if (secrets.gcp_service_account_impersonation_url) { + process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL = + secrets.gcp_service_account_impersonation_url; + } + if (secrets.okta_domain) { + process.env.OKTA_DOMAIN = secrets.okta_domain; + } + if (secrets.okta_client_id) { + process.env.OKTA_CLIENT_ID = secrets.okta_client_id; + } + if (secrets.okta_client_secret) { + process.env.OKTA_CLIENT_SECRET = secrets.okta_client_secret; + } + } catch (error) { + console.error(`Error reading secrets file: ${error.message}`); + } +} + +// Load the configuration from the file when the module is loaded. +loadConfigFromFile(); + async function main() { - require('dotenv').config(); const gcpAudience = process.env.GCP_WORKLOAD_AUDIENCE; const saImpersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; const gcsBucketName = process.env.GCS_BUCKET_NAME; @@ -146,7 +199,10 @@ async function main() { !oktaClientId || !oktaClientSecret ) { - throw new Error('Missing required environment variables for Okta/GCP.'); + throw new Error( + 'Missing required configuration. Please provide it in a ' + + 'secrets.json file or as environment variables.' + ); } try { @@ -160,11 +216,10 @@ async function main() { saImpersonationUrl ); console.log('\n--- SUCCESS! ---'); - console.log('Bucket Name:', bucketMetadata.name); - console.log('Bucket Location:', bucketMetadata.location); + console.log('Bucket Metadata:', JSON.stringify(bucketMetadata, null, 2)); } catch (error) { console.error('\n--- FAILED ---'); - console.error(error.response?.data || error); + console.error(error.message || error); process.exitCode = 1; } } diff --git a/auth/package.json b/auth/package.json index 3826539aa9..9b6d438875 100644 --- a/auth/package.json +++ b/auth/package.json @@ -21,10 +21,10 @@ "dependencies": { "@aws-sdk/client-sts": "^3.58.0", "@aws-sdk/credential-providers": "^3.0.0", + "@google-cloud/storage": "^7.18.0", "dotenv": "^17.0.0", - "gaxios": "^6.0.0", - "@google-cloud/storage": "^7.0.0", "fix": "0.0.6", + "gaxios": "^6.0.0", "google-auth-library": "^9.0.0", "yargs": "^17.0.0" }, diff --git a/auth/system-test/customCredentialSupplierAws.test.js b/auth/system-test/customCredentialSupplierAws.test.js deleted file mode 100644 index cba05aff03..0000000000 --- a/auth/system-test/customCredentialSupplierAws.test.js +++ /dev/null @@ -1,48 +0,0 @@ -// 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. - -'use strict'; - -const assert = require('assert'); -const { - authenticateWithAwsCredentials, -} = require('../customCredentialSupplierAws'); - -describe('Custom Credential Supplier AWS', () => { - const audience = process.env.GCP_WORKLOAD_AUDIENCE; - const bucketName = process.env.GCS_BUCKET_NAME; - const impersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; - - it('should authenticate using AWS credentials', async function () { - // Skip system tests if required environment variables are missing - if ( - !process.env.AWS_ACCESS_KEY_ID || - !process.env.AWS_SECRET_ACCESS_KEY || - !process.env.AWS_REGION || - !audience || - !bucketName - ) { - this.skip(); - } - - const metadata = await authenticateWithAwsCredentials( - bucketName, - audience, - impersonationUrl - ); - - assert.strictEqual(metadata.name, bucketName); - assert.ok(metadata.location); - }); -}); diff --git a/auth/system-test/customCredentialSupplierOkta.test.js b/auth/system-test/customCredentialSupplierOkta.test.js deleted file mode 100644 index 244f5a87a5..0000000000 --- a/auth/system-test/customCredentialSupplierOkta.test.js +++ /dev/null @@ -1,51 +0,0 @@ -// 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. - -'use strict'; - -const assert = require('assert'); -const { - authenticateWithOktaCredentials, -} = require('../customCredentialSupplierOkta'); - -describe('Custom Credential Supplier Okta', () => { - const audience = process.env.GCP_WORKLOAD_AUDIENCE; - const bucketName = process.env.GCS_BUCKET_NAME; - const impersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; - - it('should authenticate using Okta credentials', async function () { - // Skip system tests if required environment variables are missing - if ( - !process.env.OKTA_DOMAIN || - !process.env.OKTA_CLIENT_ID || - !process.env.OKTA_CLIENT_SECRET || - !audience || - !bucketName - ) { - this.skip(); - } - - const metadata = await authenticateWithOktaCredentials( - bucketName, - audience, - process.env.OKTA_DOMAIN, - process.env.OKTA_CLIENT_ID, - process.env.OKTA_CLIENT_SECRET, - impersonationUrl - ); - - assert.strictEqual(metadata.name, bucketName); - assert.ok(metadata.location); - }); -}); diff --git a/auth/system-test/customcredentials/aws/customCredentialSupplierAws.test.js b/auth/system-test/customcredentials/aws/customCredentialSupplierAws.test.js new file mode 100644 index 0000000000..819d55cd14 --- /dev/null +++ b/auth/system-test/customcredentials/aws/customCredentialSupplierAws.test.js @@ -0,0 +1,99 @@ +'use strict'; + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { + authenticateWithAwsCredentials, +} = require('../../../customcredentials/aws/customCredentialSupplierAws.js'); + +describe('Custom Credential Supplier AWS', () => { + // Variables to hold the original environment to restore after tests + const originalEnv = {}; + + // The configuration we need to run the test + let bucketName, audience, impersonationUrl; + + before(function () { + const secretsPath = path.resolve( + __dirname, + '../../../customcredentials/aws/custom-credentials-aws-secrets.json' + ); + + if (fs.existsSync(secretsPath)) { + try { + const content = fs.readFileSync(secretsPath, 'utf8'); + const secrets = JSON.parse(content); + + // Helper to safely set env var if it exists in the JSON + const setEnv = (envKey, jsonKey) => { + if (secrets[jsonKey]) { + // Save original value to restore later + if (process.env[envKey] === undefined) { + originalEnv[envKey] = undefined; // Mark that it was undefined + } else if ( + !Object.prototype.hasOwnProperty.call(originalEnv, envKey) + ) { + originalEnv[envKey] = process.env[envKey]; + } + process.env[envKey] = secrets[jsonKey]; + } + }; + + // Map JSON keys to Environment Variables + setEnv('GCP_WORKLOAD_AUDIENCE', 'gcp_workload_audience'); + setEnv('GCS_BUCKET_NAME', 'gcs_bucket_name'); + setEnv( + 'GCP_SERVICE_ACCOUNT_IMPERSONATION_URL', + 'gcp_service_account_impersonation_url' + ); + setEnv('AWS_ACCESS_KEY_ID', 'aws_access_key_id'); + setEnv('AWS_SECRET_ACCESS_KEY', 'aws_secret_access_key'); + setEnv('AWS_REGION', 'aws_region'); + } catch (err) { + console.warn( + 'Failed to parse secrets file, relying on system env vars.', + err + ); + } + } + + // Extract values from the Environment (whether from file or system) + bucketName = process.env.GCS_BUCKET_NAME; + audience = process.env.GCP_WORKLOAD_AUDIENCE; + impersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; + const awsKey = process.env.AWS_ACCESS_KEY_ID; + const awsSecret = process.env.AWS_SECRET_ACCESS_KEY; + const awsRegion = process.env.AWS_REGION; + + // Skip test if requirements are missing (mimics Java assumeTrue) + if (!bucketName || !audience || !awsKey || !awsSecret || !awsRegion) { + console.log('Skipping AWS system test: Required configuration missing.'); + this.skip(); + } + }); + + after(() => { + // Restore environment variables to their original state + for (const key in originalEnv) { + if (originalEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = originalEnv[key]; + } + } + }); + + it('should authenticate using AWS credentials', async () => { + // Act + const metadata = await authenticateWithAwsCredentials( + bucketName, + audience, + impersonationUrl + ); + + // Assert + assert.strictEqual(metadata.name, bucketName); + assert.ok(metadata.location); + }); +}); diff --git a/auth/system-test/customcredentials/okta/customCredentialSupplierOkta.test.js b/auth/system-test/customcredentials/okta/customCredentialSupplierOkta.test.js new file mode 100644 index 0000000000..8677b24e66 --- /dev/null +++ b/auth/system-test/customcredentials/okta/customCredentialSupplierOkta.test.js @@ -0,0 +1,102 @@ +'use strict'; + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { + authenticateWithOktaCredentials, +} = require('../../../customcredentials/okta/customCredentialSupplierOkta.js'); + +describe('Custom Credential Supplier Okta', () => { + const originalEnv = {}; + let bucketName, + audience, + impersonationUrl, + oktaDomain, + oktaClientId, + oktaClientSecret; + + before(function () { + const secretsPath = path.resolve( + __dirname, + '../../../customcredentials/okta/custom-credentials-okta-secrets.json' + ); + + if (fs.existsSync(secretsPath)) { + try { + const content = fs.readFileSync(secretsPath, 'utf8'); + const secrets = JSON.parse(content); + + const setEnv = (envKey, jsonKey) => { + if (secrets[jsonKey]) { + if (process.env[envKey] === undefined) { + originalEnv[envKey] = undefined; + } else if ( + !Object.prototype.hasOwnProperty.call(originalEnv, envKey) + ) { + originalEnv[envKey] = process.env[envKey]; + } + process.env[envKey] = secrets[jsonKey]; + } + }; + + setEnv('GCP_WORKLOAD_AUDIENCE', 'gcp_workload_audience'); + setEnv('GCS_BUCKET_NAME', 'gcs_bucket_name'); + setEnv( + 'GCP_SERVICE_ACCOUNT_IMPERSONATION_URL', + 'gcp_service_account_impersonation_url' + ); + setEnv('OKTA_DOMAIN', 'okta_domain'); + setEnv('OKTA_CLIENT_ID', 'okta_client_id'); + setEnv('OKTA_CLIENT_SECRET', 'okta_client_secret'); + } catch (err) { + console.warn( + 'Failed to parse secrets file, relying on system env vars.', + err + ); + } + } + + bucketName = process.env.GCS_BUCKET_NAME; + audience = process.env.GCP_WORKLOAD_AUDIENCE; + impersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; + oktaDomain = process.env.OKTA_DOMAIN; + oktaClientId = process.env.OKTA_CLIENT_ID; + oktaClientSecret = process.env.OKTA_CLIENT_SECRET; + + if ( + !bucketName || + !audience || + !oktaDomain || + !oktaClientId || + !oktaClientSecret + ) { + console.log('Skipping Okta system test: Required configuration missing.'); + this.skip(); + } + }); + + after(() => { + for (const key in originalEnv) { + if (originalEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = originalEnv[key]; + } + } + }); + + it('should authenticate using Okta credentials', async () => { + const metadata = await authenticateWithOktaCredentials( + bucketName, + audience, + oktaDomain, + oktaClientId, + oktaClientSecret, + impersonationUrl + ); + + assert.strictEqual(metadata.name, bucketName); + assert.ok(metadata.location); + }); +}); From 258463b7dd39dd878b69edd658cb16f77f755827 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Mon, 15 Dec 2025 13:06:00 -0800 Subject: [PATCH 6/9] package.json changes. --- auth/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/auth/package.json b/auth/package.json index 9b6d438875..00c59e7b65 100644 --- a/auth/package.json +++ b/auth/package.json @@ -15,8 +15,9 @@ "test:auth": "c8 mocha -p -j 2 system-test/auth.test.js --timeout=30000", "test:downscoping": "c8 mocha -p -j 2 system-test/downscoping.test.js --timeout=30000", "test:accessTokenFromImpersonatedCredentials": "c8 mocha -p -j 2 system-test/accessTokenFromImpersonatedCredentials.test.js --timeout=30000", + "test:customcredentials": "c8 mocha -p -j 2 \"system-test/customcredentials/**/*.test.js\" --timeout=30000", "test": "npm -- run system-test", - "system-test": "c8 mocha -p -j 2 system-test/*.test.js --timeout=30000" + "system-test": "c8 mocha -p -j 2 \"system-test/**/*.test.js\" --timeout=30000" }, "dependencies": { "@aws-sdk/client-sts": "^3.58.0", From 83548c0e3bdf122c5f71c7bc2816df231a7c05ae Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Mon, 15 Dec 2025 13:12:10 -0800 Subject: [PATCH 7/9] Added copyright headers. --- .../aws/customCredentialSupplierAws.test.js | 14 ++++++++++++++ .../okta/customCredentialSupplierOkta.test.js | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/auth/system-test/customcredentials/aws/customCredentialSupplierAws.test.js b/auth/system-test/customcredentials/aws/customCredentialSupplierAws.test.js index 819d55cd14..5a3993828b 100644 --- a/auth/system-test/customcredentials/aws/customCredentialSupplierAws.test.js +++ b/auth/system-test/customcredentials/aws/customCredentialSupplierAws.test.js @@ -1,3 +1,17 @@ +// 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. + 'use strict'; const assert = require('assert'); diff --git a/auth/system-test/customcredentials/okta/customCredentialSupplierOkta.test.js b/auth/system-test/customcredentials/okta/customCredentialSupplierOkta.test.js index 8677b24e66..1156a0b094 100644 --- a/auth/system-test/customcredentials/okta/customCredentialSupplierOkta.test.js +++ b/auth/system-test/customcredentials/okta/customCredentialSupplierOkta.test.js @@ -1,3 +1,17 @@ +// 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. + 'use strict'; const assert = require('assert'); From 9243b12885112e0436195e6e4c344256e9a7692f Mon Sep 17 00:00:00 2001 From: Jennifer Davis Date: Wed, 17 Dec 2025 11:15:15 -0800 Subject: [PATCH 8/9] fix: simplify configuration of secrets rather than a bunch of if statements also remove some extraneous comments Signed-off-by: Jennifer Davis --- auth/customcredentials/aws/Dockerfile | 10 ----- auth/customcredentials/aws/README.md | 1 - .../aws/customCredentialSupplierAws.js | 38 +++++-------------- .../okta/customCredentialSupplierOkta.js | 33 +++++----------- 4 files changed, 18 insertions(+), 64 deletions(-) diff --git a/auth/customcredentials/aws/Dockerfile b/auth/customcredentials/aws/Dockerfile index 23760295b3..7f8e9cc0b3 100644 --- a/auth/customcredentials/aws/Dockerfile +++ b/auth/customcredentials/aws/Dockerfile @@ -1,25 +1,15 @@ -# Use the official Node.js image. -# https://hub.docker.com/_/node FROM node:20-slim -# Create and change to the app directory. WORKDIR /app -# Copy application dependency manifests to the container image. -# A wildcard is used to ensure both package.json AND package-lock.json are copied. COPY package*.json ./ -# Install production dependencies. RUN npm install --omit=dev -# Create a non-root user for security RUN useradd -m appuser -# Copy local code to the container image. COPY --chown=appuser:appuser . . -# Switch to non-root user USER appuser -# Run the web service on container startup. CMD [ "node", "customCredentialSupplierAws.js" ] diff --git a/auth/customcredentials/aws/README.md b/auth/customcredentials/aws/README.md index 00602f274d..8d5669ea1f 100644 --- a/auth/customcredentials/aws/README.md +++ b/auth/customcredentials/aws/README.md @@ -119,4 +119,3 @@ eksctl delete cluster --name your-cluster-name ## Testing This sample is not continuously tested. It is provided for instructional purposes and may require modifications to work in your environment. -``` \ No newline at end of file diff --git a/auth/customcredentials/aws/customCredentialSupplierAws.js b/auth/customcredentials/aws/customCredentialSupplierAws.js index 17c253427c..e5ba805753 100644 --- a/auth/customcredentials/aws/customCredentialSupplierAws.js +++ b/auth/customcredentials/aws/customCredentialSupplierAws.js @@ -30,11 +30,8 @@ const {Storage} = require('@google-cloud/storage'); */ class CustomAwsSupplier { constructor() { - // Will be cached upon first resolution. this.region = null; - // Initialize the AWS credential provider. - // The AWS SDK handles memoization (caching) and proactive refreshing internally. this.awsCredentialsProvider = fromNodeProviderChain(); } @@ -65,7 +62,6 @@ class CustomAwsSupplier { * Retrieves AWS security credentials using the AWS SDK's default provider chain. */ async getAwsSecurityCredentials(_context) { - // Call the initialized provider. It will return cached creds or refresh if needed. const awsCredentials = await this.awsCredentialsProvider(); if (!awsCredentials.accessKeyId || !awsCredentials.secretAccessKey) { @@ -75,7 +71,6 @@ class CustomAwsSupplier { ); } - // Map the AWS SDK format to the google-auth-library format. return { accessKeyId: awsCredentials.accessKeyId, secretAccessKey: awsCredentials.secretAccessKey, @@ -131,40 +126,25 @@ function loadConfigFromFile() { try { const secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8')); - if (!secrets) { return; } - // AWS SDK for Node.js looks for environment variables with specific names. - if (secrets.aws_access_key_id) { - process.env.AWS_ACCESS_KEY_ID = secrets.aws_access_key_id; - } - if (secrets.aws_secret_access_key) { - process.env.AWS_SECRET_ACCESS_KEY = secrets.aws_secret_access_key; - } - if (secrets.aws_region) { - process.env.AWS_REGION = secrets.aws_region; - } - - // Set custom GCP variables so they can be retrieved from process.env. - if (secrets.gcp_workload_audience) { - process.env.GCP_WORKLOAD_AUDIENCE = secrets.gcp_workload_audience; - } - if (secrets.gcs_bucket_name) { - process.env.GCS_BUCKET_NAME = secrets.gcs_bucket_name; - } - if (secrets.gcp_service_account_impersonation_url) { - process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL = - secrets.gcp_service_account_impersonation_url; - } + const configMapping = { + aws_access_key_id: 'AWS_ACCESS_KEY_ID', + aws_secret_access_key: 'AWS_SECRET_ACCESS_KEY', + aws_region: 'AWS_REGION', + gcp_workload_audience: 'GCP_WORKLOAD_AUDIENCE', + gcs_bucket_name: 'GCS_BUCKET_NAME', + gcp_service_account_impersonation_url: + 'GCP_SERVICE_ACCOUNT_IMPERSONATION_URL', + }; } catch (error) { console.error(`Error reading secrets file: ${error.message}`); } } async function main() { - // Reads the secrets.json if running locally. loadConfigFromFile(); const gcpAudience = process.env.GCP_WORKLOAD_AUDIENCE; diff --git a/auth/customcredentials/okta/customCredentialSupplierOkta.js b/auth/customcredentials/okta/customCredentialSupplierOkta.js index 42d787ca32..d468796ac8 100644 --- a/auth/customcredentials/okta/customCredentialSupplierOkta.js +++ b/auth/customcredentials/okta/customCredentialSupplierOkta.js @@ -25,7 +25,6 @@ const path = require('path'); */ class OktaClientCredentialsSupplier { constructor(domain, clientId, clientSecret) { - // Ensure domain URL is clean const cleanDomain = domain.endsWith('/') ? domain.slice(0, -1) : domain; this.oktaTokenUrl = `${cleanDomain}/oauth2/default/v1/token`; @@ -42,7 +41,6 @@ class OktaClientCredentialsSupplier { * @returns {Promise} A promise that resolves with the Okta Access token. */ async getSubjectToken() { - // Check if the current token is still valid (with a 60-second buffer). const isTokenValid = this.accessToken && Date.now() < this.expiryTime - 60 * 1000; @@ -151,37 +149,24 @@ function loadConfigFromFile() { try { const secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8')); - if (!secrets) { return; } - // Map JSON keys (snake_case) to Environment Variables (UPPER_CASE) - if (secrets.gcp_workload_audience) { - process.env.GCP_WORKLOAD_AUDIENCE = secrets.gcp_workload_audience; - } - if (secrets.gcs_bucket_name) { - process.env.GCS_BUCKET_NAME = secrets.gcs_bucket_name; - } - if (secrets.gcp_service_account_impersonation_url) { - process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL = - secrets.gcp_service_account_impersonation_url; - } - if (secrets.okta_domain) { - process.env.OKTA_DOMAIN = secrets.okta_domain; - } - if (secrets.okta_client_id) { - process.env.OKTA_CLIENT_ID = secrets.okta_client_id; - } - if (secrets.okta_client_secret) { - process.env.OKTA_CLIENT_SECRET = secrets.okta_client_secret; - } + const configMapping = { + gcp_workload_audience: 'GCP_WORKLOAD_AUDIENCE', + gcs_bucket_name: 'GCS_BUCKET_NAME', + gcp_service_account_impersonation_url: + 'GCP_SERVICE_ACCOUNT_IMPERSONATION_URL', + okta_domain: 'OKTA_DOMAIN', + okta_client_id: 'OKTA_CLIENT_ID', + okta_client_secret: 'OKTA_CLIENT_SECRET', + }; } catch (error) { console.error(`Error reading secrets file: ${error.message}`); } } -// Load the configuration from the file when the module is loaded. loadConfigFromFile(); async function main() { From 91a4fe4f1ab00ab449c9f2d991a3d594b4f77f5d Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Wed, 17 Dec 2025 12:59:18 -0800 Subject: [PATCH 9/9] Crisper load config from file. --- .../aws/customCredentialSupplierAws.js | 22 ++++++++-------- auth/customcredentials/okta/README.md | 25 ++++++------------- .../okta/customCredentialSupplierOkta.js | 24 ++++++++++-------- 3 files changed, 33 insertions(+), 38 deletions(-) diff --git a/auth/customcredentials/aws/customCredentialSupplierAws.js b/auth/customcredentials/aws/customCredentialSupplierAws.js index e5ba805753..c8f136bcf0 100644 --- a/auth/customcredentials/aws/customCredentialSupplierAws.js +++ b/auth/customcredentials/aws/customCredentialSupplierAws.js @@ -117,20 +117,16 @@ async function authenticateWithAwsCredentials( * variables are only set for the current process. */ function loadConfigFromFile() { - const secretsFile = 'custom-credentials-aws-secrets.json'; - const secretsPath = path.resolve(__dirname, secretsFile); - - if (!fs.existsSync(secretsPath)) { - return; - } + const secretsPath = path.resolve( + __dirname, + 'custom-credentials-aws-secrets.json' + ); + if (!fs.existsSync(secretsPath)) return; try { const secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8')); - if (!secrets) { - return; - } - const configMapping = { + const envMap = { aws_access_key_id: 'AWS_ACCESS_KEY_ID', aws_secret_access_key: 'AWS_SECRET_ACCESS_KEY', aws_region: 'AWS_REGION', @@ -139,6 +135,12 @@ function loadConfigFromFile() { gcp_service_account_impersonation_url: 'GCP_SERVICE_ACCOUNT_IMPERSONATION_URL', }; + + for (const [jsonKey, envKey] of Object.entries(envMap)) { + if (secrets[jsonKey]) { + process.env[envKey] = secrets[jsonKey]; + } + } } catch (error) { console.error(`Error reading secrets file: ${error.message}`); } diff --git a/auth/customcredentials/okta/README.md b/auth/customcredentials/okta/README.md index 7c2e99b1fb..daca8c1e03 100644 --- a/auth/customcredentials/okta/README.md +++ b/auth/customcredentials/okta/README.md @@ -57,25 +57,14 @@ This command downloads all required Node.js libraries. npm install ``` -### 2. Configure Credentials - -For local development, this sample reads configuration from a JSON file. - -1. Create a file named `custom-credentials-okta-secrets.json` in the project root. -2. Add the following content, replacing the placeholder values with your configuration: - -```json -{ - "gcp_workload_audience": "//iam.googleapis.com/projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/YOUR_POOL/providers/YOUR_PROVIDER", - "gcs_bucket_name": "your-bucket-name", - "okta_domain": "https://dev-123456.okta.com", - "okta_client_id": "your-okta-client-id", - "okta_client_secret": "your-okta-client-secret", - "gcp_service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/your-sa@your-project.iam.gserviceaccount.com:generateAccessToken" -} -``` +### 2. Configure Credentials for Local Development + +1. Copy the example secrets file to a new file named `custom-credentials-okta-secrets.json` in the project root: + ```bash + cp custom-credentials-okta-secrets.json.example custom-credentials-okta-secrets.json + ``` +2. Open `custom-credentials-okta-secrets.json` and fill in the required values for your AWS and Google Cloud configuration. Do not check your `custom-credentials-okta-secrets.json` file into version control. -**Note:** Do not check your secrets file into version control. ### 3. Run the Application diff --git a/auth/customcredentials/okta/customCredentialSupplierOkta.js b/auth/customcredentials/okta/customCredentialSupplierOkta.js index d468796ac8..b4ed10b654 100644 --- a/auth/customcredentials/okta/customCredentialSupplierOkta.js +++ b/auth/customcredentials/okta/customCredentialSupplierOkta.js @@ -140,20 +140,17 @@ async function authenticateWithOktaCredentials( * variables are only set for the current process. */ function loadConfigFromFile() { - const secretsFile = 'custom-credentials-okta-secrets.json'; - const secretsPath = path.resolve(__dirname, secretsFile); - - if (!fs.existsSync(secretsPath)) { - return; - } + const secretsPath = path.resolve( + __dirname, + 'custom-credentials-okta-secrets.json' + ); + if (!fs.existsSync(secretsPath)) return; try { const secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8')); - if (!secrets) { - return; - } - const configMapping = { + // Define the mapping: JSON Key -> Environment Variable + const envMap = { gcp_workload_audience: 'GCP_WORKLOAD_AUDIENCE', gcs_bucket_name: 'GCS_BUCKET_NAME', gcp_service_account_impersonation_url: @@ -162,6 +159,13 @@ function loadConfigFromFile() { okta_client_id: 'OKTA_CLIENT_ID', okta_client_secret: 'OKTA_CLIENT_SECRET', }; + + // Iterate and assign + for (const [jsonKey, envKey] of Object.entries(envMap)) { + if (secrets[jsonKey]) { + process.env[envKey] = secrets[jsonKey]; + } + } } catch (error) { console.error(`Error reading secrets file: ${error.message}`); }