Skip to content

Commit a76a967

Browse files
committed
refactor!: Request Revamp
1 parent 8f15e14 commit a76a967

39 files changed

+672
-638
lines changed

.readme-partials.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1185,7 +1185,7 @@ body: |-
11851185
11861186
// Get impersonated credentials:
11871187
const authHeaders = await targetClient.getRequestHeaders();
1188-
// Do something with `authHeaders.Authorization`.
1188+
// Do something with `authHeaders.get('Authorization')`.
11891189
11901190
// Use impersonated credentials:
11911191
const url = 'https://www.googleapis.com/storage/v1/b?project=anotherProjectID'

browser-test/test.oauth2.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ const FEDERATED_SIGNON_JWK_CERTS = [
4646
},
4747
];
4848
const FEDERATED_SIGNON_JWK_CERTS_AXIOS_RESPONSE = {
49-
headers: {
49+
headers: new Headers({
5050
'cache-control':
5151
'cache-control: public, max-age=24000, must-revalidate, no-transform',
52-
},
52+
}),
5353
data: {keys: FEDERATED_SIGNON_JWK_CERTS},
5454
};
5555

package.json

+5-6
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
"dependencies": {
2020
"base64-js": "^1.3.0",
2121
"ecdsa-sig-formatter": "^1.0.11",
22-
"gaxios": "^7.0.0-rc.1",
23-
"gcp-metadata": "^6.1.0",
24-
"gtoken": "^7.0.0",
22+
"gaxios": "^7.0.0-rc.4",
23+
"gcp-metadata": "^7.0.0-rc.1",
24+
"gtoken": "^8.0.0-rc.1",
2525
"jws": "^4.0.0"
2626
},
2727
"devDependencies": {
@@ -54,7 +54,7 @@
5454
"mocha": "^9.2.2",
5555
"mv": "^2.1.1",
5656
"ncp": "^2.0.0",
57-
"nock": "^13.0.0",
57+
"nock": "^14.0.1",
5858
"null-loader": "^4.0.0",
5959
"puppeteer": "^24.0.0",
6060
"sinon": "^18.0.0",
@@ -84,8 +84,7 @@
8484
"browser-test": "karma start",
8585
"docs-test": "linkinator docs",
8686
"predocs-test": "npm run docs",
87-
"prelint": "cd samples; npm link ../; npm install",
88-
"precompile": "gts clean"
87+
"prelint": "cd samples; npm link ../; npm install"
8988
},
9089
"license": "Apache-2.0"
9190
}

src/auth/authclient.ts

