Created
May 31, 2021 20:05
-
-
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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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