Skip to content

Commit

Permalink
Add proxy support
Browse files Browse the repository at this point in the history
  • Loading branch information
sethvargo committed Aug 30, 2022
1 parent 246692c commit d101e59
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 270 deletions.
2 changes: 1 addition & 1 deletion dist/main/index.js

Large diffs are not rendered by default.

283 changes: 148 additions & 135 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions package.json
Expand Up @@ -24,22 +24,23 @@
"license": "Apache-2.0",
"dependencies": {
"@actions/core": "^1.9.1",
"@actions/http-client": "^2.0.1",
"@google-github-actions/actions-utils": "^0.4.2"
},
"devDependencies": {
"@types/chai": "^4.3.3",
"@types/mocha": "^9.1.1",
"@types/node": "^18.7.13",
"@typescript-eslint/eslint-plugin": "^5.34.0",
"@typescript-eslint/parser": "^5.34.0",
"@types/node": "^18.7.14",
"@typescript-eslint/eslint-plugin": "^5.36.1",
"@typescript-eslint/parser": "^5.36.1",
"@vercel/ncc": "^0.34.0",
"chai": "^4.3.6",
"eslint": "^8.22.0",
"eslint": "^8.23.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"mocha": "^10.0.0",
"prettier": "^2.7.1",
"ts-node": "^10.9.1",
"typescript": "^4.7.4"
"typescript": "^4.8.2"
}
}
141 changes: 52 additions & 89 deletions src/base.ts
@@ -1,7 +1,7 @@
'use strict';

