From 4a3617cb11ed3a310dac9cfe7ebd2bdf7db5b27f Mon Sep 17 00:00:00 2001 From: Santi Quiroga Date: Fri, 23 Oct 2026 23:53:48 +0000 Subject: [PATCH] fix(pubsub): reuse GoogleAuth instance and deprecate credential options --- handwritten/pubsub/src/pubsub.ts | 61 +++++++++++++--- handwritten/pubsub/test/pubsub.ts | 111 +++++++++++++++++++++++++++++- 2 files changed, 161 insertions(+), 11 deletions(-) diff --git a/handwritten/pubsub/src/pubsub.ts b/handwritten/pubsub/src/pubsub.ts index 6bd991daff0b..0cdb83f035bd 100644 --- a/handwritten/pubsub/src/pubsub.ts +++ b/handwritten/pubsub/src/pubsub.ts @@ -17,7 +17,12 @@ import {paginator} from '@google-cloud/paginator'; import {replaceProjectIdToken} from '@google-cloud/projectify'; import * as extend from 'extend'; -import {AuthClient, GoogleAuth, GoogleAuthOptions} from 'google-auth-library'; +import { + AuthClient, + GoogleAuth, + GoogleAuthOptions, + JWT, +} from 'google-auth-library'; import * as gax from 'google-gax'; import {v1} from './'; @@ -127,10 +132,12 @@ type GetAllSubscriptionsResponse = PagedResponse< >; export type GetSubscriptionsCallback = - GetAllSubscriptionsCallback | GetTopicSubscriptionsCallback; + | GetAllSubscriptionsCallback + | GetTopicSubscriptionsCallback; export type GetSubscriptionsResponse = - GetAllSubscriptionsResponse | GetTopicSubscriptionsResponse; + | GetAllSubscriptionsResponse + | GetTopicSubscriptionsResponse; export type GetTopicsCallback = RequestCallback< Topic, @@ -187,7 +194,8 @@ export interface PagedCallback { } export type PagedResponse = - [Item[]] | [Item[], {} | null, Response]; + | [Item[]] + | [Item[], {} | null, Response]; export type ObjectStream = { addListener(event: 'data', listener: (data: O) => void): ObjectStream; @@ -214,10 +222,13 @@ interface GetClientCallback { * https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application * Application Default Credentials}, your project ID will be detected * automatically. - * @property {string} [keyFilename] Full path to the a .json, .pem, or .p12 key - * downloaded from the Google Developers Console. If you provide a path to a - * JSON file, the `projectId` option above is not necessary. NOTE: .pem and - * .p12 require you to specify the `email` option as well. + * @property {string} [keyFilename] @deprecated 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. + * Full path to the a .json, .pem, or .p12 key downloaded from the Google + * Developers Console. If you provide a path to a JSON file, the `projectId` + * option above is not necessary. NOTE: .pem and .p12 require you to specify + * the `email` option as well. * @property {string} [apiEndpoint] The `apiEndpoint` from options will set the * host. If not set, the `PUBSUB_EMULATOR_HOST` environment variable from the * gcloud SDK is honored. We also check the `CLOUD_API_ENDPOINT_OVERRIDES_PUBSUB` @@ -226,7 +237,10 @@ interface GetClientCallback { * we will assume that it's an emulator and disable strict SSL checks. * @property {string} [email] Account email address. Required when using a .pem * or .p12 keyFilename. - * @property {object} [credentials] Credentials object. + * @property {object} [credentials] @deprecated 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. + * Credentials object. * @property {string} [credentials.client_email] * @property {string} [credentials.private_key] * @property {boolean} [autoRetry=true] Automatically retry requests if the @@ -332,7 +346,9 @@ export class PubSub { this.isEmulator = false; this.determineBaseUrl_(); this.api = {}; - this.auth = new GoogleAuth(this.options as GoogleAuthOptions); + this.auth = + this.options.auth || + new GoogleAuth(this.options as GoogleAuthOptions); this.projectId = this.options.projectId || PROJECT_ID_PLACEHOLDER; if (this.projectId !== PROJECT_ID_PLACEHOLDER) { this.name = PubSub.formatName_(this.projectId); @@ -1246,6 +1262,7 @@ export class PubSub { this.options.projectId = projectId!; } + this.options.auth ??= this.auth; return this.options; } @@ -1259,6 +1276,18 @@ export class PubSub { if (!this.schemaClient) { const options = (await this.getClientConfig()) as gax.ClientOptions; this.schemaClient = new v1.SchemaServiceClient(options); + if ( + this.auth.cachedCredential && + 'useJWTAccessWithScope' in this.auth.cachedCredential + ) { + const jwt = this.auth.cachedCredential as JWT; + const {defaultServicePath, useJWTAccessWithScope, defaultScopes} = jwt; + this.auth.setGapicJWTValues?.(jwt); + jwt.defaultServicePath = defaultServicePath ?? jwt.defaultServicePath; + jwt.useJWTAccessWithScope = + useJWTAccessWithScope ?? jwt.useJWTAccessWithScope; + jwt.defaultScopes = defaultScopes ?? jwt.defaultScopes; + } } return this.schemaClient!; @@ -1308,6 +1337,18 @@ export class PubSub { if (!gaxClient) { // Lazily instantiate client. gaxClient = new v1[config.client](options) as unknown as gax.ClientStub; + if ( + this.auth.cachedCredential && + 'useJWTAccessWithScope' in this.auth.cachedCredential + ) { + const jwt = this.auth.cachedCredential as JWT; + const {defaultServicePath, useJWTAccessWithScope, defaultScopes} = jwt; + this.auth.setGapicJWTValues?.(jwt); + jwt.defaultServicePath = defaultServicePath ?? jwt.defaultServicePath; + jwt.useJWTAccessWithScope = + useJWTAccessWithScope ?? jwt.useJWTAccessWithScope; + jwt.defaultScopes = defaultScopes ?? jwt.defaultScopes; + } this.api[config.client] = gaxClient; } diff --git a/handwritten/pubsub/test/pubsub.ts b/handwritten/pubsub/test/pubsub.ts index b2ecbfa7d29f..a2d206e00d34 100644 --- a/handwritten/pubsub/test/pubsub.ts +++ b/handwritten/pubsub/test/pubsub.ts @@ -196,7 +196,7 @@ describe('PubSub', () => { './snapshot': {Snapshot: FakeSnapshot}, './subscription': {Subscription}, './topic': {Topic: FakeTopic}, - './index': {v1:v1Override}, + './index': {v1: v1Override}, './util': fakeUtil, }).PubSub; }); @@ -340,6 +340,19 @@ describe('PubSub', () => { assert.strictEqual(pubsub.auth, fakeGoogleAuthInstance); }); + it('should reuse an existing GoogleAuth instance if provided in options.auth', () => { + const existingAuth = {} as gax.GoogleAuth; + let googleAuthCalled = false; + googleAuthOverride = () => { + googleAuthCalled = true; + return {}; + }; + + const pubsub = new PubSub({auth: existingAuth}); + assert.strictEqual(pubsub.auth, existingAuth); + assert.strictEqual(googleAuthCalled, false); + }); + it('should localize the options provided', () => { const expectedOptions = Object.assign({}, DEFAULT_OPTIONS, OPTIONS); @@ -1553,12 +1566,64 @@ describe('PubSub', () => { options: pubsubTypes.ClientConfig, ) { assert.strictEqual(options, pubsub.options); + assert.strictEqual(options.auth, pubsub.auth); return FAKE_CLIENT_INSTANCE; }; const client = await pubsub.getClientAsync_(CONFIG); assert.strictEqual(client, FAKE_CLIENT_INSTANCE); }); + + it('should sync GAPIC JWT values on cachedCredential after instantiating client without overwriting defined properties', async () => { + pubsub.projectId = PROJECT_ID_PLACEHOLDER; + const fakeJwt: { + useJWTAccessWithScope?: boolean; + defaultServicePath?: string; + defaultScopes?: string[]; + } = { + useJWTAccessWithScope: undefined, + defaultServicePath: 'custom.googleapis.com', + defaultScopes: undefined, + }; + const setGapicJWTValuesSpy = sandbox.spy(function ( + this: { + useJWTAccessWithScope?: boolean; + defaultServicePath?: string; + defaultScopes?: string[]; + }, + client: typeof fakeJwt, + ) { + // Simulate older google-auth-library setGapicJWTValues using '=' instead of '??=' + client.defaultServicePath = this.defaultServicePath; + client.useJWTAccessWithScope = this.useJWTAccessWithScope; + client.defaultScopes = this.defaultScopes; + }); + sandbox.stub(pubsub, 'auth').value({ + cachedCredential: null, + useJWTAccessWithScope: true, + defaultServicePath: 'pubsub.googleapis.com', + defaultScopes: ['https://www.googleapis.com/auth/pubsub'], + getProjectId: async function () { + // Simulate getProjectId() caching a JWT client before GAPIC constructor runs + this.cachedCredential = fakeJwt; + return PROJECT_ID; + }, + setGapicJWTValues: setGapicJWTValuesSpy, + }); + + // tslint:disable-next-line only-arrow-functions no-any + v1ClientOverrides.FakeClient = function () { + return FAKE_CLIENT_INSTANCE; + }; + + await pubsub.getClientAsync_(CONFIG); + assert(setGapicJWTValuesSpy.calledOnceWithExactly(fakeJwt)); + assert.strictEqual(fakeJwt.useJWTAccessWithScope, true); + assert.strictEqual(fakeJwt.defaultServicePath, 'custom.googleapis.com'); + assert.deepStrictEqual(fakeJwt.defaultScopes, [ + 'https://www.googleapis.com/auth/pubsub', + ]); + }); }); describe('getClient_', () => { @@ -1886,5 +1951,49 @@ describe('PubSub', () => { await pubsub.validateSchema!(ischema, {}); assert.ok(called); }); + + it('syncs GAPIC JWT values on cachedCredential in getSchemaClient()', async () => { + pubsub.projectId = PROJECT_ID_PLACEHOLDER; + const fakeJwt: { + useJWTAccessWithScope?: boolean; + defaultServicePath?: string; + defaultScopes?: string[]; + } = { + useJWTAccessWithScope: undefined, + defaultServicePath: undefined, + defaultScopes: ['https://www.googleapis.com/auth/custom'], + }; + const setGapicJWTValuesSpy = sandbox.spy(function ( + this: { + useJWTAccessWithScope?: boolean; + defaultServicePath?: string; + defaultScopes?: string[]; + }, + client: typeof fakeJwt, + ) { + client.defaultServicePath = this.defaultServicePath; + client.useJWTAccessWithScope = this.useJWTAccessWithScope; + client.defaultScopes = this.defaultScopes; + }); + sandbox.stub(pubsub, 'auth').value({ + cachedCredential: null, + getProjectId: async function () { + this.cachedCredential = fakeJwt; + return PROJECT_ID; + }, + setGapicJWTValues: setGapicJWTValuesSpy, + }); + + const client = await pubsub.getSchemaClient(); + assert(setGapicJWTValuesSpy.calledOnceWithExactly(fakeJwt)); + assert.strictEqual(fakeJwt.useJWTAccessWithScope, true); + assert.strictEqual(fakeJwt.defaultServicePath, 'pubsub.googleapis.com'); + assert.deepStrictEqual(fakeJwt.defaultScopes, [ + 'https://www.googleapis.com/auth/custom', + ]); + sandbox.stub(client!, 'close').resolves(); + await pubsub.close(); + sandbox.restore(); + }); }); });