Skip to content
This repository was archived by the owner on Mar 19, 2026. It is now read-only.

Commit 8c50526

Browse files
authored
fix: defaultAwsSecurityCredentialSupplier fetches aws-credentials correctly from credential-url (#901)
* fix: defaultAwsSecurityCredentialSupplier fetches aws-credentials correctly from credential-url. * Reverted changes to responseType * Added tests for responseType.
1 parent fb49387 commit 8c50526

12 files changed

Lines changed: 317 additions & 0 deletions

packages/google-auth-library-nodejs/src/auth/baseexternalclient.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,7 @@ export abstract class BaseExternalAccountClient extends AuthClient {
477477
...BaseExternalAccountClient.RETRY_CONFIG,
478478
headers,
479479
url: `${this.cloudResourceManagerURL.toString()}${projectNumber}`,
480+
responseType: 'json',
480481
};
481482
AuthClient.setMethodName(opts, 'getProjectId');
482483
const response = await this.transporter.request<ProjectInfo>(opts);
@@ -669,6 +670,7 @@ export abstract class BaseExternalAccountClient extends AuthClient {
669670
scope: this.getScopesArray(),
670671
lifetime: this.serviceAccountImpersonationLifetime + 's',
671672
},
673+
responseType: 'json',
672674
};
673675
AuthClient.setMethodName(opts, 'getImpersonatedAccessToken');
674676
const response =

packages/google-auth-library-nodejs/src/auth/defaultawssecuritycredentialssupplier.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export class DefaultAwsSecurityCredentialsSupplier
127127
...this.additionalGaxiosOptions,
128128
url: this.regionUrl,
129129
method: 'GET',
130+
responseType: 'text',
130131
headers: metadataHeaders,
131132
};
132133
AuthClient.setMethodName(opts, 'getAwsRegion');
@@ -191,6 +192,7 @@ export class DefaultAwsSecurityCredentialsSupplier
191192
...this.additionalGaxiosOptions,
192193
url: this.imdsV2SessionTokenUrl,
193194
method: 'PUT',
195+
responseType: 'text',
194196
headers: {'x-aws-ec2-metadata-token-ttl-seconds': '300'},
195197
};
196198
AuthClient.setMethodName(opts, '#getImdsV2SessionToken');
@@ -218,6 +220,7 @@ export class DefaultAwsSecurityCredentialsSupplier
218220
...this.additionalGaxiosOptions,
219221
url: this.securityCredentialsUrl,
220222
method: 'GET',
223+
responseType: 'text',
221224
headers: headers,
222225
};
223226
AuthClient.setMethodName(opts, '#getAwsRoleName');
@@ -243,6 +246,7 @@ export class DefaultAwsSecurityCredentialsSupplier
243246
...this.additionalGaxiosOptions,
244247
url: `${this.securityCredentialsUrl}/${roleName}`,
245248
headers: headers,
249+
responseType: 'json',
246250
} as GaxiosOptions;
247251
AuthClient.setMethodName(opts, '#retrieveAwsSecurityCredentials');
248252
const response =

packages/google-auth-library-nodejs/src/auth/externalAccountAuthorizedUserClient.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ class ExternalAccountAuthorizedUserHandler extends OAuthClientAuthHandler {
120120
grant_type: 'refresh_token',
121121
refresh_token: refreshToken,
122122
}),
123+
responseType: 'json',
123124
};
124125
AuthClient.setMethodName(opts, 'refreshToken');
125126

packages/google-auth-library-nodejs/src/auth/refreshclient.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export class UserRefreshClient extends OAuth2Client {
109109
refresh_token: this._refreshToken,
110110
target_audience: targetAudience,
111111
} as {}),
112+
responseType: 'json',
112113
};
113114
AuthClient.setMethodName(opts, 'fetchIdToken');
114115