import https, { RequestOptions } from 'https';
import { URL, URLSearchParams } from 'url';
import { HttpClient } from '@actions/http-client';
import { URLSearchParams } from 'url';
import {
GoogleAccessTokenParameters,
GoogleAccessTokenResponse,
Expand All @@ -13,85 +13,53 @@ import {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { version: appVersion } = require('../package.json');

// userAgent is the default user agent.
const userAgent = `google-github-actions:auth/${appVersion}`;

/**
* BaseClient is the default HTTP client for interacting with the IAM
* credentials API.
*/
export class BaseClient {
/**
* request is a high-level helper that returns a promise from the executed
* request.
* client is the HTTP client.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
static request(opts: RequestOptions, data?: any): Promise<string> {
if (!opts.headers) {
opts.headers = {};
}

if (!opts.headers['User-Agent']) {
opts.headers['User-Agent'] = `google-github-actions:auth/${appVersion}`;
}

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);
}
protected readonly client: HttpClient;

req.end();
});
constructor() {
this.client = new HttpClient(userAgent);
}

/**
* googleIDToken generates a Google Cloud ID token for the provided
* service account email or unique id.
*/
static async googleIDToken(
async googleIDToken(
token: string,
{ serviceAccount, audience, delegates, includeEmail }: GoogleIDTokenParameters,
): Promise<GoogleIDTokenResponse> {
const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`;
const tokenURL = new URL(
`https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateIdToken`,
);
const pth = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}: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',
},
const headers = {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
};

try {
const resp = await BaseClient.request(opts, JSON.stringify(data));
const parsed = JSON.parse(resp);
const resp = await this.client.request('POST', pth, JSON.stringify(data), headers);
const body = await resp.readBody();
const statusCode = resp.message.statusCode || 500;
if (statusCode >= 400) {
throw new Error(`(${statusCode}) ${body}`);
}
const parsed = JSON.parse(body);
return {
token: parsed['token'],
};
Expand All @@ -104,14 +72,11 @@ export class BaseClient {
* googleAccessToken generates a Google Cloud access token for the provided
* service account email or unique id.
*/
static async googleAccessToken(
async googleAccessToken(
token: string,
{ serviceAccount, delegates, scopes, lifetime }: GoogleAccessTokenParameters,
): Promise<GoogleAccessTokenResponse> {
const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`;
const tokenURL = new URL(
`https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateAccessToken`,
);
const pth = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:generateAccessToken`;

const data: Record<string, string | Array<string>> = {};
if (delegates && delegates.length > 0) {
Expand All @@ -125,21 +90,20 @@ export class BaseClient {
data.lifetime = `${lifetime}s`;
}

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',
},
const headers = {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
};

try {
const resp = await BaseClient.request(opts, JSON.stringify(data));
const parsed = JSON.parse(resp);
const resp = await this.client.request('POST', pth, JSON.stringify(data), headers);
const body = await resp.readBody();
const statusCode = resp.message.statusCode || 500;
if (statusCode >= 400) {
throw new Error(`(${statusCode}) ${body}`);
}
const parsed = JSON.parse(body);
return {
accessToken: parsed['accessToken'],
expiration: parsed['expireTime'],
Expand All @@ -155,27 +119,26 @@ export class BaseClient {
*
* @param assertion A signed JWT.
*/
static async googleOAuthToken(assertion: string): Promise<GoogleAccessTokenResponse> {
const tokenURL = new URL('https://oauth2.googleapis.com/token');

const opts = {
hostname: tokenURL.hostname,
port: tokenURL.port,
path: tokenURL.pathname + tokenURL.search,
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
async googleOAuthToken(assertion: string): Promise<GoogleAccessTokenResponse> {
const pth = `https://oauth2.googleapis.com/token`;

const headers = {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
};

const data = new URLSearchParams();
data.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
data.append('assertion', assertion);

try {
const resp = await BaseClient.request(opts, data.toString());
const parsed = JSON.parse(resp);
const resp = await this.client.request('POST', pth, data.toString(), headers);
const body = await resp.readBody();
const statusCode = resp.message.statusCode || 500;
if (statusCode >= 400) {
throw new Error(`(${statusCode}) ${body}`);
}
const parsed = JSON.parse(body);

// Normalize the expiration to be a timestamp like the iamcredentials API.
// This API returns the number of seconds until expiration, so convert
Expand Down
10 changes: 10 additions & 0 deletions src/client/auth_client.ts
Expand Up @@ -9,6 +9,16 @@ export interface AuthClient {
getProjectID(): Promise<string>;
getServiceAccount(): Promise<string>;
createCredentialsFile(outputDir: string): Promise<string>;

/**
* Provided by BaseClient.
*/
googleIDToken(token: string, params: GoogleIDTokenParameters): Promise<GoogleIDTokenResponse>;
googleAccessToken(
token: string,
params: GoogleAccessTokenParameters,
): Promise<GoogleAccessTokenResponse>;
googleOAuthToken(assertion: string): Promise<GoogleAccessTokenResponse>;
}

/**
Expand Down
5 changes: 4 additions & 1 deletion src/client/credentials_json_client.ts
Expand Up @@ -10,6 +10,7 @@ import {
} from '@google-github-actions/actions-utils';

import { AuthClient } from './auth_client';
import { BaseClient } from '../base';

/**
* Available options to create the CredentialsJSONClient.
Expand All @@ -27,11 +28,13 @@ interface CredentialsJSONClientOptions {
* CredentialsJSONClient is a client that accepts a service account key JSON
* credential.
*/
export class CredentialsJSONClient implements AuthClient {
export class CredentialsJSONClient extends BaseClient implements AuthClient {
readonly #projectID: string;
readonly #credentials: ServiceAccountKey;

constructor(opts: CredentialsJSONClientOptions) {
super();

const credentials = parseCredential(opts.credentialsJSON);
if (!isServiceAccountKey(credentials)) {
throw new Error(`Provided credential is not a valid service account key JSON`);
Expand Down
54 changes: 26 additions & 28 deletions src/client/workload_identity_client.ts
Expand Up @@ -35,7 +35,7 @@ interface WorkloadIdentityClientOptions {
* WorkloadIdentityClient is a client that uses the GitHub Actions runtime to
* authentication via Workload Identity.
*/
export class WorkloadIdentityClient implements AuthClient {
export class WorkloadIdentityClient extends BaseClient implements AuthClient {
readonly #projectID: string;
readonly #providerID: string;
readonly #serviceAccount: string;
Expand All @@ -46,6 +46,8 @@ export class WorkloadIdentityClient implements AuthClient {
readonly #oidcTokenRequestToken: string;

constructor(opts: WorkloadIdentityClientOptions) {
super();

this.#providerID = opts.providerID;
this.#serviceAccount = opts.serviceAccount;
this.#token = opts.token;
Expand Down Expand Up @@ -85,7 +87,7 @@ export class WorkloadIdentityClient implements AuthClient {
* OIDC token and Workload Identity Provider.
*/
async getAuthToken(): Promise<string> {
const stsURL = new URL('https://sts.googleapis.com/v1/token');
const pth = `https://sts.googleapis.com/v1/token`;

const data = {
audience: '//iam.googleapis.com/' + this.#providerID,
Expand All @@ -96,20 +98,19 @@ export class WorkloadIdentityClient implements AuthClient {
subjectToken: this.#token,
};

const opts = {
hostname: stsURL.hostname,
port: stsURL.port,
path: stsURL.pathname + stsURL.search,
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
};

try {
const resp = await BaseClient.request(opts, JSON.stringify(data));
const parsed = JSON.parse(resp);
const resp = await this.client.request('POST', pth, JSON.stringify(data), headers);
const body = await resp.readBody();
const statusCode = resp.message.statusCode || 500;
if (statusCode >= 400) {
throw new Error(`(${statusCode}) ${body}`);
}
const parsed = JSON.parse(body);
return parsed['access_token'];
} catch (err) {
throw new Error(
Expand All @@ -129,9 +130,7 @@ export class WorkloadIdentityClient implements AuthClient {
const serviceAccount = await this.getServiceAccount();
const federatedToken = await this.getAuthToken();

const signJWTURL = new URL(
`https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:signJwt`,
);
const pth = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:signJwt`;

const data: Record<string, string | Array<string>> = {
payload: unsignedJWT,
Expand All @@ -140,21 +139,20 @@ export class WorkloadIdentityClient implements AuthClient {
data.delegates = delegates;
}

const opts = {
hostname: signJWTURL.hostname,
port: signJWTURL.port,
path: signJWTURL.pathname + signJWTURL.search,
method: 'POST',
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${federatedToken}`,
'Content-Type': 'application/json',
},
const headers = {
'Accept': 'application/json',
'Authorization': `Bearer ${federatedToken}`,
'Content-Type': 'application/json',
};

try {
const resp = await BaseClient.request(opts, JSON.stringify(data));
const parsed = JSON.parse(resp);
const resp = await this.client.request('POST', pth, JSON.stringify(data), headers);
const body = await resp.readBody();
const statusCode = resp.message.statusCode || 500;
if (statusCode >= 400) {
throw new Error(`(${statusCode}) ${body}`);
}
const parsed = JSON.parse(body);
return parsed['signedJwt'];
} catch (err) {
throw new Error(`Failed to sign JWT using ${serviceAccount}: ${err}`);
Expand Down

0 comments on commit d101e59

Please sign in to comment.