Skip to content

Commit c4e906d

Browse files
authored
Add proxy support (#218)
1 parent 246692c commit c4e906d

File tree

9 files changed

+253
-270
lines changed

9 files changed

+253
-270
lines changed

dist/main/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

Lines changed: 148 additions & 135 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,23 @@
2424
"license": "Apache-2.0",
2525
"dependencies": {
2626
"@actions/core": "^1.9.1",
27+
"@actions/http-client": "^2.0.1",
2728
"@google-github-actions/actions-utils": "^0.4.2"
2829
},
2930
"devDependencies": {
3031
"@types/chai": "^4.3.3",
3132
"@types/mocha": "^9.1.1",
32-
"@types/node": "^18.7.13",
33-
"@typescript-eslint/eslint-plugin": "^5.34.0",
34-
"@typescript-eslint/parser": "^5.34.0",
33+
"@types/node": "^18.7.14",
34+
"@typescript-eslint/eslint-plugin": "^5.36.1",
35+
"@typescript-eslint/parser": "^5.36.1",
3536
"@vercel/ncc": "^0.34.0",
3637
"chai": "^4.3.6",
37-
"eslint": "^8.22.0",
38+
"eslint": "^8.23.0",
3839
"eslint-config-prettier": "^8.5.0",
3940
"eslint-plugin-prettier": "^4.2.1",
4041
"mocha": "^10.0.0",
4142
"prettier": "^2.7.1",
4243
"ts-node": "^10.9.1",
43-
"typescript": "^4.7.4"
44+
"typescript": "^4.8.2"
4445
}
4546
}

src/base.ts

Lines changed: 52 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

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