packages/google-auth-library-nodejs/src/auth/stscredentials.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ export class StsCredentials extends OAuthClientAuthHandler {
213213
data: new URLSearchParams(
214214
removeUndefinedValuesInObject(values) as Record<string, string>,
215215
),
216+
responseType: 'json',
216217
};
217218
AuthClient.setMethodName(opts, 'exchangeToken');
218219

packages/google-auth-library-nodejs/src/auth/urlsubjecttokensupplier.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export class UrlSubjectTokenSupplier implements SubjectTokenSupplier {
8888
url: this.url,
8989
method: 'GET',
9090
headers: this.headers,
91+
responseType: this.formatType,
9192
};
9293
AuthClient.setMethodName(opts, 'getSubjectToken');
9394

packages/google-auth-library-nodejs/test/test.awsclient.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,98 @@ describe('AwsClient', () => {
319319
scope.done();
320320
});
321321

322+
it('should use correct responseType for metadata requests', async () => {
323+
const scope = nock(metadataBaseUrl)
324+
.get('/latest/meta-data/placement/availability-zone')
325+
.reply(200, `${awsRegion}b`)
326+
.get('/latest/meta-data/iam/security-credentials')
327+
.reply(200, awsRole)
328+
.get(`/latest/meta-data/iam/security-credentials/${awsRole}`)
329+
.reply(200, awsSecurityCredentials);
330+
331+
const client = new AwsClient(awsOptions);
332+
const requestSpy = sinon.spy(client.transporter, 'request');
333+
334+
await client.retrieveSubjectToken();
335+
336+
// 1. GET /latest/meta-data/placement/availability-zone (Region)
337+
assert.strictEqual(
338+
requestSpy.getCall(0)!.args[0]!.responseType,
339+
'text',
340+
);
341+
// 2. GET /latest/meta-data/iam/security-credentials (Role)
342+
assert.strictEqual(
343+
requestSpy.getCall(1)!.args[0]!.responseType,
344+
'text',
345+
);
346+
// 3. GET /latest/meta-data/iam/security-credentials/{role} (Credentials)
347+
assert.strictEqual(
348+
requestSpy.getCall(2)!.args[0]!.responseType,
349+
'json',
350+
);
351+
352+
scope.done();
353+
requestSpy.restore();
354+
});
355+
356+
it('should use responseType: text for IMDSv2 session token', async () => {
357+
const scopes: nock.Scope[] = [];
358+
scopes.push(
359+
nock(metadataBaseUrl, {
360+
reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'},
361+
})
362+
.put('/latest/api/token')
363+
.twice()
364+
.reply(200, awsSessionToken),
365+
);
366+
367+
scopes.push(
368+
nock(metadataBaseUrl, {
369+
reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken},
370+
})
371+
.get('/latest/meta-data/placement/availability-zone')
372+
.reply(200, `${awsRegion}b`)
373+
.get('/latest/meta-data/iam/security-credentials')
374+
.reply(200, awsRole)
375+
.get(`/latest/meta-data/iam/security-credentials/${awsRole}`)
376+
.reply(200, awsSecurityCredentials),
377+
);
378+
379+
const client = new AwsClient(awsOptionsWithImdsv2);
380+
const requestSpy = sinon.spy(client.transporter, 'request');
381+
382+
await client.retrieveSubjectToken();
383+
384+
// 1. PUT /latest/api/token (IMDSv2 session token for region)
385+
assert.strictEqual(
386+
requestSpy.getCall(0)!.args[0]!.responseType,
387+
'text',
388+
);
389+
// 2. GET /latest/meta-data/placement/availability-zone (Region)
390+
assert.strictEqual(
391+
requestSpy.getCall(1)!.args[0]!.responseType,
392+
'text',
393+
);
394+
// 3. PUT /latest/api/token (IMDSv2 session token for credentials)
395+
assert.strictEqual(
396+
requestSpy.getCall(2)!.args[0]!.responseType,
397+
'text',
398+
);
399+
// 4. GET /latest/meta-data/iam/security-credentials (Role)
400+
assert.strictEqual(
401+
requestSpy.getCall(3)!.args[0]!.responseType,
402+
'text',
403+
);
404+
// 5. GET /latest/meta-data/iam/security-credentials/{role} (Credentials)
405+
assert.strictEqual(
406+
requestSpy.getCall(4)!.args[0]!.responseType,
407+
'json',
408+
);
409+
410+
scopes.forEach(scope => scope.done());
411+
requestSpy.restore();
412+
});
413+
322414
it('should resolve on success with ipv6', async () => {
323415
const ipv6baseUrl = 'http://[fd00:ec2::254]';
324416
const ipv6CredentialSource = {

packages/google-auth-library-nodejs/test/test.baseexternalclient.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,63 @@ describe('BaseExternalAccountClient', () => {
481481
});
482482

483483
describe('getProjectId()', () => {
484+
it('should use responseType: json', async () => {
485+
const projectNumber = 'my-proj-number';
486+
const projectId = 'my-proj-id';
487+
const response = {
488+
projectNumber,
489+
projectId,
490+
lifecycleState: 'ACTIVE',
491+
name: 'project-name',
492+
createTime: '2018-11-06T04:42:54.109Z',
493+
parent: {
494+
type: 'folder',
495+
id: '12345678901',
496+
},
497+
};
498+
const options = Object.assign({}, externalAccountOptions);
499+
options.audience = getAudience(projectNumber);
500+
const scopes = [
501+
mockStsTokenExchange([
502+
{
503+
statusCode: 200,
504+
response: stsSuccessfulResponse,
505+
request: {
506+
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
507+
audience: options.audience,
508+
scope: 'https://www.googleapis.com/auth/cloud-platform',
509+
requested_token_type:
510+
'urn:ietf:params:oauth:token-type:access_token',
511+
subject_token: 'subject_token_0',
512+
subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',
513+
},
514+
},
515+
]),
516+
mockCloudResourceManager(
517+
projectNumber,
518+
stsSuccessfulResponse.access_token,
519+
200,
520+
response,
521+
),
522+
];
523+
const client = new TestExternalAccountClient(options);
524+
const requestSpy = sinon.spy(client.transporter, 'request');
525+
526+
await client.getProjectId();
527+
528+
const call = requestSpy
529+
.getCalls()
530+
.find(
531+
c =>
532+
c.args[0] &&
533+
String((c.args[0] as any).url).includes('cloudresourcemanager'),
534+
);
535+
assert.ok(call);
536+
assert.strictEqual((call!.args[0] as any).responseType, 'json');
537+
538+
scopes.forEach(scope => scope.done());
539+
});
540+
484541
it('should resolve for workforce pools when workforce_pool_user_project is provided', async () => {
485542
const options = Object.assign(
486543
{},
@@ -1227,6 +1284,54 @@ describe('BaseExternalAccountClient', () => {
12271284
},
12281285
};
12291286

1287+
it('should use responseType: json for impersonation', async () => {
1288+
const scopes: nock.Scope[] = [];
1289+
scopes.push(
1290+
mockStsTokenExchange([
1291+
{
1292+
statusCode: 200,
1293+
response: stsSuccessfulResponse,
1294+
request: {
1295+
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
1296+
audience,
1297+
scope: 'https://www.googleapis.com/auth/cloud-platform',
1298+
requested_token_type:
1299+
'urn:ietf:params:oauth:token-type:access_token',
1300+
subject_token: 'subject_token_0',
1301+
subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',
1302+
},
1303+
},
1304+
]),
1305+
);
1306+
scopes.push(
1307+
mockGenerateAccessToken({
1308+
statusCode: 200,
1309+
response: saSuccessResponse,
1310+
token: stsSuccessfulResponse.access_token,
1311+
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
1312+
}),
1313+
);
1314+
1315+
const client = new TestExternalAccountClient(
1316+
externalAccountOptionsWithSA,
1317+
);
1318+
const requestSpy = sinon.spy(client.transporter, 'request');
1319+
1320+
await client.getAccessToken();
1321+
1322+
const call = requestSpy
1323+
.getCalls()
1324+
.find(
1325+
c =>
1326+
c.args[0] &&
1327+
String((c.args[0] as any).url).includes('iamcredentials'),
1328+
);
1329+
assert.ok(call);
1330+
assert.strictEqual((call!.args[0] as any).responseType, 'json');
1331+
1332+
scopes.forEach(scope => scope.done());
1333+
});
1334+
12301335
it('should resolve with the expected response', async () => {
12311336
const scopes: nock.Scope[] = [];
12321337
scopes.push(

packages/google-auth-library-nodejs/test/test.externalaccountauthorizeduserclient.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,33 @@ describe('ExternalAccountAuthorizedUserClient', () => {
219219
});
220220

221221
describe('getAccessToken()', () => {
222+
it('should use responseType: json', async () => {
223+
const scope = mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [
224+
{
225+
statusCode: 200,
226+
response: successfulRefreshResponse,
227+
request: {
228+
grant_type: 'refresh_token',
229+
refresh_token: 'refreshToken',
230+
},
231+
},
232+
]);
233+
234+
const client = new ExternalAccountAuthorizedUserClient(
235+
externalAccountAuthorizedUserCredentialOptions,
236+
);
237+
const requestSpy = sinon.spy(
238+
(client as any).externalAccountAuthorizedUserHandler.transporter,
239+
'request',
240+
);
241+
242+
await client.getAccessToken();
243+
244+
const call = requestSpy.getCall(0);
245+
assert.strictEqual(call.args[0]!.responseType, 'json');
246+
scope.done();
247+
});
248+
222249
it('should resolve with the expected response', async () => {
223250
const scope = mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [
224251
{

packages/google-auth-library-nodejs/test/test.identitypoolclient.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,43 @@ describe('IdentityPoolClient', () => {
877877

878878
describe('for url-sourced subject tokens', () => {
879879
describe('retrieveSubjectToken()', () => {
880+
it('should use responseType: text for text format', async () => {
881+
const externalSubjectToken = 'SUBJECT_TOKEN_1';
882+
const scope = nock(metadataBaseUrl, {
883+
reqheaders: metadataHeaders,
884+
})
885+
.get(metadataPath)
886+
.reply(200, externalSubjectToken);
887+
888+
const client = new IdentityPoolClient(urlSourcedOptions);
889+
const requestSpy = sinon.spy(client.transporter, 'request');
890+
891+
await client.retrieveSubjectToken();
892+
893+
const call = requestSpy.getCall(0);
894+
assert.strictEqual(call.args[0]!.responseType, 'text');
895+
scope.done();
896+
});
897+
it('should use responseType: json for json format', async () => {
898+
const externalSubjectToken = 'SUBJECT_TOKEN_1';
899+
const jsonResponse = {
900+
access_token: externalSubjectToken,
901+
};
902+
const scope = nock(metadataBaseUrl, {
903+
reqheaders: metadataHeaders,
904+
})
905+
.get(metadataPath)
906+
.reply(200, jsonResponse);
907+
908+
const client = new IdentityPoolClient(jsonRespUrlSourcedOptions);
909+
const requestSpy = sinon.spy(client.transporter, 'request');
910+
911+
await client.retrieveSubjectToken();
912+
913+
const call = requestSpy.getCall(0);
914+
assert.strictEqual(call.args[0]!.responseType, 'json');
915+
scope.done();
916+
});
880917
it('should resolve on text response success', async () => {
881918
const externalSubjectToken = 'SUBJECT_TOKEN_1';
882919
const scope = nock(metadataBaseUrl, {

0 commit comments

Comments
 (0)