Skip to content

Commit 9e8f7c0

Browse files
authored
Create BYOID auth client when detecting BYOID credentials (#11592)
1 parent 44c62c8 commit 9e8f7c0

4 files changed

Lines changed: 112 additions & 42 deletions

File tree

packages/core/src/code_assist/oauth2.test.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
clearOauthClientCache,
1515
} from './oauth2.js';
1616
import { UserAccountManager } from '../utils/userAccountManager.js';
17-
import { OAuth2Client, Compute } from 'google-auth-library';
17+
import { OAuth2Client, Compute, GoogleAuth } from 'google-auth-library';
1818
import * as fs from 'node:fs';
1919
import * as path from 'node:path';
2020
import http from 'node:http';
@@ -420,6 +420,53 @@ describe('oauth2', () => {
420420
// Assert the correct credentials were used
421421
expect(mockClient.setCredentials).toHaveBeenCalledWith(envCreds);
422422
});
423+
424+
it('should use GoogleAuth for BYOID credentials from GOOGLE_APPLICATION_CREDENTIALS', async () => {
425+
// Setup BYOID credentials via environment variable
426+
const byoidCredentials = {
427+
type: 'external_account_authorized_user',
428+
client_id: 'mock-client-id',
429+
};
430+
const envCredsPath = path.join(tempHomeDir, 'byoid_creds.json');
431+
await fs.promises.writeFile(
432+
envCredsPath,
433+
JSON.stringify(byoidCredentials),
434+
);
435+
vi.stubEnv('GOOGLE_APPLICATION_CREDENTIALS', envCredsPath);
436+
437+
// Mock GoogleAuth and its chain of calls
438+
const mockExternalAccountClient = {
439+
getAccessToken: vi.fn().mockResolvedValue({ token: 'byoid-token' }),
440+
};
441+
const mockFromJSON = vi
442+
.fn()
443+
.mockResolvedValue(mockExternalAccountClient);
444+
const mockGoogleAuthInstance = {
445+
fromJSON: mockFromJSON,
446+
};
447+
(GoogleAuth as unknown as Mock).mockImplementation(
448+
() => mockGoogleAuthInstance,
449+
);
450+
451+
const mockOAuth2Client = {
452+
on: vi.fn(),
453+
};
454+
(OAuth2Client as unknown as Mock).mockImplementation(
455+
() => mockOAuth2Client,
456+
);
457+
458+
const client = await getOauthClient(
459+
AuthType.LOGIN_WITH_GOOGLE,
460+
mockConfig,
461+
);
462+
463+
// Assert that GoogleAuth was used and the correct client was returned
464+
expect(GoogleAuth).toHaveBeenCalledWith({
465+
scopes: expect.any(Array),
466+
});
467+
expect(mockFromJSON).toHaveBeenCalledWith(byoidCredentials);
468+
expect(client).toBe(mockExternalAccountClient);
469+
});
423470
});
424471