16+
// userAgent is the default user agent.
17+
const userAgent = `google-github-actions:auth/${appVersion}`;
18+
19+
/**
20+
* BaseClient is the default HTTP client for interacting with the IAM
21+
* credentials API.
22+
*/
1623
export class BaseClient {
1724
/**
18-
* request is a high-level helper that returns a promise from the executed
19-
* request.
25+
* client is the HTTP client.
2026
*/
21-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
22-
static request(opts: RequestOptions, data?: any): Promise<string> {
23-
if (!opts.headers) {
24-
opts.headers = {};
25-
}
26-
27-
if (!opts.headers['User-Agent']) {
28-
opts.headers['User-Agent'] = `google-github-actions:auth/${appVersion}`;
29-
}
30-
31-
return new Promise((resolve, reject) => {
32-
const req = https.request(opts, (res) => {
33-
res.setEncoding('utf8');
34-
35-
let body = '';
36-
res.on('data', (data) => {
37-
body += data;
38-
});
39-
40-
res.on('end', () => {
41-
if (res.statusCode && res.statusCode >= 400) {
42-
reject(body);
43-
} else {
44-
resolve(body);
45-
}
46-
});
47-
});
48-
49-
req.on('error', (err) => {
50-
reject(err);
51-
});
52-
53-
if (data != null) {
54-
req.write(data);
55-
}
27+
protected readonly client: HttpClient;
5628

57-
req.end();
58-
});
29+
constructor() {
30+
this.client = new HttpClient(userAgent);
5931
}
6032

6133
/**
6234
* googleIDToken generates a Google Cloud ID token for the provided
6335
* service account email or unique id.
6436
*/
65-
static async googleIDToken(
37+
async googleIDToken(
6638
token: string,
6739
{ serviceAccount, audience, delegates, includeEmail }: GoogleIDTokenParameters,
6840
): Promise<GoogleIDTokenResponse> {
69-
const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`;
70-
const tokenURL = new URL(
71-
`https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateIdToken`,
72-
);
41+
const pth = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:generateIdToken`;
7342

7443
const data = {
7544
delegates: delegates,
7645
audience: audience,
7746
includeEmail: includeEmail,
7847
};
7948

80-
const opts = {
81-
hostname: tokenURL.hostname,
82-
port: tokenURL.port,
83-
path: tokenURL.pathname + tokenURL.search,
84-
method: 'POST',
85-
headers: {
86-
'Authorization': `Bearer ${token}`,
87-
'Accept': 'application/json',
88-
'Content-Type': 'application/json',
89-
},
49+
const headers = {
50+
'Authorization': `Bearer ${token}`,
51+
'Accept': 'application/json',
52+
'Content-Type': 'application/json',
9053
};
9154

9255
try {
93-
const resp = await BaseClient.request(opts, JSON.stringify(data));
94-
const parsed = JSON.parse(resp);
56+
const resp = await this.client.request('POST', pth, JSON.stringify(data), headers);
57+
const body = await resp.readBody();
58+
const statusCode = resp.message.statusCode || 500;
59+
if (statusCode >= 400) {
60+
throw new Error(`(${statusCode}) ${body}`);
61+
}
62+
const parsed = JSON.parse(body);
9563
return {
9664
token: parsed['token'],
9765
};
@@ -104,14 +72,11 @@ export class BaseClient {
10472
* googleAccessToken generates a Google Cloud access token for the provided
10573
* service account email or unique id.
10674
*/
107-
static async googleAccessToken(
75+
async googleAccessToken(
10876
token: string,
10977
{ serviceAccount, delegates, scopes, lifetime }: GoogleAccessTokenParameters,
11078
): Promise<GoogleAccessTokenResponse> {
111-
const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`;
112-
const tokenURL = new URL(
113-
`https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateAccessToken`,
114-
);
79+
const pth = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:generateAccessToken`;
11580

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

128-
const opts = {
129-
hostname: tokenURL.hostname,
130-
port: tokenURL.port,
131-
path: tokenURL.pathname + tokenURL.search,
132-
method: 'POST',
133-
headers: {
134-
'Authorization': `Bearer ${token}`,
135-
'Accept': 'application/json',
136-
'Content-Type': 'application/json',
137-
},
93+
const headers = {
94+
'Authorization': `Bearer ${token}`,
95+
'Accept': 'application/json',
96+
'Content-Type': 'application/json',
13897
};
13998

14099
try {
141-
const resp = await BaseClient.request(opts, JSON.stringify(data));
142-
const parsed = JSON.parse(resp);
100+
const resp = await this.client.request('POST', pth, JSON.stringify(data), headers);
101+
const body = await resp.readBody();
102+
const statusCode = resp.message.statusCode || 500;
103+
if (statusCode >= 400) {
104+
throw new Error(`(${statusCode}) ${body}`);
105+
}
106+
const parsed = JSON.parse(body);
143107
return {
144108
accessToken: parsed['accessToken'],
145109
expiration: parsed['expireTime'],
@@ -155,27 +119,26 @@ export class BaseClient {
155119
*
156120
* @param assertion A signed JWT.
157121
*/
158-
static async googleOAuthToken(assertion: string): Promise<GoogleAccessTokenResponse> {
159-
const tokenURL = new URL('https://oauth2.googleapis.com/token');
160-
161-
const opts = {
162-
hostname: tokenURL.hostname,
163-
port: tokenURL.port,
164-
path: tokenURL.pathname + tokenURL.search,
165-
method: 'POST',
166-
headers: {
167-
'Accept': 'application/json',
168-
'Content-Type': 'application/x-www-form-urlencoded',
169-
},
122+
async googleOAuthToken(assertion: string): Promise<GoogleAccessTokenResponse> {
123+
const pth = `https://oauth2.googleapis.com/token`;
124+
125+
const headers = {
126+
'Accept': 'application/json',
127+
'Content-Type': 'application/x-www-form-urlencoded',
170128
};
171129

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

176134
try {
177-
const resp = await BaseClient.request(opts, data.toString());
178-
const parsed = JSON.parse(resp);
135+
const resp = await this.client.request('POST', pth, data.toString(), headers);
136+
const body = await resp.readBody();
137+
const statusCode = resp.message.statusCode || 500;
138+
if (statusCode >= 400) {
139+
throw new Error(`(${statusCode}) ${body}`);
140+
}
141+
const parsed = JSON.parse(body);
179142

180143
// Normalize the expiration to be a timestamp like the iamcredentials API.
181144
// This API returns the number of seconds until expiration, so convert

src/client/auth_client.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ export interface AuthClient {
99
getProjectID(): Promise<string>;
1010
getServiceAccount(): Promise<string>;
1111
createCredentialsFile(outputDir: string): Promise<string>;
12+
13+
/**
14+
* Provided by BaseClient.
15+
*/
16+
googleIDToken(token: string, params: GoogleIDTokenParameters): Promise<GoogleIDTokenResponse>;
17+
googleAccessToken(
18+
token: string,
19+
params: GoogleAccessTokenParameters,
20+
): Promise<GoogleAccessTokenResponse>;
21+
googleOAuthToken(assertion: string): Promise<GoogleAccessTokenResponse>;
1222
}
1323

1424
/**

src/client/credentials_json_client.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from '@google-github-actions/actions-utils';
1111

1212
import { AuthClient } from './auth_client';
13+
import { BaseClient } from '../base';
1314

1415
/**
1516
* Available options to create the CredentialsJSONClient.
@@ -27,11 +28,13 @@ interface CredentialsJSONClientOptions {
2728
* CredentialsJSONClient is a client that accepts a service account key JSON
2829
* credential.
2930
*/
30-
export class CredentialsJSONClient implements AuthClient {
31+
export class CredentialsJSONClient extends BaseClient implements AuthClient {
3132
readonly #projectID: string;
3233
readonly #credentials: ServiceAccountKey;
3334

3435
constructor(opts: CredentialsJSONClientOptions) {
36+
super();
37+
3538
const credentials = parseCredential(opts.credentialsJSON);
3639
if (!isServiceAccountKey(credentials)) {
3740
throw new Error(`Provided credential is not a valid service account key JSON`);

src/client/workload_identity_client.ts

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ interface WorkloadIdentityClientOptions {
3535
* WorkloadIdentityClient is a client that uses the GitHub Actions runtime to
3636
* authentication via Workload Identity.
3737
*/
38-
export class WorkloadIdentityClient implements AuthClient {
38+
export class WorkloadIdentityClient extends BaseClient implements AuthClient {
3939
readonly #projectID: string;
4040
readonly #providerID: string;
4141
readonly #serviceAccount: string;
@@ -46,6 +46,8 @@ export class WorkloadIdentityClient implements AuthClient {
4646
readonly #oidcTokenRequestToken: string;
4747

4848
constructor(opts: WorkloadIdentityClientOptions) {
49+
super();
50+
4951
this.#providerID = opts.providerID;
5052
this.#serviceAccount = opts.serviceAccount;
5153
this.#token = opts.token;
@@ -85,7 +87,7 @@ export class WorkloadIdentityClient implements AuthClient {
8587
* OIDC token and Workload Identity Provider.
8688
*/
8789
async getAuthToken(): Promise<string> {
88-
const stsURL = new URL('https://sts.googleapis.com/v1/token');
90+
const pth = `https://sts.googleapis.com/v1/token`;
8991

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

99-
const opts = {
100-
hostname: stsURL.hostname,
101-
port: stsURL.port,
102-
path: stsURL.pathname + stsURL.search,
103-
method: 'POST',
104-
headers: {
105-
'Accept': 'application/json',
106-
'Content-Type': 'application/json',
107-
},
101+
const headers = {
102+
'Accept': 'application/json',
103+
'Content-Type': 'application/json',
108104
};
109105

110106
try {
111-
const resp = await BaseClient.request(opts, JSON.stringify(data));
112-
const parsed = JSON.parse(resp);
107+
const resp = await this.client.request('POST', pth, JSON.stringify(data), headers);
108+
const body = await resp.readBody();
109+
const statusCode = resp.message.statusCode || 500;
110+
if (statusCode >= 400) {
111+
throw new Error(`(${statusCode}) ${body}`);
112+
}
113+
const parsed = JSON.parse(body);
113114
return parsed['access_token'];
114115
} catch (err) {
115116
throw new Error(
@@ -129,9 +130,7 @@ export class WorkloadIdentityClient implements AuthClient {
129130
const serviceAccount = await this.getServiceAccount();
130131
const federatedToken = await this.getAuthToken();
131132

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

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

143-
const opts = {
144-
hostname: signJWTURL.hostname,
145-
port: signJWTURL.port,
146-
path: signJWTURL.pathname + signJWTURL.search,
147-
method: 'POST',
148-
headers: {
149-
'Accept': 'application/json',
150-
'Authorization': `Bearer ${federatedToken}`,
151-
'Content-Type': 'application/json',
152-
},
142+
const headers = {
143+
'Accept': 'application/json',
144+
'Authorization': `Bearer ${federatedToken}`,
145+
'Content-Type': 'application/json',
153146
};
154147

155148
try {
156-
const resp = await BaseClient.request(opts, JSON.stringify(data));
157-
const parsed = JSON.parse(resp);
149+
const resp = await this.client.request('POST', pth, JSON.stringify(data), headers);
150+
const body = await resp.readBody();
151+
const statusCode = resp.message.statusCode || 500;
152+
if (statusCode >= 400) {
153+
throw new Error(`(${statusCode}) ${body}`);
154+
}
155+
const parsed = JSON.parse(body);
158156
return parsed['signedJwt'];
159157
} catch (err) {
160158
throw new Error(`Failed to sign JWT using ${serviceAccount}: ${err}`);

0 commit comments

Comments
 (0)