Skip to content

Instantly share code, notes, and snippets.

@wvanderdeijl
Created May 31, 2021 20:05
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
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 = 'ID@PROJECT.iam.gserviceaccount.com';
(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: 'user@example.com',
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": "user@example.com",
"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);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment