Skip to content

Instantly share code, notes, and snippets.

@wvanderdeijl
Last active June 27, 2024 06:27
Show Gist options
  • Save wvanderdeijl/28c736b55f7977baae1cf5d3f9aee377 to your computer and use it in GitHub Desktop.
Save wvanderdeijl/28c736b55f7977baae1cf5d3f9aee377 to your computer and use it in GitHub Desktop.
Showcasing Google Cloud Workload Identity Federation from AWS using raw http requests
// 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 { Sha256 } from '@aws-crypto/sha256-universal'; // or @aws-crypto/sha256-js
import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts';
import { SignatureV4 } from '@aws-sdk/signature-v4';
import axios from 'axios';
const AWS_REGION = 'eu-west-1';
const AWS_ROLE_ARN = 'arn:aws:iam::999999999999:role/my-federated-role';
const GCP_IDENTITY_PROVIDER = '//iam.googleapis.com/projects/PROJECTNR/locations/global/workloadIdentityPools/POOL-ID/providers/PROVIDER-ID';
const GCP_SERVICE_ACCOUNT = '[email protected]';
(async () => {
try {
// 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, '');
const assumeRoleResponse = await stsClient.send(new AssumeRoleCommand({ RoleArn: AWS_ROLE_ARN, RoleSessionName: sessionName }));
// create a signer utility from the AWS SDK to create a Signature Version 4 (SigV4)
const signer = new SignatureV4({
applyChecksum: false, // do not include a x-amz-content-sha256 header as google sts will throw a 400 when given unexpected headers
service: 'sts',
region: AWS_REGION,
sha256: Sha256,
credentials: {
// use the temporary security credentials we got from 'sts assume-role'
accessKeyId: assumeRoleResponse.Credentials.AccessKeyId,
secretAccessKey: assumeRoleResponse.Credentials.SecretAccessKey,
sessionToken: assumeRoleResponse.Credentials.SessionToken,
},
});
// Build a signed request to AWS STS to get our own identity (which is the assumed IAM Role), but do not execute this request.
// Instead, we are going to give this fully signed request to google so it can execute this request on our behalf as a means to
// prove our identity.
// Depending on your programming language and AWS SDK there might be better ways to build a SigV4 signed request.
const signedRequest = await signer.sign({
method: 'POST',
protocol: 'https', // not used for signing
hostname: `sts.${AWS_REGION}.amazonaws.com`, // not used for signing
path: '/',
query: {
Action: 'GetCallerIdentity',
Version: '2011-06-15',
},
headers: {
'host': `sts.${AWS_REGION}.amazonaws.com`,
'x-goog-cloud-target-resource': GCP_IDENTITY_PROVIDER,
},
});
console.log('signed request:');
console.log(signedRequest);
/*
{
method: 'POST',
protocol: 'https',
hostname: 'sts.eu-west-1.amazonaws.com',
path: '/',
headers: {
host: 'sts.eu-west-1.amazonaws.com',
'x-goog-cloud-target-resource': '//iam.googleapis.com/....',
'x-amz-date': '20210531T163536Z',
'x-amz-security-token': 'IQoJ....1h/u',
authorization: 'AWS4-HMAC-SHA256 Credential=ASIA....TYOH/20210531/eu-west-1/sts/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token;x-goog-cloud-target-resource, Signature=187e....9d71'
},
query: { Action: 'GetCallerIdentity', Version: '2011-06-15' }
}
*/
// Convert the signed request to a format as google wants to receive it
// see https://cloud.google.com/iam/docs/access-resources-aws#exchange-token
const getCallerIdentityToken = {
// https://sts.${AWS_REGION}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15
url: `${signedRequest.protocol}://${signedRequest.hostname}${signedRequest.path}?${new URLSearchParams(
signedRequest.query as Record<string, string>,
).toString()}`,
method: signedRequest.method,
// google wants to receive the headers as an array of objects with a key and value property
headers: Object.entries(signedRequest.headers).map(([key, value]) => ({ key, value })),
};
console.log('-'.repeat(80));
console.log('getCallerIdentityToken:');
console.log(getCallerIdentityToken);
/*
{
url: 'https://sts.eu-west-1.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15',
method: 'POST',
headers: [
{ key: 'host', value: 'sts.eu-west-1.amazonaws.com' },
{ key: 'x-goog-cloud-target-resource', value: '//iam.googleapis.com/....' },
{ key: 'x-amz-date', value: '20210531T163817Z' },
{ key: 'x-amz-security-token', value: 'IQoJ....1h/u' },
{ key: 'authorization', value: 'AWS4-HMAC-SHA256 Credential=ASIA....TYOH/20210531/eu-west-1/sts/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token;x-goog-cloud-target-resource, Signature=187e....9d71' }
]
}
*/
// Invoke google sts service to get a federated access token.
const federated = await axios.post<FederatedToken>('https://sts.googleapis.com/v1/token', {
audience: GCP_IDENTITY_PROVIDER,
grantType: 'urn:ietf:params:oauth:grant-type:token-exchange',
requestedTokenType: 'urn:ietf:params:oauth:token-type:access_token',
scope: 'https://www.googleapis.com/auth/cloud-platform',
subjectTokenType: 'urn:ietf:params:aws:token-type:aws4_request',
// send the aws sts request as an uri-encoded JSON object
subjectToken: encodeURIComponent(JSON.stringify(getCallerIdentityToken)),
});
console.log('-'.repeat(80));
console.log('federated access token:');
console.log(federated.data);
/*
{
access_token: 'ya29.d.Kt0E....iHPg',
issued_token_type: 'urn:ietf:params:oauth:token-type:access_token',
token_type: 'Bearer',
expires_in: 900
}
*/
// Sign a custom token with the federated service account
// see https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signJwt
// The token will be signed by the service account and the associated public key that is needed to validate the token can be
// retrieved in several formats from the following endpoints:
// - RSA public key wrapped in an X.509 v3 certificate: https://www.googleapis.com/service_accounts/v1/metadata/x509/{ACCOUNT_EMAIL}
// - Raw key in JSON format: https://www.googleapis.com/service_accounts/v1/metadata/raw/{ACCOUNT_EMAIL}
// - JSON Web Key (JWK): https://www.googleapis.com/service_accounts/v1/metadata/jwk/{ACCOUNT_EMAIL}
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 axios.post<{ keyId: string; signedJwt: string }>(
`https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${GCP_SERVICE_ACCOUNT}:signJwt`,
{
payload: JSON.stringify(customTokenPayload),
},
{ headers: { authorization: `Bearer ${federated.data.access_token}` } },
);
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": 1622480215,
"exp": 1622480515
}
*/
// 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 axios.post<{ token: string }>(
`https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${GCP_SERVICE_ACCOUNT}:generateIdToken`,
{
audience: 'https://example.com',
includeEmail: false,
},
{ headers: { authorization: `Bearer ${federated.data.access_token}` } },
);
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": 1622483939,
"iat": 1622480339,
"iss": "https://accounts.google.com",
"sub": "1000....6293" // numerical unique id of the service account
}
*/
// Only a limited number of Google Cloud APIs support federated tokens. Call `generateAccessToken` to get a "service account
// access token" which can be used to invoke any Google Cloud API this service account has access to.
// see https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken
// But if you want to invoke Google API's, it is much more convenient to use the `google-auth-library` for your programming
// language as that will handle all the fetching (and refreshing) of tokens.
const serviceAccountToken = await axios.post<{ accessToken: string; expireTime: string }>(
`https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${GCP_SERVICE_ACCOUNT}:generateAccessToken`,
{
// see https://developers.google.com/identity/protocols/oauth2/scopes
scope: ['https://www.googleapis.com/auth/cloud-platform'],
},
{ headers: { authorization: `Bearer ${federated.data.access_token}` } },
);
// Example of invoking a google cloud api with the service account access token
const projects = await axios.get('https://cloudresourcemanager.googleapis.com/v1/projects', {
headers: { authorization: `Bearer ${serviceAccountToken.data.accessToken}` },
});
console.log('-'.repeat(80));
console.log('GET projects result (with service account access token):');
console.log(projects.data);
} catch (e) {
if (axios.isAxiosError(e)) {
console.log('ERROR RESPONSE', e.response?.data);
}
throw e;
}
})().catch(e => {
console.log(e);
process.exit(1);
});
// see https://cloud.google.com/iam/docs/reference/sts/rest/v1/TopLevel/token#response-body
interface FederatedToken {
access_token: string;
issued_token_type: 'urn:ietf:params:oauth:token-type:access_token'; // same as requestedTokenType from token request
token_type: 'Bearer';
expires_in: number;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment