From bf0e53e25d1ee0946243aa3b26bd1b87d12f1e76 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Tue, 17 Feb 2026 16:28:23 -0800 Subject: [PATCH 1/3] fix: defaultAwsSecurityCredentialSupplier fetches aws-credentials correctly from credential-url. --- .../src/auth/defaultawssecuritycredentialssupplier.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/google-auth-library-nodejs/src/auth/defaultawssecuritycredentialssupplier.ts b/packages/google-auth-library-nodejs/src/auth/defaultawssecuritycredentialssupplier.ts index a59818be7..0cd2f0d33 100644 --- a/packages/google-auth-library-nodejs/src/auth/defaultawssecuritycredentialssupplier.ts +++ b/packages/google-auth-library-nodejs/src/auth/defaultawssecuritycredentialssupplier.ts @@ -243,6 +243,7 @@ export class DefaultAwsSecurityCredentialsSupplier ...this.additionalGaxiosOptions, url: `${this.securityCredentialsUrl}/${roleName}`, headers: headers, + responseType: 'json', } as GaxiosOptions; AuthClient.setMethodName(opts, '#retrieveAwsSecurityCredentials'); const response = From dfbe8b80c3c52a7ba8693a967ef80cc0e7537869 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Wed, 18 Feb 2026 20:02:12 -0800 Subject: [PATCH 2/3] Reverted changes to responseType --- .../google-auth-library-nodejs/src/auth/baseexternalclient.ts | 2 ++ .../src/auth/defaultawssecuritycredentialssupplier.ts | 3 +++ .../src/auth/externalAccountAuthorizedUserClient.ts | 1 + packages/google-auth-library-nodejs/src/auth/refreshclient.ts | 1 + packages/google-auth-library-nodejs/src/auth/stscredentials.ts | 1 + .../src/auth/urlsubjecttokensupplier.ts | 1 + 6 files changed, 9 insertions(+) diff --git a/packages/google-auth-library-nodejs/src/auth/baseexternalclient.ts b/packages/google-auth-library-nodejs/src/auth/baseexternalclient.ts index 5c9196c4c..48111eb63 100644 --- a/packages/google-auth-library-nodejs/src/auth/baseexternalclient.ts +++ b/packages/google-auth-library-nodejs/src/auth/baseexternalclient.ts @@ -477,6 +477,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { ...BaseExternalAccountClient.RETRY_CONFIG, headers, url: `${this.cloudResourceManagerURL.toString()}${projectNumber}`, + responseType: 'json', }; AuthClient.setMethodName(opts, 'getProjectId'); const response = await this.transporter.request(opts); @@ -669,6 +670,7 @@ export abstract class BaseExternalAccountClient extends AuthClient { scope: this.getScopesArray(), lifetime: this.serviceAccountImpersonationLifetime + 's', }, + responseType: 'json', }; AuthClient.setMethodName(opts, 'getImpersonatedAccessToken'); const response = diff --git a/packages/google-auth-library-nodejs/src/auth/defaultawssecuritycredentialssupplier.ts b/packages/google-auth-library-nodejs/src/auth/defaultawssecuritycredentialssupplier.ts index 0cd2f0d33..657721c4c 100644 --- a/packages/google-auth-library-nodejs/src/auth/defaultawssecuritycredentialssupplier.ts +++ b/packages/google-auth-library-nodejs/src/auth/defaultawssecuritycredentialssupplier.ts @@ -127,6 +127,7 @@ export class DefaultAwsSecurityCredentialsSupplier ...this.additionalGaxiosOptions, url: this.regionUrl, method: 'GET', + responseType: 'text', headers: metadataHeaders, }; AuthClient.setMethodName(opts, 'getAwsRegion'); @@ -191,6 +192,7 @@ export class DefaultAwsSecurityCredentialsSupplier ...this.additionalGaxiosOptions, url: this.imdsV2SessionTokenUrl, method: 'PUT', + responseType: 'text', headers: {'x-aws-ec2-metadata-token-ttl-seconds': '300'}, }; AuthClient.setMethodName(opts, '#getImdsV2SessionToken'); @@ -218,6 +220,7 @@ export class DefaultAwsSecurityCredentialsSupplier ...this.additionalGaxiosOptions, url: this.securityCredentialsUrl, method: 'GET', + responseType: 'text', headers: headers, }; AuthClient.setMethodName(opts, '#getAwsRoleName'); diff --git a/packages/google-auth-library-nodejs/src/auth/externalAccountAuthorizedUserClient.ts b/packages/google-auth-library-nodejs/src/auth/externalAccountAuthorizedUserClient.ts index d473e4dcd..320db0554 100644 --- a/packages/google-auth-library-nodejs/src/auth/externalAccountAuthorizedUserClient.ts +++ b/packages/google-auth-library-nodejs/src/auth/externalAccountAuthorizedUserClient.ts @@ -120,6 +120,7 @@ class ExternalAccountAuthorizedUserHandler extends OAuthClientAuthHandler { grant_type: 'refresh_token', refresh_token: refreshToken, }), + responseType: 'json', }; AuthClient.setMethodName(opts, 'refreshToken'); diff --git a/packages/google-auth-library-nodejs/src/auth/refreshclient.ts b/packages/google-auth-library-nodejs/src/auth/refreshclient.ts index 41abcaea8..201d68be8 100644 --- a/packages/google-auth-library-nodejs/src/auth/refreshclient.ts +++ b/packages/google-auth-library-nodejs/src/auth/refreshclient.ts @@ -109,6 +109,7 @@ export class UserRefreshClient extends OAuth2Client { refresh_token: this._refreshToken, target_audience: targetAudience, } as {}), + responseType: 'json', }; AuthClient.setMethodName(opts, 'fetchIdToken'); diff --git a/packages/google-auth-library-nodejs/src/auth/stscredentials.ts b/packages/google-auth-library-nodejs/src/auth/stscredentials.ts index 625f77319..e8a0e57a1 100644 --- a/packages/google-auth-library-nodejs/src/auth/stscredentials.ts +++ b/packages/google-auth-library-nodejs/src/auth/stscredentials.ts @@ -213,6 +213,7 @@ export class StsCredentials extends OAuthClientAuthHandler { data: new URLSearchParams( removeUndefinedValuesInObject(values) as Record, ), + responseType: 'json', }; AuthClient.setMethodName(opts, 'exchangeToken'); diff --git a/packages/google-auth-library-nodejs/src/auth/urlsubjecttokensupplier.ts b/packages/google-auth-library-nodejs/src/auth/urlsubjecttokensupplier.ts index 75a0f45fc..4d373f94f 100644 --- a/packages/google-auth-library-nodejs/src/auth/urlsubjecttokensupplier.ts +++ b/packages/google-auth-library-nodejs/src/auth/urlsubjecttokensupplier.ts @@ -88,6 +88,7 @@ export class UrlSubjectTokenSupplier implements SubjectTokenSupplier { url: this.url, method: 'GET', headers: this.headers, + responseType: this.formatType, }; AuthClient.setMethodName(opts, 'getSubjectToken'); From fb9a6698e2d9b0bc6ae7c660fba415454b5a9d75 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Thu, 19 Feb 2026 12:17:48 -0800 Subject: [PATCH 3/3] Added tests for responseType. --- .../test/test.awsclient.ts | 92 +++++++++++++++ .../test/test.baseexternalclient.ts | 105 ++++++++++++++++++ ...est.externalaccountauthorizeduserclient.ts | 27 +++++ .../test/test.identitypoolclient.ts | 37 ++++++ .../test/test.refresh.ts | 21 ++++ .../test/test.stscredentials.ts | 25 +++++ 6 files changed, 307 insertions(+) diff --git a/packages/google-auth-library-nodejs/test/test.awsclient.ts b/packages/google-auth-library-nodejs/test/test.awsclient.ts index 4e7b6586c..ecd147f21 100644 --- a/packages/google-auth-library-nodejs/test/test.awsclient.ts +++ b/packages/google-auth-library-nodejs/test/test.awsclient.ts @@ -319,6 +319,98 @@ describe('AwsClient', () => { scope.done(); }); + it('should use correct responseType for metadata requests', async () => { + const scope = nock(metadataBaseUrl) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`) + .get('/latest/meta-data/iam/security-credentials') + .reply(200, awsRole) + .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) + .reply(200, awsSecurityCredentials); + + const client = new AwsClient(awsOptions); + const requestSpy = sinon.spy(client.transporter, 'request'); + + await client.retrieveSubjectToken(); + + // 1. GET /latest/meta-data/placement/availability-zone (Region) + assert.strictEqual( + requestSpy.getCall(0)!.args[0]!.responseType, + 'text', + ); + // 2. GET /latest/meta-data/iam/security-credentials (Role) + assert.strictEqual( + requestSpy.getCall(1)!.args[0]!.responseType, + 'text', + ); + // 3. GET /latest/meta-data/iam/security-credentials/{role} (Credentials) + assert.strictEqual( + requestSpy.getCall(2)!.args[0]!.responseType, + 'json', + ); + + scope.done(); + requestSpy.restore(); + }); + + it('should use responseType: text for IMDSv2 session token', async () => { + const scopes: nock.Scope[] = []; + scopes.push( + nock(metadataBaseUrl, { + reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'}, + }) + .put('/latest/api/token') + .twice() + .reply(200, awsSessionToken), + ); + + scopes.push( + nock(metadataBaseUrl, { + reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken}, + }) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`) + .get('/latest/meta-data/iam/security-credentials') + .reply(200, awsRole) + .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) + .reply(200, awsSecurityCredentials), + ); + + const client = new AwsClient(awsOptionsWithImdsv2); + const requestSpy = sinon.spy(client.transporter, 'request'); + + await client.retrieveSubjectToken(); + + // 1. PUT /latest/api/token (IMDSv2 session token for region) + assert.strictEqual( + requestSpy.getCall(0)!.args[0]!.responseType, + 'text', + ); + // 2. GET /latest/meta-data/placement/availability-zone (Region) + assert.strictEqual( + requestSpy.getCall(1)!.args[0]!.responseType, + 'text', + ); + // 3. PUT /latest/api/token (IMDSv2 session token for credentials) + assert.strictEqual( + requestSpy.getCall(2)!.args[0]!.responseType, + 'text', + ); + // 4. GET /latest/meta-data/iam/security-credentials (Role) + assert.strictEqual( + requestSpy.getCall(3)!.args[0]!.responseType, + 'text', + ); + // 5. GET /latest/meta-data/iam/security-credentials/{role} (Credentials) + assert.strictEqual( + requestSpy.getCall(4)!.args[0]!.responseType, + 'json', + ); + + scopes.forEach(scope => scope.done()); + requestSpy.restore(); + }); + it('should resolve on success with ipv6', async () => { const ipv6baseUrl = 'http://[fd00:ec2::254]'; const ipv6CredentialSource = { diff --git a/packages/google-auth-library-nodejs/test/test.baseexternalclient.ts b/packages/google-auth-library-nodejs/test/test.baseexternalclient.ts index c3f7444ce..3020b47cd 100644 --- a/packages/google-auth-library-nodejs/test/test.baseexternalclient.ts +++ b/packages/google-auth-library-nodejs/test/test.baseexternalclient.ts @@ -481,6 +481,63 @@ describe('BaseExternalAccountClient', () => { }); describe('getProjectId()', () => { + it('should use responseType: json', async () => { + const projectNumber = 'my-proj-number'; + const projectId = 'my-proj-id'; + const response = { + projectNumber, + projectId, + lifecycleState: 'ACTIVE', + name: 'project-name', + createTime: '2018-11-06T04:42:54.109Z', + parent: { + type: 'folder', + id: '12345678901', + }, + }; + const options = Object.assign({}, externalAccountOptions); + options.audience = getAudience(projectNumber); + const scopes = [ + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience: options.audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]), + mockCloudResourceManager( + projectNumber, + stsSuccessfulResponse.access_token, + 200, + response, + ), + ]; + const client = new TestExternalAccountClient(options); + const requestSpy = sinon.spy(client.transporter, 'request'); + + await client.getProjectId(); + + const call = requestSpy + .getCalls() + .find( + c => + c.args[0] && + String((c.args[0] as any).url).includes('cloudresourcemanager'), + ); + assert.ok(call); + assert.strictEqual((call!.args[0] as any).responseType, 'json'); + + scopes.forEach(scope => scope.done()); + }); + it('should resolve for workforce pools when workforce_pool_user_project is provided', async () => { const options = Object.assign( {}, @@ -1227,6 +1284,54 @@ describe('BaseExternalAccountClient', () => { }, }; + it('should use responseType: json for impersonation', async () => { + const scopes: nock.Scope[] = []; + scopes.push( + mockStsTokenExchange([ + { + statusCode: 200, + response: stsSuccessfulResponse, + request: { + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + audience, + scope: 'https://www.googleapis.com/auth/cloud-platform', + requested_token_type: + 'urn:ietf:params:oauth:token-type:access_token', + subject_token: 'subject_token_0', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + }, + }, + ]), + ); + scopes.push( + mockGenerateAccessToken({ + statusCode: 200, + response: saSuccessResponse, + token: stsSuccessfulResponse.access_token, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }), + ); + + const client = new TestExternalAccountClient( + externalAccountOptionsWithSA, + ); + const requestSpy = sinon.spy(client.transporter, 'request'); + + await client.getAccessToken(); + + const call = requestSpy + .getCalls() + .find( + c => + c.args[0] && + String((c.args[0] as any).url).includes('iamcredentials'), + ); + assert.ok(call); + assert.strictEqual((call!.args[0] as any).responseType, 'json'); + + scopes.forEach(scope => scope.done()); + }); + it('should resolve with the expected response', async () => { const scopes: nock.Scope[] = []; scopes.push( diff --git a/packages/google-auth-library-nodejs/test/test.externalaccountauthorizeduserclient.ts b/packages/google-auth-library-nodejs/test/test.externalaccountauthorizeduserclient.ts index e65aa2133..e5aca190e 100644 --- a/packages/google-auth-library-nodejs/test/test.externalaccountauthorizeduserclient.ts +++ b/packages/google-auth-library-nodejs/test/test.externalaccountauthorizeduserclient.ts @@ -219,6 +219,33 @@ describe('ExternalAccountAuthorizedUserClient', () => { }); describe('getAccessToken()', () => { + it('should use responseType: json', async () => { + const scope = mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ + { + statusCode: 200, + response: successfulRefreshResponse, + request: { + grant_type: 'refresh_token', + refresh_token: 'refreshToken', + }, + }, + ]); + + const client = new ExternalAccountAuthorizedUserClient( + externalAccountAuthorizedUserCredentialOptions, + ); + const requestSpy = sinon.spy( + (client as any).externalAccountAuthorizedUserHandler.transporter, + 'request', + ); + + await client.getAccessToken(); + + const call = requestSpy.getCall(0); + assert.strictEqual(call.args[0]!.responseType, 'json'); + scope.done(); + }); + it('should resolve with the expected response', async () => { const scope = mockStsTokenRefresh(BASE_URL, REFRESH_PATH, [ { diff --git a/packages/google-auth-library-nodejs/test/test.identitypoolclient.ts b/packages/google-auth-library-nodejs/test/test.identitypoolclient.ts index 63ac15bde..d8b2ff366 100644 --- a/packages/google-auth-library-nodejs/test/test.identitypoolclient.ts +++ b/packages/google-auth-library-nodejs/test/test.identitypoolclient.ts @@ -877,6 +877,43 @@ describe('IdentityPoolClient', () => { describe('for url-sourced subject tokens', () => { describe('retrieveSubjectToken()', () => { + it('should use responseType: text for text format', async () => { + const externalSubjectToken = 'SUBJECT_TOKEN_1'; + const scope = nock(metadataBaseUrl, { + reqheaders: metadataHeaders, + }) + .get(metadataPath) + .reply(200, externalSubjectToken); + + const client = new IdentityPoolClient(urlSourcedOptions); + const requestSpy = sinon.spy(client.transporter, 'request'); + + await client.retrieveSubjectToken(); + + const call = requestSpy.getCall(0); + assert.strictEqual(call.args[0]!.responseType, 'text'); + scope.done(); + }); + it('should use responseType: json for json format', async () => { + const externalSubjectToken = 'SUBJECT_TOKEN_1'; + const jsonResponse = { + access_token: externalSubjectToken, + }; + const scope = nock(metadataBaseUrl, { + reqheaders: metadataHeaders, + }) + .get(metadataPath) + .reply(200, jsonResponse); + + const client = new IdentityPoolClient(jsonRespUrlSourcedOptions); + const requestSpy = sinon.spy(client.transporter, 'request'); + + await client.retrieveSubjectToken(); + + const call = requestSpy.getCall(0); + assert.strictEqual(call.args[0]!.responseType, 'json'); + scope.done(); + }); it('should resolve on text response success', async () => { const externalSubjectToken = 'SUBJECT_TOKEN_1'; const scope = nock(metadataBaseUrl, { diff --git a/packages/google-auth-library-nodejs/test/test.refresh.ts b/packages/google-auth-library-nodejs/test/test.refresh.ts index 2902540f3..470d8cb7c 100644 --- a/packages/google-auth-library-nodejs/test/test.refresh.ts +++ b/packages/google-auth-library-nodejs/test/test.refresh.ts @@ -16,6 +16,7 @@ import * as assert from 'assert'; import {describe, it} from 'mocha'; import * as fs from 'fs'; import * as nock from 'nock'; +import * as sinon from 'sinon'; import {UserRefreshClient} from '../src'; describe('refresh', () => { @@ -187,4 +188,24 @@ describe('refresh', () => { assert.strictEqual(headers.get('x-goog-user-project'), 'my-quota-project'); req.done(); }); + + it('fetchIdToken should use responseType: json', async () => { + const client = new UserRefreshClient({ + clientId: 'id', + clientSecret: 'secret', + refreshToken: 'refresh', + }); + const requestSpy = sinon.spy(client.transporter, 'request'); + + const scope = nock('https://oauth2.googleapis.com') + .post('/token') + .reply(200, {id_token: 'id-token'}); + + await client.fetchIdToken('target-aud'); + + const call = requestSpy.getCall(0); + assert.strictEqual(call.args[0]!.responseType, 'json'); + scope.done(); + sinon.restore(); + }); }); diff --git a/packages/google-auth-library-nodejs/test/test.stscredentials.ts b/packages/google-auth-library-nodejs/test/test.stscredentials.ts index ebe981749..38102ebb6 100644 --- a/packages/google-auth-library-nodejs/test/test.stscredentials.ts +++ b/packages/google-auth-library-nodejs/test/test.stscredentials.ts @@ -15,6 +15,7 @@ import * as assert from 'assert'; import {describe, it, afterEach} from 'mocha'; import * as nock from 'nock'; +import * as sinon from 'sinon'; import {createCrypto} from '../src/crypto/crypto'; import { StsCredentials, @@ -152,6 +153,30 @@ describe('StsCredentials', () => { ); describe('without client authentication', () => { + it('should use responseType: json', async () => { + const scope = mockStsTokenExchange( + 200, + stsSuccessfulResponse, + expectedRequest, + additionalHeaders, + ); + const stsCredentials = new StsCredentials(tokenExchangeEndpoint); + const requestSpy = sinon.spy( + (stsCredentials as any).transporter, + 'request', + ); + + await stsCredentials.exchangeToken( + stsCredentialsOptions, + additionalHeaders, + options, + ); + + const call = requestSpy.getCall(0); + assert.strictEqual(call.args[0]!.responseType, 'json'); + scope.done(); + }); + it('should handle successful full request', async () => { const scope = mockStsTokenExchange( 200,