diff --git a/core/packages/google-auth-library-nodejs/src/auth/externalclient.ts b/core/packages/google-auth-library-nodejs/src/auth/externalclient.ts index f9a7cb0f15ad..84a8953d1a50 100644 --- a/core/packages/google-auth-library-nodejs/src/auth/externalclient.ts +++ b/core/packages/google-auth-library-nodejs/src/auth/externalclient.ts @@ -32,6 +32,7 @@ import { PluggableAuthClient, PluggableAuthClientOptions, } from './pluggable-auth-client'; +import {isAdcWarningSuppressed} from '../util'; export type ExternalAccountClientOptions = | IdentityPoolClientOptions @@ -73,6 +74,11 @@ export class ExternalAccountClient { static fromJSON( options: ExternalAccountClientOptions, ): BaseExternalAccountClient | null { + if (!isAdcWarningSuppressed()) { + console.warn( + 'The `fromJSON` method does not validate the credential configuration. A security risk occurs when a credential configuration configured with malicious URLs is used. When the credential configuration is accepted from an untrusted source, you should validate it before using it with this method. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', + ); + } if (options && options.type === EXTERNAL_ACCOUNT_TYPE) { if ((options as AwsClientOptions).credential_source?.environment_id) { return new AwsClient(options as AwsClientOptions); diff --git a/core/packages/google-auth-library-nodejs/src/auth/googleauth.ts b/core/packages/google-auth-library-nodejs/src/auth/googleauth.ts index 52e930f7d022..aa8c01e831df 100644 --- a/core/packages/google-auth-library-nodejs/src/auth/googleauth.ts +++ b/core/packages/google-auth-library-nodejs/src/auth/googleauth.ts @@ -47,7 +47,13 @@ import { GDCH_SERVICE_ACCOUNT_TYPE, GdchCredentialsInput, } from './gdchclient'; -import {originalOrCamelOptions} from '../util'; +import { + isAdcWarningSuppressed, + isFromJsonWarningSuppressed, + originalOrCamelOptions, + suppressAdcWarning, + suppressFromJsonWarning, +} from '../util'; import {AnyAuthClient, AnyAuthClientConstructor} from '..'; /** @@ -248,7 +254,11 @@ export class GoogleAuth { private _cachedProjectId?: string | null; // To save the contents of the JSON credential file - jsonContent: JWTInput | ExternalAccountClientOptions | GdchCredentialsInput | null = null; + jsonContent: + | JWTInput + | ExternalAccountClientOptions + | GdchCredentialsInput + | null = null; apiKey: string | null; cachedCredential: AnyAuthClient | T | null = null; @@ -281,9 +291,19 @@ export class GoogleAuth { this._cachedProjectId = opts.projectId || null; this.cachedCredential = opts.authClient || null; this.keyFilename = opts.keyFilename || opts.keyFile; + if (this.keyFilename) { + console.warn( + 'The `keyFilename` option is deprecated. Please use the `auth` object constructor instead. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', + ); + } this.scopes = opts.scopes; this.clientOptions = opts.clientOptions || {}; this.jsonContent = opts.credentials || null; + if (this.jsonContent) { + console.warn( + 'The `credentials` option is deprecated. Please use the `auth` object constructor instead. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', + ); + } this.apiKey = opts.apiKey || this.clientOptions.apiKey || null; // Cannot use both API Key + Credentials @@ -302,9 +322,9 @@ export class GoogleAuth { // variables are set on the JWT client in order to indicate the type of library, // and sign the JWT with the correct audience and scopes (if not supplied). setGapicJWTValues(client: JWT) { - client.defaultServicePath = this.defaultServicePath; - client.useJWTAccessWithScope = this.useJWTAccessWithScope; - client.defaultScopes = this.defaultScopes; + client.defaultServicePath ??= this.defaultServicePath; + client.useJWTAccessWithScope ??= this.useJWTAccessWithScope; + client.defaultScopes ??= this.defaultScopes; } /** @@ -666,7 +686,7 @@ export class GoogleAuth { // Now open a read stream on the file, and parse it. const readStream = fs.createReadStream(filePath); - return this.fromStream(readStream, options); + return suppressAdcWarning(() => this.fromStream(readStream, options)); } /** @@ -696,7 +716,9 @@ export class GoogleAuth { ); } - const sourceClient = this.fromJSON(json.source_credentials); + const sourceClient = suppressFromJsonWarning(() => + this.fromJSON(json.source_credentials!), + ); if (json.service_account_impersonation_url?.length > 256) { /** @@ -774,11 +796,21 @@ export class GoogleAuth { * @returns JWT or UserRefresh Client with data */ fromJSON( - json: JWTInput | ImpersonatedJWTInput | GdchCredentialsInput | ExternalAccountClientOptions, + json: + | JWTInput + | ImpersonatedJWTInput + | GdchCredentialsInput + | ExternalAccountClientOptions, options: AuthClientOptions = {}, ): JSONClient { let client: JSONClient; + if (!isFromJsonWarningSuppressed()) { + console.warn( + 'The `fromJSON` method is deprecated. Please use the `JWT` constructor instead. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', + ); + } + // user's preferred universe domain const preferredUniverseDomain = originalOrCamelOptions(options).get('universe_domain'); @@ -824,10 +856,14 @@ export class GoogleAuth { * @returns JWT or UserRefresh Client with data */ private _cacheClientFromJSON( - json: JWTInput | ImpersonatedJWTInput | GdchCredentialsInput | ExternalAccountClientOptions, + json: + | JWTInput + | ImpersonatedJWTInput + | GdchCredentialsInput + | ExternalAccountClientOptions, options?: AuthClientOptions, ): JSONClient { - const client = this.fromJSON(json, options); + const client = suppressFromJsonWarning(() => this.fromJSON(json, options)); // cache both raw data used to instantiate client and client itself. this.jsonContent = json; @@ -896,6 +932,12 @@ export class GoogleAuth { optionsOrCallback: AuthClientOptions | CredentialCallback = {}, callback?: CredentialCallback, ): Promise | void { + const isADC = isAdcWarningSuppressed(); + if (!isADC) { + console.warn( + 'The `fromStream` method is deprecated. Please use the `JWT` constructor with a parsed stream instead. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', + ); + } let options: AuthClientOptions = {}; if (typeof optionsOrCallback === 'function') { callback = optionsOrCallback; @@ -903,18 +945,19 @@ export class GoogleAuth { options = optionsOrCallback; } if (callback) { - this.fromStreamAsync(inputStream, options).then( + this.fromStreamAsync(inputStream, options, isADC).then( r => callback!(null, r), callback, ); } else { - return this.fromStreamAsync(inputStream, options); + return this.fromStreamAsync(inputStream, options, isADC); } } private fromStreamAsync( inputStream: stream.Readable, options?: AuthClientOptions, + isADC = false, ): Promise { return new Promise((resolve, reject) => { if (!inputStream) { @@ -931,7 +974,11 @@ export class GoogleAuth { try { try { const data = JSON.parse(chunks.join('')); - const r = this._cacheClientFromJSON(data, options); + const r = isADC + ? suppressAdcWarning(() => + this._cacheClientFromJSON(data, options), + ) + : this._cacheClientFromJSON(data, options); return resolve(r); } catch (err) { // If we failed parsing this.keyFileName, assume that it @@ -1145,6 +1192,9 @@ export class GoogleAuth { */ async getClient(): Promise { if (this.cachedCredential) { + if (this.cachedCredential instanceof JWT) { + this.setGapicJWTValues(this.cachedCredential); + } return this.cachedCredential; } @@ -1154,6 +1204,9 @@ export class GoogleAuth { try { const client = await this.#pendingAuthClient; + if (client instanceof JWT) { + this.setGapicJWTValues(client); + } if (client instanceof GdchClient && !client.apiAudience) { const opts = this.clientOptions as any; const endpoint = opts.apiEndpoint || opts.servicePath; diff --git a/core/packages/google-auth-library-nodejs/src/auth/impersonated.ts b/core/packages/google-auth-library-nodejs/src/auth/impersonated.ts index 97742ef668b3..5ee552ec26f1 100644 --- a/core/packages/google-auth-library-nodejs/src/auth/impersonated.ts +++ b/core/packages/google-auth-library-nodejs/src/auth/impersonated.ts @@ -23,7 +23,7 @@ import {AuthClient} from './authclient'; import {IdTokenProvider} from './idtokenclient'; import {GaxiosError} from 'gaxios'; import {SignBlobResponse} from './googleauth'; -import {originalOrCamelOptions} from '../util'; +import {isAdcWarningSuppressed, originalOrCamelOptions} from '../util'; export interface ImpersonatedOptions extends OAuth2ClientOptions { /** @@ -121,6 +121,11 @@ export class Impersonated extends OAuth2Client implements IdTokenProvider { */ constructor(options: ImpersonatedOptions = {}) { super(options); + if (!isAdcWarningSuppressed()) { + console.warn( + 'The `Impersonated` constructor does not validate the credential configuration. A security risk occurs when a credential configuration configured with malicious URLs is used. When the credential configuration is accepted from an untrusted source, you should validate it before using it with this method. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', + ); + } // Start with an expired refresh token, which will automatically be // refreshed before the first API call is made. this.credentials = { diff --git a/core/packages/google-auth-library-nodejs/src/util.ts b/core/packages/google-auth-library-nodejs/src/util.ts index 238ab604b109..f9c17fd8cb03 100644 --- a/core/packages/google-auth-library-nodejs/src/util.ts +++ b/core/packages/google-auth-library-nodejs/src/util.ts @@ -300,3 +300,48 @@ export function getWellKnownCertificateConfigFileLocation(): string { function _isWindows(): boolean { return os.platform().startsWith('win'); } + +let adcWarningSuppressionDepth = 0; +let fromJsonWarningSuppressionDepth = 0; + +/** + * Returns true if ADC deprecation/validation warnings are currently suppressed. + * @internal + */ +export function isAdcWarningSuppressed(): boolean { + return adcWarningSuppressionDepth > 0; +} + +/** + * Runs a synchronous callback while suppressing ADC deprecation/validation warnings. + * @internal + */ +export function suppressAdcWarning(fn: () => T): T { + adcWarningSuppressionDepth++; + try { + return fn(); + } finally { + adcWarningSuppressionDepth--; + } +} + +/** + * Returns true if the `GoogleAuth.fromJSON` deprecation warning is currently suppressed. + * @internal + */ +export function isFromJsonWarningSuppressed(): boolean { + return adcWarningSuppressionDepth > 0 || fromJsonWarningSuppressionDepth > 0; +} + +/** + * Runs a synchronous callback while suppressing the `GoogleAuth.fromJSON` deprecation warning. + * @internal + */ +export function suppressFromJsonWarning(fn: () => T): T { + fromJsonWarningSuppressionDepth++; + try { + return fn(); + } finally { + fromJsonWarningSuppressionDepth--; + } +} diff --git a/core/packages/google-auth-library-nodejs/test/test.externalclient.ts b/core/packages/google-auth-library-nodejs/test/test.externalclient.ts index d85a420b7fc3..76d9e7eee165 100644 --- a/core/packages/google-auth-library-nodejs/test/test.externalclient.ts +++ b/core/packages/google-auth-library-nodejs/test/test.externalclient.ts @@ -14,6 +14,7 @@ import * as assert from 'assert'; import {describe, it} from 'mocha'; +import * as sinon from 'sinon'; import {AwsClient} from '../src/auth/awsclient'; import {IdentityPoolClient} from '../src/auth/identitypoolclient'; import {ExternalAccountClient} from '../src/auth/externalclient'; @@ -272,5 +273,19 @@ describe('ExternalAccountClient', () => { return ExternalAccountClient.fromJSON(invalidOptions); }); }); + + it('should log a warning when called directly', () => { + const warnSpy = sinon.spy(console, 'warn'); + try { + ExternalAccountClient.fromJSON(fileSourcedOptions); + assert( + warnSpy.calledOnceWithExactly( + 'The `fromJSON` method does not validate the credential configuration. A security risk occurs when a credential configuration configured with malicious URLs is used. When the credential configuration is accepted from an untrusted source, you should validate it before using it with this method. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', + ), + ); + } finally { + warnSpy.restore(); + } + }); }); }); diff --git a/core/packages/google-auth-library-nodejs/test/test.googleauth.ts b/core/packages/google-auth-library-nodejs/test/test.googleauth.ts index dde1ceaee0ec..3bb0ecfac8db 100644 --- a/core/packages/google-auth-library-nodejs/test/test.googleauth.ts +++ b/core/packages/google-auth-library-nodejs/test/test.googleauth.ts @@ -16,7 +16,7 @@ import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; import * as child_process from 'child_process'; import * as crypto from 'crypto'; -import {CredentialRequest} from '../src/auth/credentials'; +import {CredentialRequest, CredentialBody} from '../src/auth/credentials'; import * as fs from 'fs'; import { BASE_PATH, @@ -45,7 +45,6 @@ import { AnyAuthClient, GdchClient, } from '../src'; -import {CredentialBody} from '../src/auth/credentials'; import * as envDetect from '../src/auth/envDetect'; import {Compute} from '../src/auth/computeclient'; import { @@ -3114,4 +3113,254 @@ describe('googleauth', () => { ); }); }); + + describe('credential deprecation and validation warnings', () => { + const keyFilenameWarning = + 'The `keyFilename` option is deprecated. Please use the `auth` object constructor instead. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.'; + const credentialsWarning = + 'The `credentials` option is deprecated. Please use the `auth` object constructor instead. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.'; + const fromJSONWarning = + 'The `fromJSON` method is deprecated. Please use the `JWT` constructor instead. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.'; + const fromStreamWarning = + 'The `fromStream` method is deprecated. Please use the `JWT` constructor with a parsed stream instead. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.'; + const externalAccountWarning = + 'The `fromJSON` method does not validate the credential configuration. A security risk occurs when a credential configuration configured with malicious URLs is used. When the credential configuration is accepted from an untrusted source, you should validate it before using it with this method. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.'; + const impersonatedWarning = + 'The `Impersonated` constructor does not validate the credential configuration. A security risk occurs when a credential configuration configured with malicious URLs is used. When the credential configuration is accepted from an untrusted source, you should validate it before using it with this method. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.'; + + let sandbox: sinon.SinonSandbox; + let auth: GoogleAuth; + let warnSpy: sinon.SinonSpy; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + auth = new GoogleAuth(); + warnSpy = sandbox.spy(console, 'warn'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should not warn when GoogleAuth is instantiated without keyFilename or credentials', () => { + new GoogleAuth(); + assert.strictEqual(warnSpy.called, false); + }); + + it('should warn when GoogleAuth is instantiated with keyFilename or keyFile', () => { + new GoogleAuth({keyFilename: './test/fixtures/private.json'}); + assert(warnSpy.calledOnceWithExactly(keyFilenameWarning)); + + warnSpy.resetHistory(); + new GoogleAuth({keyFile: './test/fixtures/private.json'}); + assert(warnSpy.calledOnceWithExactly(keyFilenameWarning)); + }); + + it('should warn when GoogleAuth is instantiated with credentials', () => { + new GoogleAuth({credentials: privateJSON}); + assert(warnSpy.calledOnceWithExactly(credentialsWarning)); + }); + + it('should warn when fromJSON is called directly', () => { + auth.fromJSON(privateJSON); + assert(warnSpy.calledOnceWithExactly(fromJSONWarning)); + }); + + it('should warn for both fromJSON and ExternalAccountClient when fromJSON is called with external_account', () => { + auth.fromJSON(externalAccountJSON); + assert.strictEqual(warnSpy.callCount, 2); + assert(warnSpy.firstCall.calledWithExactly(fromJSONWarning)); + assert(warnSpy.secondCall.calledWithExactly(externalAccountWarning)); + }); + + it('should warn for both fromJSON and Impersonated (without duplicate fromJSON warning) when fromJSON is called with impersonated_service_account', () => { + const impersonatedJson = JSON.parse( + fs.readFileSync( + './test/fixtures/impersonated_application_default_credentials.json', + 'utf-8', + ), + ); + auth.fromJSON(impersonatedJson); + assert.strictEqual(warnSpy.callCount, 2); + assert(warnSpy.firstCall.calledWithExactly(fromJSONWarning)); + assert(warnSpy.secondCall.calledWithExactly(impersonatedWarning)); + }); + + it('should warn when fromStream is called directly (without duplicate fromJSON warning)', async () => { + const stream = fs.createReadStream('./test/fixtures/private.json'); + await auth.fromStream(stream); + assert(warnSpy.calledOnceWithExactly(fromStreamWarning)); + }); + + it('should warn for both fromStream and ExternalAccountClient when fromStream is called with external_account', async () => { + const stream = fs.createReadStream( + './test/fixtures/external-account-cred.json', + ); + await auth.fromStream(stream); + assert.strictEqual(warnSpy.callCount, 2); + assert(warnSpy.firstCall.calledWithExactly(fromStreamWarning)); + assert(warnSpy.secondCall.calledWithExactly(externalAccountWarning)); + }); + + it('should warn for both fromStream and Impersonated when fromStream is called with impersonated_service_account', async () => { + const stream = fs.createReadStream( + './test/fixtures/impersonated_application_default_credentials.json', + ); + await auth.fromStream(stream); + assert.strictEqual(warnSpy.callCount, 2); + assert(warnSpy.firstCall.calledWithExactly(fromStreamWarning)); + assert(warnSpy.secondCall.calledWithExactly(impersonatedWarning)); + }); + + it('should warn only once for service_account via getClient() with credentials or keyFilename', async () => { + const authWithCreds = new GoogleAuth({credentials: privateJSON}); + await authWithCreds.getClient(); + assert(warnSpy.calledOnceWithExactly(credentialsWarning)); + + warnSpy.resetHistory(); + const authWithKeyFile = new GoogleAuth({ + keyFilename: './test/fixtures/private.json', + }); + await authWithKeyFile.getClient(); + assert(warnSpy.calledOnceWithExactly(keyFilenameWarning)); + }); + + it('should warn for credentials/keyFilename and ExternalAccountClient via getClient() with external_account', async () => { + const authWithCreds = new GoogleAuth({credentials: externalAccountJSON}); + await authWithCreds.getClient(); + assert.strictEqual(warnSpy.callCount, 2); + assert(warnSpy.firstCall.calledWithExactly(credentialsWarning)); + assert(warnSpy.secondCall.calledWithExactly(externalAccountWarning)); + + warnSpy.resetHistory(); + const authWithKeyFile = new GoogleAuth({ + keyFilename: './test/fixtures/external-account-cred.json', + }); + await authWithKeyFile.getClient(); + assert.strictEqual(warnSpy.callCount, 2); + assert(warnSpy.firstCall.calledWithExactly(keyFilenameWarning)); + assert(warnSpy.secondCall.calledWithExactly(externalAccountWarning)); + }); + + it('should warn for credentials/keyFilename and Impersonated via getClient() with impersonated_service_account', async () => { + const impersonatedJson = JSON.parse( + fs.readFileSync( + './test/fixtures/impersonated_application_default_credentials.json', + 'utf-8', + ), + ); + const authWithCreds = new GoogleAuth({credentials: impersonatedJson}); + await authWithCreds.getClient(); + assert.strictEqual(warnSpy.callCount, 2); + assert(warnSpy.firstCall.calledWithExactly(credentialsWarning)); + assert(warnSpy.secondCall.calledWithExactly(impersonatedWarning)); + + warnSpy.resetHistory(); + const authWithKeyFile = new GoogleAuth({ + keyFilename: + './test/fixtures/impersonated_application_default_credentials.json', + }); + await authWithKeyFile.getClient(); + assert.strictEqual(warnSpy.callCount, 2); + assert(warnSpy.firstCall.calledWithExactly(keyFilenameWarning)); + assert(warnSpy.secondCall.calledWithExactly(impersonatedWarning)); + }); + + it('should preserve this.fromJSON in the call chain for fromStream, getClient, and fromImpersonatedJSON', async () => { + const fromJsonSpy = sandbox.spy(auth, 'fromJSON'); + const stream = fs.createReadStream('./test/fixtures/private.json'); + await auth.fromStream(stream); + assert.strictEqual(fromJsonSpy.callCount, 1); + assert(warnSpy.calledOnceWithExactly(fromStreamWarning)); + + fromJsonSpy.resetHistory(); + warnSpy.resetHistory(); + const impersonatedJson = JSON.parse( + fs.readFileSync( + './test/fixtures/impersonated_application_default_credentials.json', + 'utf-8', + ), + ); + auth.fromImpersonatedJSON(impersonatedJson); + assert.strictEqual(fromJsonSpy.callCount, 1); + assert(warnSpy.calledOnceWithExactly(impersonatedWarning)); + }); + + it('should suppress warnings when loading credentials via ADC (_getApplicationCredentialsFromFilePath)', async () => { + await auth._getApplicationCredentialsFromFilePath( + './test/fixtures/private.json', + ); + await auth._getApplicationCredentialsFromFilePath( + './test/fixtures/refresh.json', + ); + await auth._getApplicationCredentialsFromFilePath( + './test/fixtures/external-account-cred.json', + ); + await auth._getApplicationCredentialsFromFilePath( + './test/fixtures/impersonated_application_default_credentials.json', + ); + assert.strictEqual(warnSpy.called, false); + }); + + it('should not suppress warnings on concurrent non-ADC calls while ADC stream is reading', async () => { + const otherAuth = new GoogleAuth(); + const adcPromise = auth._getApplicationCredentialsFromFilePath( + './test/fixtures/external-account-cred.json', + ); + // Direct fromJSON call interleaved while ADC stream I/O is in flight + otherAuth.fromJSON(privateJSON); + await adcPromise; + assert(warnSpy.calledOnceWithExactly(fromJSONWarning)); + }); + + it('should sync GAPIC JWT values on cachedCredential when getClient() is called after getProjectId()', async () => { + const authWithKeyFile = new GoogleAuth({ + keyFilename: './test/fixtures/private.json', + scopes: ['https://www.googleapis.com/auth/pubsub'], + }); + // Resolve projectId from keyFilename, which caches the JWT client before GAPIC properties are set + await authWithKeyFile.getProjectId(); + assert(authWithKeyFile.cachedCredential instanceof JWT); + assert.strictEqual( + authWithKeyFile.cachedCredential.useJWTAccessWithScope, + undefined, + ); + + // Simulate GAPIC client constructor setting properties on the shared GoogleAuth instance + authWithKeyFile.useJWTAccessWithScope = true; + authWithKeyFile.defaultServicePath = 'pubsub.googleapis.com'; + authWithKeyFile.defaultScopes = [ + 'https://www.googleapis.com/auth/pubsub', + ]; + + const client = (await authWithKeyFile.getClient()) as JWT; + assert.strictEqual(client.useJWTAccessWithScope, true); + assert.strictEqual(client.defaultServicePath, 'pubsub.googleapis.com'); + assert.deepStrictEqual(client.defaultScopes, [ + 'https://www.googleapis.com/auth/pubsub', + ]); + client.key = fs.readFileSync('./test/fixtures/private.pem', 'utf8'); + const headers = await authWithKeyFile.getRequestHeaders(); + assert(headers.get('authorization')?.startsWith('Bearer ')); + }); + + it('should not overwrite existing defined properties on JWT in setGapicJWTValues()', () => { + const jwt = new JWT({ + email: 'test@example.com', + key: 'key', + }); + jwt.useJWTAccessWithScope = false; + jwt.defaultServicePath = 'custom.googleapis.com'; + auth.useJWTAccessWithScope = undefined; + auth.defaultServicePath = undefined; + auth.defaultScopes = ['https://www.googleapis.com/auth/cloud-platform']; + + auth.setGapicJWTValues(jwt); + assert.strictEqual(jwt.useJWTAccessWithScope, false); + assert.strictEqual(jwt.defaultServicePath, 'custom.googleapis.com'); + assert.deepStrictEqual(jwt.defaultScopes, [ + 'https://www.googleapis.com/auth/cloud-platform', + ]); + }); + }); }); diff --git a/core/packages/google-auth-library-nodejs/test/test.impersonated.ts b/core/packages/google-auth-library-nodejs/test/test.impersonated.ts index e7be5ddf177b..0011d130e66f 100644 --- a/core/packages/google-auth-library-nodejs/test/test.impersonated.ts +++ b/core/packages/google-auth-library-nodejs/test/test.impersonated.ts @@ -17,6 +17,7 @@ import * as assert from 'assert'; import * as nock from 'nock'; import {describe, it, afterEach} from 'mocha'; +import * as sinon from 'sinon'; import {Impersonated, JWT, UserRefreshClient} from '../src'; import {CredentialRequest} from '../src/auth/credentials'; @@ -589,4 +590,22 @@ describe('impersonated', () => { assert.equal(resp.signedBlob, expectedSignedBlob); scopes.forEach(s => s.done()); }); + + it('should log a warning when instantiated directly', () => { + const sourceClient = createSampleJWTClient(); + const warnSpy = sinon.spy(console, 'warn'); + try { + new Impersonated({ + sourceClient, + targetPrincipal: 'target@project.iam.gserviceaccount.com', + }); + assert( + warnSpy.calledOnceWithExactly( + 'The `Impersonated` constructor does not validate the credential configuration. A security risk occurs when a credential configuration configured with malicious URLs is used. When the credential configuration is accepted from an untrusted source, you should validate it before using it with this method. For more details, see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials.', + ), + ); + } finally { + warnSpy.restore(); + } + }); });