import https, { RequestOptions } from 'https'; import { URL } from 'url'; /** * GoogleFederatedTokenParameters are the parameters to generate a Federated * Identity Token as described in: * * https://cloud.google.com/iam/docs/access-resources-oidc#exchange-token * * @param providerID Full path (including project, location, etc) to the Google * Cloud Workload Identity Provider. * @param token OIDC token to exchange for a Google Cloud federated token. */ interface GoogleFederatedTokenParameters { providerID: string; token: string; } /** * GoogleAccessTokenParameters are the parameters to generate a Google Cloud * access token as described in: * * https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken * * @param token OAuth token or Federated access token with permissions to call * the API. * @param serviceAccount Email address or unique identifier of the service * account. * @param delegates Optional sequence of service accounts in the delegation * chain. * @param lifetime Optional validity period as a duration. */ interface GoogleAccessTokenParameters { token: string; serviceAccount: string; delegates?: Array; scopes?: Array; lifetime?: string; } /** * GoogleAccessTokenResponse is the response from generating an access token. * * @param accessToken OAuth 2.0 access token. * @param expiration A timestamp in RFC3339 UTC "Zulu" format when the token * expires. */ interface GoogleAccessTokenResponse { accessToken: string; expiration: string; } /** * GoogleIDTokenParameters are the parameters to generate a Google Cloud * ID token as described in: * * https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateIdToken * * @param token OAuth token or Federated access token with permissions to call * the API. * @param serviceAccount Email address or unique identifier of the service * account. * @param audience The audience for the token. * @param delegates Optional sequence of service accounts in the delegation * chain. */ interface GoogleIDTokenParameters { token: string; serviceAccount: string; audience: string; delegates?: Array; includeEmail?: boolean; } /** * GoogleIDTokenResponse is the response from generating an ID token. * * @param token ID token. * expires. */ interface GoogleIDTokenResponse { token: string; } export class Client { /** * request is a high-level helper that returns a promise from the executed * request. */ static request(opts: RequestOptions, data?: any): Promise { return new Promise((resolve, reject) => { const req = https.request(opts, (res) => { res.setEncoding('utf8'); let body = ''; res.on('data', (data) => { body += data; }); res.on('end', () => { if (res.statusCode && res.statusCode >= 400) { reject(body); } else { resolve(body); } }); }); req.on('error', (err) => { reject(err); }); if (data != null) { req.write(data); } req.end(); }); } /** * googleFederatedToken generates a Google Cloud federated token using the * provided OIDC token and Workload Identity Provider. */ static async googleFederatedToken({ providerID, token, }: GoogleFederatedTokenParameters): Promise { const stsURL = new URL('https://sts.googleapis.com/v1/token'); const data = { audience: '//iam.googleapis.com/' + providerID, 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:oauth:token-type:jwt', subjectToken: token, }; const opts = { hostname: stsURL.hostname, port: stsURL.port, path: stsURL.pathname + stsURL.search, method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, }; try { const resp = await Client.request(opts, JSON.stringify(data)); const parsed = JSON.parse(resp); return parsed['access_token']; } catch (err) { throw new Error(`failed to generate Google Cloud federated token for ${providerID}: ${err}`); } } /** * googleAccessToken generates a Google Cloud access token for the provided * service account email or unique id. */ static async googleAccessToken({ token, serviceAccount, delegates, scopes, lifetime, }: GoogleAccessTokenParameters): Promise { const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`; const tokenURL = new URL( `https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateAccessToken`, ); const data = { delegates: delegates, lifetime: lifetime, scope: scopes, }; const opts = { hostname: tokenURL.hostname, port: tokenURL.port, path: tokenURL.pathname + tokenURL.search, method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json', 'Content-Type': 'application/json', }, }; try { const resp = await Client.request(opts, JSON.stringify(data)); const parsed = JSON.parse(resp); return { accessToken: parsed['accessToken'], expiration: parsed['expireTime'], }; } catch (err) { throw new Error(`failed to generate Google Cloud access token for ${serviceAccount}: ${err}`); } } /** * googleIDToken generates a Google Cloud ID token for the provided * service account email or unique id. */ static async googleIDToken({ token, serviceAccount, audience, delegates, includeEmail, }: GoogleIDTokenParameters): Promise { const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`; const tokenURL = new URL( `https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateIdToken`, ); const data = { delegates: delegates, audience: audience, includeEmail: includeEmail, }; const opts = { hostname: tokenURL.hostname, port: tokenURL.port, path: tokenURL.pathname + tokenURL.search, method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json', 'Content-Type': 'application/json', }, }; try { const resp = await Client.request(opts, JSON.stringify(data)); const parsed = JSON.parse(resp); return { token: parsed['token'], }; } catch (err) { throw new Error(`failed to generate Google Cloud ID token for ${serviceAccount}: ${err}`); } } }