+43-18
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ export interface CredentialsClient {
163163
* { Authorization: 'Bearer <access_token_value>' }
164164
* @param url The URI being authorized.
165165
*/
166-
getRequestHeaders(url?: string): Promise<Headers>;
166+
getRequestHeaders(url?: string | URL): Promise<Headers>;
167167

168168
/**
169169
* Provides an alternative Gaxios request implementation with auth credentials
@@ -251,10 +251,13 @@ export abstract class AuthClient
251251
* resolves with authorization header fields.
252252
*
253253
* The result has the form:
254-
* { Authorization: 'Bearer <access_token_value>' }
254+
* ```ts
255+
* new Headers({'Authorization': 'Bearer <access_token_value>'});
256+
* ```
257+
*
255258
* @param url The URI being authorized.
256259
*/
257-
abstract getRequestHeaders(url?: string): Promise<Headers>;
260+
abstract getRequestHeaders(url?: string | URL): Promise<Headers>;
258261

259262
/**
260263
* @return A promise that resolves with the current GCP access token
@@ -285,35 +288,58 @@ export abstract class AuthClient
285288
// the x-goog-user-project header, to indicate an alternate account for
286289
// billing and quota:
287290
if (
288-
!headers['x-goog-user-project'] && // don't override a value the user sets.
291+
!headers.has('x-goog-user-project') && // don't override a value the user sets.
289292
this.quotaProjectId
290293
) {
291-
headers['x-goog-user-project'] = this.quotaProjectId;
294+
headers.set('x-goog-user-project', this.quotaProjectId);
292295
}
293296
return headers;
294297
}
295298

299+
/**
300+
* Adds the `x-goog-user-project` and `Authorization` headers to the target Headers
301+
* object, if they exist on the source.
302+
*
303+
* @param target the headers to target
304+
* @param source the headers to source from
305+
* @returns the target headers
306+
*/
307+
protected addUserProjectAndAuthHeaders<T extends Headers>(
308+
target: T,
309+
source: Headers
310+
): T {
311+
const xGoogUserProject = source.get('x-goog-user-project');
312+
const authorizationHeader = source.get('Authorization');
313+
314+
if (xGoogUserProject) {
315+
target.set('x-goog-user-project', xGoogUserProject);
316+
}
317+
318+
if (authorizationHeader) {
319+
target.set('Authorization', authorizationHeader);
320+
}
321+
322+
return target;
323+
}
324+
296325
static readonly DEFAULT_REQUEST_INTERCEPTOR: Parameters<
297326
Gaxios['interceptors']['request']['add']
298327
>[0] = {
299328
resolved: async config => {
300-
const headers = config.headers || {};
301-
302329
// Set `x-goog-api-client`, if not already set
303-
if (!headers['x-goog-api-client']) {
330+
if (!config.headers.has('x-goog-api-client')) {
304331
const nodeVersion = process.version.replace(/^v/, '');
305-
headers['x-goog-api-client'] = `gl-node/${nodeVersion}`;
332+
config.headers.set('x-goog-api-client', `gl-node/${nodeVersion}`);
306333
}
307334

308335
// Set `User-Agent`
309-
if (!headers['User-Agent']) {
310-
headers['User-Agent'] = USER_AGENT;
311-
} else if (!headers['User-Agent'].includes(`${PRODUCT_NAME}/`)) {
312-
headers['User-Agent'] = `${headers['User-Agent']} ${USER_AGENT}`;
336+
const userAgent = config.headers.get('User-Agent');
337+
if (!userAgent) {
338+
config.headers.set('User-Agent', USER_AGENT);
339+
} else if (!userAgent.includes(`${PRODUCT_NAME}/`)) {
340+
config.headers.set('User-Agent', `${userAgent} ${USER_AGENT}`);
313341
}
314342

315-
config.headers = headers;
316-
317343
return config;
318344
},
319345
};
@@ -337,9 +363,8 @@ export abstract class AuthClient
337363
}
338364
}
339365

340-
export interface Headers {
341-
[index: string]: string;
342-
}
366+
// TypeScript does not have `HeadersInit` in the standard types yet
367+
export type HeadersInit = ConstructorParameters<typeof Headers>[0];
343368

344369
export interface GetAccessTokenResponse {
345370
token?: string | null;

src/auth/awsclient.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121

2222
import {DefaultAwsSecurityCredentialsSupplier} from './defaultawssecuritycredentialssupplier';
2323
import {originalOrCamelOptions, SnakeToCamelObject} from '../util';
24+
import {Gaxios} from 'gaxios';
2425

2526
/**
2627
* AWS credentials JSON interface. This is used for AWS workloads.
@@ -240,7 +241,7 @@ export class AwsClient extends BaseExternalAccountClient {
240241
// headers: [{key: 'x-amz-date', value: '...'}, ...]
241242
// }))
242243
const reformattedHeader: {key: string; value: string}[] = [];
243-
const extendedHeaders = Object.assign(
244+
const extendedHeaders = Gaxios.mergeHeaders(
244245
{
245246
// The full, canonical resource name of the workload identity pool
246247
// provider, with or without the HTTPS prefix.
@@ -250,13 +251,12 @@ export class AwsClient extends BaseExternalAccountClient {
250251
},
251252
options.headers
252253
);
254+
253255
// Reformat header to GCP STS expected format.
254-
for (const key in extendedHeaders) {
255-
reformattedHeader.push({
256-
key,
257-
value: extendedHeaders[key],
258-
});
259-
}
256+
extendedHeaders.forEach((value, key) =>
257+
reformattedHeader.push({key, value})
258+
);
259+
260260
// Serialize the reformatted signed request.
261261
return encodeURIComponent(
262262
JSON.stringify({

src/auth/awsrequestsigner.ts

+34-36
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,11 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import {GaxiosOptions} from 'gaxios';
15+
import {Gaxios, GaxiosOptions} from 'gaxios';
1616

17-
import {Headers} from './authclient';
17+
import {HeadersInit} from './authclient';
1818
import {Crypto, createCrypto, fromArrayBufferToHex} from '../crypto/crypto';
1919

20-
type HttpMethod =
21-
| 'GET'
22-
| 'POST'
23-
| 'PUT'
24-
| 'PATCH'
25-
| 'HEAD'
26-
| 'DELETE'
27-
| 'CONNECT'
28-
| 'OPTIONS'
29-
| 'TRACE';
30-
3120
/** Interface defining the AWS authorization header map for signed requests. */
3221
interface AwsAuthHeaderMap {
3322
amzDate?: string;
@@ -60,15 +49,15 @@ interface GenerateAuthHeaderMapOptions {
6049
// The AWS service URL query string.
6150
canonicalQuerystring: string;
6251
// The HTTP method used to call this API.
63-
method: HttpMethod;
52+
method: string;
6453
// The AWS region.
6554
region: string;
6655
// The AWS security credentials.
6756
securityCredentials: AwsSecurityCredentials;
6857
// The optional request payload if available.
6958
requestPayload?: string;
7059
// The optional additional headers needed for the requested AWS API.
71-
additionalAmzHeaders?: Headers;
60+
additionalAmzHeaders?: HeadersInit;
7261
}
7362

7463
/** AWS Signature Version 4 signing algorithm identifier. */
@@ -113,7 +102,7 @@ export class AwsRequestSigner {
113102
*/
114103
async getRequestOptions(amzOptions: GaxiosOptions): Promise<GaxiosOptions> {
115104
if (!amzOptions.url) {
116-
throw new Error('"url" is required in "amzOptions"');
105+
throw new RangeError('"url" is required in "amzOptions"');
117106
}
118107
// Stringify JSON requests. This will be set in the request body of the
119108
// generated signed request.
@@ -127,19 +116,26 @@ export class AwsRequestSigner {
127116
const additionalAmzHeaders = amzOptions.headers;
128117
const awsSecurityCredentials = await this.getCredentials();
129118
const uri = new URL(url);
119+
120+
if (typeof requestPayload !== 'string' && requestPayload !== undefined) {
121+
throw new TypeError(
122+
`'requestPayload' is expected to be a string if provided. Got: ${requestPayload}`
123+
);
124+
}
125+
130126
const headerMap = await generateAuthenticationHeaderMap({
131127
crypto: this.crypto,
132128
host: uri.host,
133129
canonicalUri: uri.pathname,
134-
canonicalQuerystring: uri.search.substr(1),
130+
canonicalQuerystring: uri.search.slice(1),
135131
method,
136132
region: this.region,
137133
securityCredentials: awsSecurityCredentials,
138134
requestPayload,
139135
additionalAmzHeaders,
140136
});
141137
// Append additional optional headers, eg. X-Amz-Target, Content-Type, etc.
142-
const headers: {[key: string]: string} = Object.assign(
138+
const headers = Gaxios.mergeHeaders(
143139
// Add x-amz-date if available.
144140
headerMap.amzDate ? {'x-amz-date': headerMap.amzDate} : {},
145141
{
@@ -149,7 +145,7 @@ export class AwsRequestSigner {
149145
additionalAmzHeaders || {}
150146
);
151147
if (awsSecurityCredentials.token) {
152-
Object.assign(headers, {
148+
Gaxios.mergeHeaders(headers, {
153149
'x-amz-security-token': awsSecurityCredentials.token,
154150
});
155151
}
@@ -159,7 +155,7 @@ export class AwsRequestSigner {
159155
headers,
160156
};
161157

162-
if (typeof requestPayload !== 'undefined') {
158+
if (requestPayload !== undefined) {
163159
awsSignedReq.body = requestPayload;
164160
}
165161

@@ -223,7 +219,9 @@ async function getSigningKey(
223219
async function generateAuthenticationHeaderMap(
224220
options: GenerateAuthHeaderMapOptions
225221
): Promise<AwsAuthHeaderMap> {
226-
const additionalAmzHeaders = options.additionalAmzHeaders || {};
222+
const additionalAmzHeaders = Gaxios.mergeHeaders(
223+
options.additionalAmzHeaders
224+
);
227225
const requestPayload = options.requestPayload || '';
228226
// iam.amazonaws.com host => iam service.
229227
// sts.us-east-2.amazonaws.com => sts service.
@@ -237,38 +235,38 @@ async function generateAuthenticationHeaderMap(
237235
// Format: '%Y%m%d'.
238236
const dateStamp = now.toISOString().replace(/[-]/g, '').replace(/T.*/, '');
239237

240-
// Change all additional headers to be lower case.
241-
const reformattedAdditionalAmzHeaders: Headers = {};
242-
Object.keys(additionalAmzHeaders).forEach(key => {
243-
reformattedAdditionalAmzHeaders[key.toLowerCase()] =
244-
additionalAmzHeaders[key];
245-
});
246238
// Add AWS token if available.
247239
if (options.securityCredentials.token) {
248-
reformattedAdditionalAmzHeaders['x-amz-security-token'] =
249-
options.securityCredentials.token;
240+
additionalAmzHeaders.set(
241+
'x-amz-security-token',
242+
options.securityCredentials.token
243+
);
250244
}
251245
// Header keys need to be sorted alphabetically.
252-
const amzHeaders = Object.assign(
246+
const amzHeaders = Gaxios.mergeHeaders(
253247
{
254248
host: options.host,
255249
},
256250
// Previously the date was not fixed with x-amz- and could be provided manually.
257251
// https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.req
258-
reformattedAdditionalAmzHeaders.date ? {} : {'x-amz-date': amzDate},
259-
reformattedAdditionalAmzHeaders
252+
additionalAmzHeaders.has('date') ? {} : {'x-amz-date': amzDate},
253+
additionalAmzHeaders
260254
);
261255
let canonicalHeaders = '';
262-
const signedHeadersList = Object.keys(amzHeaders).sort();
256+
257+
// TypeScript is missing `Headers#keys` at the time of writing
258+
const signedHeadersList = [
259+
...(amzHeaders as Headers & {keys: () => string[]}).keys(),
260+
].sort();
263261
signedHeadersList.forEach(key => {
264-
canonicalHeaders += `${key}:${amzHeaders[key]}\n`;
262+
canonicalHeaders += `${key}:${amzHeaders.get(key)}\n`;
265263
});
266264
const signedHeaders = signedHeadersList.join(';');
267265

268266
const payloadHash = await options.crypto.sha256DigestHex(requestPayload);
269267
// https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
270268
const canonicalRequest =
271-
`${options.method}\n` +
269+
`${options.method.toUpperCase()}\n` +
272270
`${options.canonicalUri}\n` +
273271
`${options.canonicalQuerystring}\n` +
274272
`${canonicalHeaders}\n` +
@@ -298,7 +296,7 @@ async function generateAuthenticationHeaderMap(
298296

299297
return {
300298
// Do not return x-amz-date if date is available.
301-
amzDate: reformattedAdditionalAmzHeaders.date ? undefined : amzDate,
299+
amzDate: additionalAmzHeaders.has('date') ? undefined : amzDate,
302300
authorizationHeader,
303301
canonicalQuerystring: options.canonicalQuerystring,
304302
};

0 commit comments

Comments
 (0)