425472
describe('with GCP environment variables', () => {

packages/core/src/code_assist/oauth2.ts

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import type { Credentials } from 'google-auth-library';
7+
import type { Credentials, AuthClient, JWTInput } from 'google-auth-library';
88
import {
99
OAuth2Client,
1010
Compute,
1111
CodeChallengeMethod,
12+
GoogleAuth,
1213
} from 'google-auth-library';
1314
import * as http from 'node:http';
1415
import url from 'node:url';
@@ -64,7 +65,7 @@ export interface OauthWebLogin {
6465
loginCompletePromise: Promise<void>;
6566
}
6667

67-
const oauthClientPromises = new Map<AuthType, Promise<OAuth2Client>>();
68+
const oauthClientPromises = new Map<AuthType, Promise<AuthClient>>();
6869

6970
function getUseEncryptedStorageFlag() {
7071
return process.env[FORCE_ENCRYPTED_FILE_ENV_VAR] === 'true';
@@ -73,7 +74,28 @@ function getUseEncryptedStorageFlag() {
7374
async function initOauthClient(
7475
authType: AuthType,
7576
config: Config,
76-
): Promise<OAuth2Client> {
77+
): Promise<AuthClient> {
78+
const credentials = await fetchCachedCredentials();
79+
80+
if (
81+
credentials &&
82+
(credentials as { type?: string }).type ===
83+
'external_account_authorized_user'
84+
) {
85+
const auth = new GoogleAuth({
86+
scopes: OAUTH_SCOPE,
87+
});
88+
const byoidClient = await auth.fromJSON({
89+
...credentials,
90+
refresh_token: credentials.refresh_token ?? undefined,
91+
});
92+
const token = await byoidClient.getAccessToken();
93+
if (token) {
94+
debugLogger.debug('Created BYOID auth client.');
95+
return byoidClient;
96+
}
97+
}
98+
7799
const client = new OAuth2Client({
78100
clientId: OAUTH_CLIENT_ID,
79101
clientSecret: OAUTH_CLIENT_SECRET,
@@ -102,20 +124,35 @@ async function initOauthClient(
102124
}
103125
});
104126

105-
// If there are cached creds on disk, they always take precedence
106-
if (await loadCachedCredentials(client)) {
107-
// Found valid cached credentials.
108-
// Check if we need to retrieve Google Account ID or Email
109-
if (!userAccountManager.getCachedGoogleAccount()) {
110-
try {
111-
await fetchAndCacheUserInfo(client);
112-
} catch (error) {
113-
// Non-fatal, continue with existing auth.
114-
debugLogger.warn('Failed to fetch user info:', getErrorMessage(error));
127+
if (credentials) {
128+
client.setCredentials(credentials as Credentials);
129+
try {
130+
// This will verify locally that the credentials look good.
131+
const { token } = await client.getAccessToken();
132+
if (token) {
133+
// This will check with the server to see if it hasn't been revoked.
134+
await client.getTokenInfo(token);
135+
136+
if (!userAccountManager.getCachedGoogleAccount()) {
137+
try {
138+
await fetchAndCacheUserInfo(client);
139+
} catch (error) {
140+
// Non-fatal, continue with existing auth.
141+
debugLogger.warn(
142+
'Failed to fetch user info:',
143+
getErrorMessage(error),
144+
);
145+
}
146+
}
147+
debugLogger.log('Loaded cached credentials.');
148+
return client;
115149
}
150+
} catch (error) {
151+
debugLogger.debug(
152+
`Cached credentials are not valid:`,
153+
getErrorMessage(error),
154+
);
116155
}
117-
debugLogger.log('Loaded cached credentials.');
118-
return client;
119156
}
120157

121158
// In Google Cloud Shell, we can use Application Default Credentials (ADC)
@@ -218,7 +255,7 @@ async function initOauthClient(
218255
export async function getOauthClient(
219256
authType: AuthType,
220257
config: Config,
221-
): Promise<OAuth2Client> {
258+
): Promise<AuthClient> {
222259
if (!oauthClientPromises.has(authType)) {
223260
oauthClientPromises.set(authType, initOauthClient(authType, config));
224261
}
@@ -432,15 +469,12 @@ export function getAvailablePort(): Promise<number> {
432469
});
433470
}
434471

435-
async function loadCachedCredentials(client: OAuth2Client): Promise<boolean> {
472+
async function fetchCachedCredentials(): Promise<
473+
Credentials | JWTInput | null
474+
> {
436475
const useEncryptedStorage = getUseEncryptedStorageFlag();
437476
if (useEncryptedStorage) {
438-
const credentials = await OAuthCredentialStorage.loadCredentials();
439-
if (credentials) {
440-
client.setCredentials(credentials);
441-
return true;
442-
}
443-
return false;
477+
return await OAuthCredentialStorage.loadCredentials();
444478
}
445479

446480
const pathsToTry = [
@@ -450,19 +484,8 @@ async function loadCachedCredentials(client: OAuth2Client): Promise<boolean> {
450484

451485
for (const keyFile of pathsToTry) {
452486
try {
453-
const creds = await fs.readFile(keyFile, 'utf-8');
454-
client.setCredentials(JSON.parse(creds));
455-
456-
// This will verify locally that the credentials look good.
457-
const { token } = await client.getAccessToken();
458-
if (!token) {
459-
continue;
460-
}
461-
462-
// This will check with the server to see if it hasn't been revoked.
463-
await client.getTokenInfo(token);
464-
465-
return true;
487+
const keyFileString = await fs.readFile(keyFile, 'utf-8');
488+
return JSON.parse(keyFileString);
466489
} catch (error) {
467490
// Log specific error for debugging, but continue trying other paths
468491
debugLogger.debug(
@@ -472,7 +495,7 @@ async function loadCachedCredentials(client: OAuth2Client): Promise<boolean> {
472495
}
473496
}
474497

475-
return false;
498+
return null;
476499
}
477500

478501
async function cacheCredentials(credentials: Credentials) {

packages/core/src/code_assist/server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import type { OAuth2Client } from 'google-auth-library';
7+
import type { AuthClient } from 'google-auth-library';
88
import type {
99
CodeAssistGlobalUserSettingResponse,
1010
GoogleRpcResponse,
@@ -47,7 +47,7 @@ export const CODE_ASSIST_API_VERSION = 'v1internal';
4747

4848
export class CodeAssistServer implements ContentGenerator {
4949
constructor(
50-
readonly client: OAuth2Client,
50+
readonly client: AuthClient,
5151
readonly projectId?: string,
5252
readonly httpOptions: HttpOptions = {},
5353
readonly sessionId?: string,

packages/core/src/code_assist/setup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212
} from './types.js';
1313
import { UserTierId } from './types.js';
1414
import { CodeAssistServer } from './server.js';
15-
import type { OAuth2Client } from 'google-auth-library';
15+
import type { AuthClient } from 'google-auth-library';
1616

1717
export class ProjectIdRequiredError extends Error {
1818
constructor() {
@@ -32,7 +32,7 @@ export interface UserData {
3232
* @param projectId the user's project id, if any
3333
* @returns the user's actual project id
3434
*/
35-
export async function setupUser(client: OAuth2Client): Promise<UserData> {
35+
export async function setupUser(client: AuthClient): Promise<UserData> {
3636
const projectId =
3737
process.env['GOOGLE_CLOUD_PROJECT'] ||
3838
process.env['GOOGLE_CLOUD_PROJECT_ID'] ||

0 commit comments

Comments
 (0)