Skip to content

Instantly share code, notes, and snippets.

@wvanderdeijl
Last active May 16, 2025 14:28
Show Gist options
  • Save wvanderdeijl/e5b20eaa705e2b91588eab54b2534a1e to your computer and use it in GitHub Desktop.
Save wvanderdeijl/e5b20eaa705e2b91588eab54b2534a1e to your computer and use it in GitHub Desktop.
Showcasing Google Cloud Workload Identity Federation from AWS using google-auth-library and nodejs client libraries
// This example is part of a [larger serie of posts](https://gist.github.com/wvanderdeijl/734cc05dd2438a9946c396d714d5e83e) with
// examples of federation between different cloud environments.
import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts';
import { Resource } from '@google-cloud/resource-manager';
import { GoogleAuth, GoogleAuthOptions } from 'google-auth-library';
const AWS_REGION = 'eu-west-1';
const AWS_ROLE_ARN = 'arn:aws:iam::999999999999:role/my-federated-role';
const GCP_PROJECT_ID = 'my-google-project';
const GCP_IDENTITY_PROVIDER = '//iam.googleapis.com/projects/PROJECTNUMBER/locations/global/workloadIdentityPools/POOL-ID/providers/PROVIDER-ID';
const GCP_SERVICE_ACCOUNT = '[email protected]';
(async () => {
if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
// not needed when running on AWS infrastructure, as we would be getting credentials from the AWS metadata service
throw new Error('missing AWS credentials environment variables');
}
// use IAM user credentials to get temporary security credentials. Normally, this step is not necessary as using google workload
// identity federation from an AWS identity is intended to be used from AWS infrastructure that already has an attached IAM Role
const stsClient = new STSClient({ region: AWS_REGION });
const sessionName = new Date().toISOString().replace(/[:.-]/g, ''); // TODO: document
const assumeRoleResponse = await stsClient.send(new AssumeRoleCommand({ RoleArn: AWS_ROLE_ARN, RoleSessionName: sessionName }));
// Since we explicitly fetched temporary AWS credentials, we can set them in environment variables so the google-auth-library
// will pick those up. Normally, this is not needed as the google-auth-library will get the credentials from the AWS metadata service
// when running on AWS infrastructure.
process.env.AWS_REGION = AWS_REGION;
process.env.AWS_ACCESS_KEY_ID = assumeRoleResponse.Credentials.AccessKeyId;
process.env.AWS_SECRET_ACCESS_KEY = assumeRoleResponse.Credentials.SecretAccessKey;
process.env.AWS_SESSION_TOKEN = assumeRoleResponse.Credentials.SessionToken;
// This entire config JSON can all be stored on disk and the GOOGLE_APPLICATION_CREDENTIALS environment variable set to the absolute
// path of that file. Then a simple `new GoogleAuth()` would be sufficient.
// see https://cloud.google.com/iam/docs/access-resources-aws#generate-automatic
// gcloud iam workload-identity-pools create-cred-config \
// projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID \
// --service-account=SERVICE_ACCOUNT_EMAIL \
// --output-file=FILEPATH \
// --aws
const authOptions: GoogleAuthOptions = {
scopes: 'https://www.googleapis.com/auth/cloud-platform',
projectId: GCP_PROJECT_ID,
credentials: {
type: 'external_account',
audience: GCP_IDENTITY_PROVIDER,
subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request',
service_account_impersonation_url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${GCP_SERVICE_ACCOUNT}:generateAccessToken`,
token_url: 'https://sts.googleapis.com/v1/token',
credential_source: {
environment_id: 'aws1',
region_url: 'http://169.254.169.254/latest/meta-data/placement/availability-zone',
url: 'http://169.254.169.254/latest/meta-data/iam/security-credentials',
regional_cred_verification_url: 'https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15',
},
},
};
const auth = new GoogleAuth(authOptions);
const client = await auth.getClient();
const customTokenPayload = {
sub: '[email protected]',
aud: 'https://example.com',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 300,
};
const signedCustomToken = await client.request({
method: 'POST',
url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${GCP_SERVICE_ACCOUNT}:signJwt`,
data: {
payload: JSON.stringify(customTokenPayload),
},
});
console.log('-'.repeat(80));
console.log('signJwt result:');
console.log(signedCustomToken.data);
/*
decoded token header:
{
"alg": "RS256",
"kid": "e8c4....e35a",
"typ": "JWT"
}
decoded token payload:
{
"sub": "[email protected]",
"aud": "https://example.com",
"iat": 1622488946,
"exp": 1622489246
}
*/
// Get an OIDC id-token for the federated service account
// see https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateIdToken
const idtoken = await client.request<{ token: string }>({
method: 'POST',
url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${GCP_SERVICE_ACCOUNT}:generateIdToken`,
data: {
audience: 'https://example.com',
includeEmail: false,
},
});
console.log('-'.repeat(80));
console.log('generateIdToken result:');
console.log(idtoken.data);
/*
decoded token header:
{
"alg": "RS256",
"kid": "1719....08de",
"typ": "JWT"
}
decoded token payload:
{
"aud": "https://example.com",
"azp": "1000....6293", // numerical unique id of the service account
"exp": 1622492676,
"iat": 1622489076,
"iss": "https://accounts.google.com",
"sub": "1000....6293" // numerical unique id of the service account
}
*/
// Example of using a google client library with identity federation
// explicit options argument would not be needed if GOOGLE_APPLICATION_CREDENTIALS contains the absolute path of a
// config file created by `gcloud iam workload-identity-pools create-cred-config`
const resourceManagerClient = new Resource(authOptions);
const [projects] = await resourceManagerClient.getProjects();
console.log('-'.repeat(80));
console.log('projects retrieved with @google-cloud/resource-manager client:');
console.log(projects.map(p => p.id));
})().catch(e => {
console.log(e);
process.exit(1);
});
import { Storage } from '@google-cloud/storage';
// Example of an AWS Lambda function using federated access to Google Cloud Storage
export const handler = async (event) => {
// Alternatively include the aws-credentials.json file in your Lambda distribution and set the GOOGLE_APPLICATION_CREDENTIALS
// environment variable with the full path of that file. Then you could simply use `new Storage()` and it will automatically find the
// credentials.
const credentials = {
// part below is only when using service account impersonation
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/[email protected]:generateAccessToken",
"service_account_impersonation": { "token_lifetime_seconds": 600 },
// part above is only when using service account impersonation
"universe_domain": "googleapis.com",
"type": "external_account",
"audience": "//iam.googleapis.com/projects/000000000000/locations/global/workloadIdentityPools/my-pool/providers/my-provider-id",
"subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"environment_id": "aws1",
"region_url": "http://169.254.169.254/latest/meta-data/placement/availability-zone",
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials",
"regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
"imdsv2_session_token_url": "http://169.254.169.254/latest/api/token"
},
"token_info_url": "https://sts.googleapis.com/v1/introspect"
};
const storage = new Storage({credentials})
const bucket = storage.bucket('my-unique-bucket-name23573957');
const [files] = await bucket.getFiles();
return {
statusCode: 200,
body: JSON.stringify({numberOfFiles: files.length}),
};
};

AWS Role to Google Cloud Federation

Setup Workload Identity Federation

See documentation at https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds

  • Create a Google Workload Identity federation pool

    GCP_PROJECT_ID="my-project-id"
    POOL_ID="my-pool-id"
    
    gcloud iam workload-identity-pools create $POOL_ID \
        --project=$GCP_PROJECT_ID \
        --location=global \
        --display-name="My Federation Pool" \
        --description="Full description"
  • Create a Workload Identity Provider for AWS. You can add attribute mappings to set the subject (identity), and any additional attributes in the Google token so you can use those in a IAM policy using principalSet://.

    When building attribute conditions and mappings keep in mind that the arn received in the AWS credentials will be something like arn:aws:sts::{account}:assumed-role/{role}/{session} where {account} is the numeric id of the AWS account, {role} is the name of the AWS Role (aka identity) being used and {session} is the name of the AWS session, the EC2 instance, AWS Lambda, or other AWS infrastructure component that assumed this role.

    By adding an attribute-condition we restrict this identity pool to only AWS Roles, and no other AWS identities (such as Users).

    If you only want to allow a single AWS Role access you could use a stricter attribute-condition of assertion.arn.startsWith('arn:aws:sts::$AWS_ACCOUNT_ID:assumed-role/$AWS_ROLE/

    You could also map a value to google.groups so you can grant privileges to a group of identities.

    PROVIDER_ID="my-provider-id"
    AWS_ACCOUNT_ID="your-aws-account-id" # 12-digit AWS account ID
    PROVIDER_DISPLAY_NAME="My AWS Provider"
    PROVIDER_DESCRIPTION="Provider for specific AWS account"
    
    gcloud iam workload-identity-pools providers create-aws $PROVIDER_ID \
        --project=$GCP_PROJECT_ID \
        --location="global" \
        --workload-identity-pool=$POOL_ID \
        --account-id=$AWS_ACCOUNT_ID \
        --display-name="$PROVIDER_DISPLAY_NAME" \
        --description="$PROVIDER_DESCRIPTION" \
        --attribute-condition="assertion.arn.startsWith('arn:aws:sts::${AWS_ACCOUNT_ID}:assumed-role/')" \
        --attribute-mapping=" \
            google.subject=assertion.arn, \
            attribute.account=assertion.account, \
            attribute.aws_role=assertion.arn.extract('assumed-role/{role}/'), \
            attribute.aws_instance=assertion.arn.extract('assumed-role/{role_and_session}').extract('/{session}') \
        "

Setup Direct Resource Access

The federated AWS identity can be used directly to access Google Cloud resources. Some scenarios might require Google Service Account impersonation which is described in the next section. For direct access follow the steps below:

  • Create a storage bucket for testing
    GCS_BUCKET_NAME=my-unique-bucket-name23573957
    
    gcloud storage buckets create gs://$GCS_BUCKET_NAME \
        --project=$GCP_PROJECT_ID \
        --location=europe-west4
  • Grant the federated AWS identity access to the storage bucket
    GCP_PROJECT_NUMBER=$(gcloud projects describe $GCP_PROJECT_ID --format=value\(projectNumber\))
    AWS_ROLE=my-aws-role
    
    gcloud storage buckets add-iam-policy-binding gs://$GCS_BUCKET_NAME \
        --role=roles/storage.objectViewer \
        --member="principalSet://iam.googleapis.com/projects/$GCP_PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/attribute.aws_role/$AWS_ROLE"
  • Generate the aws-credentials.json config file to be used in the AWS code. Remove the --enable-imdsv2 if you want to use version 1 of the AWS Instance Metadata Service. The generated file does not contain confidential information and can be added to source control of your AWS code and deployed with that application.
    gcloud iam workload-identity-pools create-cred-config \
        projects/$GCP_PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID \
        --aws \
        --enable-imdsv2 \
        --output-file=aws-credentials.json

Setup Service Account Impersonation

  • Create a storage bucket for testing
    GCS_BUCKET_NAME=my-unique-bucket-name23573957
    
    gcloud storage buckets create gs://$GCS_BUCKET_NAME \
        --project=$GCP_PROJECT_ID \
        --location=europe-west4
  • Create a Google Service Account
    GCP_SERVICE_ACCOUNT=my-service-account
    
    gcloud iam service-accounts create $GCP_SERVICE_ACCOUNT --project=$GCP_PROJECT_ID
  • Grant the Google Service account access to the storage bucket
    gcloud storage buckets add-iam-policy-binding gs://$GCS_BUCKET_NAME \
        --role=roles/storage.objectViewer \
        --member="serviceAccount:$GCP_SERVICE_ACCOUNT@$GCP_PROJECT_ID.iam.gserviceaccount.com"
  • Grant the federated AWS identity access to impersonate the Google Service account
    GCP_PROJECT_NUMBER=$(gcloud projects describe $GCP_PROJECT_ID --format=value\(projectNumber\))
    AWS_ROLE=my-aws-role
    
    gcloud iam service-accounts add-iam-policy-binding $GCP_SERVICE_ACCOUNT@$GCP_PROJECT_ID.iam.gserviceaccount.com \
        --project=$GCP_PROJECT_ID \
        --role=roles/iam.workloadIdentityUser \
        --member="principalSet://iam.googleapis.com/projects/$GCP_PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/attribute.aws_role/$AWS_ROLE"
  • Generate the aws-sa-credentials.json config file to be used in the AWS code. Remove the --enable-imdsv2 if you want to use version 1 of the AWS Instance Metadata Service. The generated file does not contain confidential information and can be added to source control of your AWS code and deployed with that application.
    gcloud iam workload-identity-pools create-cred-config \
        projects/$GCP_PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID \
        --service-account=$GCP_SERVICE_ACCOUNT@$GCP_PROJECT_ID.iam.gserviceaccount.com \
        --service-account-token-lifetime-seconds=600 \
        --aws \
        --enable-imdsv2 \
        --output-file=aws-sa-credentials.json

Other examples

This example is part of a larger serie of posts with examples of federation between different cloud environments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment