From 9d74b7c0e8c166aa10563eabe05d2fa3b4f6d63a Mon Sep 17 00:00:00 2001 From: Caroline Rose <50421366+rosecm@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:39:11 -0500 Subject: [PATCH] feat(auth): Add option for metadata server application default credentials without project override (#12948) --- docs/get-started/authentication.md | 12 +++++- packages/cli/src/config/auth.test.ts | 4 +- packages/cli/src/config/auth.ts | 2 +- packages/cli/src/gemini.tsx | 14 +++++-- packages/cli/src/ui/auth/AuthDialog.test.tsx | 37 ++++++++++++++++++- packages/cli/src/ui/auth/AuthDialog.tsx | 14 +++++-- .../core/src/code_assist/codeAssist.test.ts | 6 +-- packages/core/src/code_assist/codeAssist.ts | 2 +- packages/core/src/code_assist/oauth2.test.ts | 10 ++--- packages/core/src/code_assist/oauth2.ts | 15 +++++--- .../core/src/core/contentGenerator.test.ts | 19 +++++++++- packages/core/src/core/contentGenerator.ts | 7 ++-- packages/core/src/telemetry/loggers.test.ts | 4 +- packages/core/src/telemetry/metrics.ts | 2 +- 14 files changed, 113 insertions(+), 35 deletions(-) diff --git a/docs/get-started/authentication.md b/docs/get-started/authentication.md index 712d6ee065..97d4d9fa50 100644 --- a/docs/get-started/authentication.md +++ b/docs/get-started/authentication.md @@ -8,13 +8,23 @@ CLI, configure **one** of the following authentication methods: - Use Gemini API key - Use Vertex AI - Headless (non-interactive) mode -- Google Cloud Shell +- Google Cloud Environments (Cloud Shell, Compute Engine, etc.) ## Quick Check: Running in Google Cloud Shell? If you are running the Gemini CLI within a Google Cloud Shell environment, authentication is typically automatic using your Cloud Shell credentials. +### Other Google Cloud Environments (e.g., Compute Engine) + +Some other Google Cloud environments, such as Compute Engine VMs, might also +support automatic authentication. In these environments, Gemini CLI can +automatically use Application Default Credentials (ADC) sourced from the +environment's metadata server. + +If automatic authentication does not occur in your environment, you will need to +use one of the interactive methods described below. + ## Authenticate in Interactive mode When you run Gemini CLI through the command-line, Gemini CLI will provide the diff --git a/packages/cli/src/config/auth.test.ts b/packages/cli/src/config/auth.test.ts index ae1af240a3..7bbaafdfcb 100644 --- a/packages/cli/src/config/auth.test.ts +++ b/packages/cli/src/config/auth.test.ts @@ -32,8 +32,8 @@ describe('validateAuthMethod', () => { expect(validateAuthMethod(AuthType.LOGIN_WITH_GOOGLE)).toBeNull(); }); - it('should return null for CLOUD_SHELL', () => { - expect(validateAuthMethod(AuthType.CLOUD_SHELL)).toBeNull(); + it('should return null for COMPUTE_ADC', () => { + expect(validateAuthMethod(AuthType.COMPUTE_ADC)).toBeNull(); }); describe('USE_GEMINI', () => { diff --git a/packages/cli/src/config/auth.ts b/packages/cli/src/config/auth.ts index 7492e09b7b..87940307e1 100644 --- a/packages/cli/src/config/auth.ts +++ b/packages/cli/src/config/auth.ts @@ -11,7 +11,7 @@ export function validateAuthMethod(authMethod: string): string | null { loadEnvironment(loadSettings().merged); if ( authMethod === AuthType.LOGIN_WITH_GOOGLE || - authMethod === AuthType.CLOUD_SHELL + authMethod === AuthType.COMPUTE_ADC ) { return null; } diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index f7a2f48064..7d7d072022 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -284,13 +284,19 @@ export async function main() { validateDnsResolutionOrder(settings.merged.advanced?.dnsResolutionOrder), ); - // Set a default auth type if one isn't set. - if (!settings.merged.security?.auth?.selectedType) { - if (process.env['CLOUD_SHELL'] === 'true') { + // Set a default auth type if one isn't set or is set to a legacy type + if ( + !settings.merged.security?.auth?.selectedType || + settings.merged.security?.auth?.selectedType === AuthType.LEGACY_CLOUD_SHELL + ) { + if ( + process.env['CLOUD_SHELL'] === 'true' || + process.env['GEMINI_CLI_USE_COMPUTE_ADC'] === 'true' + ) { settings.setValue( SettingScope.User, 'selectedAuthType', - AuthType.CLOUD_SHELL, + AuthType.COMPUTE_ADC, ); } } diff --git a/packages/cli/src/ui/auth/AuthDialog.test.tsx b/packages/cli/src/ui/auth/AuthDialog.test.tsx index 0059e8202a..ec7f70dd36 100644 --- a/packages/cli/src/ui/auth/AuthDialog.test.tsx +++ b/packages/cli/src/ui/auth/AuthDialog.test.tsx @@ -109,8 +109,41 @@ describe('AuthDialog', () => { const items = mockedRadioButtonSelect.mock.calls[0][0].items; expect(items).toContainEqual({ label: 'Use Cloud Shell user credentials', - value: AuthType.CLOUD_SHELL, - key: AuthType.CLOUD_SHELL, + value: AuthType.COMPUTE_ADC, + key: AuthType.COMPUTE_ADC, + }); + }); + + it('does not show metadata server application default credentials option in Cloud Shell environment', () => { + process.env['CLOUD_SHELL'] = 'true'; + renderWithProviders(); + const items = mockedRadioButtonSelect.mock.calls[0][0].items; + expect(items).not.toContainEqual({ + label: 'Use metadata server application default credentials', + value: AuthType.COMPUTE_ADC, + key: AuthType.COMPUTE_ADC, + }); + }); + + it('shows metadata server application default credentials option when GEMINI_CLI_USE_COMPUTE_ADC env var is true', () => { + process.env['GEMINI_CLI_USE_COMPUTE_ADC'] = 'true'; + renderWithProviders(); + const items = mockedRadioButtonSelect.mock.calls[0][0].items; + expect(items).toContainEqual({ + label: 'Use metadata server application default credentials', + value: AuthType.COMPUTE_ADC, + key: AuthType.COMPUTE_ADC, + }); + }); + + it('does not show Cloud Shell option when when GEMINI_CLI_USE_COMPUTE_ADC env var is true', () => { + process.env['GEMINI_CLI_USE_COMPUTE_ADC'] = 'true'; + renderWithProviders(); + const items = mockedRadioButtonSelect.mock.calls[0][0].items; + expect(items).not.toContainEqual({ + label: 'Use Cloud Shell user credentials', + value: AuthType.COMPUTE_ADC, + key: AuthType.COMPUTE_ADC, }); }); diff --git a/packages/cli/src/ui/auth/AuthDialog.tsx b/packages/cli/src/ui/auth/AuthDialog.tsx index 61ea01764d..ecd51f6ed4 100644 --- a/packages/cli/src/ui/auth/AuthDialog.tsx +++ b/packages/cli/src/ui/auth/AuthDialog.tsx @@ -50,11 +50,19 @@ export function AuthDialog({ ? [ { label: 'Use Cloud Shell user credentials', - value: AuthType.CLOUD_SHELL, - key: AuthType.CLOUD_SHELL, + value: AuthType.COMPUTE_ADC, + key: AuthType.COMPUTE_ADC, }, ] - : []), + : process.env['GEMINI_CLI_USE_COMPUTE_ADC'] === 'true' + ? [ + { + label: 'Use metadata server application default credentials', + value: AuthType.COMPUTE_ADC, + key: AuthType.COMPUTE_ADC, + }, + ] + : []), { label: 'Use Gemini API Key', value: AuthType.USE_GEMINI, diff --git a/packages/core/src/code_assist/codeAssist.test.ts b/packages/core/src/code_assist/codeAssist.test.ts index 1608a7d976..0974e2237e 100644 --- a/packages/core/src/code_assist/codeAssist.test.ts +++ b/packages/core/src/code_assist/codeAssist.test.ts @@ -68,18 +68,18 @@ describe('codeAssist', () => { expect(generator).toBeInstanceOf(MockedCodeAssistServer); }); - it('should create a server for CLOUD_SHELL', async () => { + it('should create a server for COMPUTE_ADC', async () => { mockedGetOauthClient.mockResolvedValue(mockAuthClient as never); mockedSetupUser.mockResolvedValue(mockUserData); const generator = await createCodeAssistContentGenerator( httpOptions, - AuthType.CLOUD_SHELL, + AuthType.COMPUTE_ADC, mockConfig, ); expect(getOauthClient).toHaveBeenCalledWith( - AuthType.CLOUD_SHELL, + AuthType.COMPUTE_ADC, mockConfig, ); expect(setupUser).toHaveBeenCalledWith(mockAuthClient); diff --git a/packages/core/src/code_assist/codeAssist.ts b/packages/core/src/code_assist/codeAssist.ts index c8ade92edd..f8c9ac47b8 100644 --- a/packages/core/src/code_assist/codeAssist.ts +++ b/packages/core/src/code_assist/codeAssist.ts @@ -21,7 +21,7 @@ export async function createCodeAssistContentGenerator( ): Promise { if ( authType === AuthType.LOGIN_WITH_GOOGLE || - authType === AuthType.CLOUD_SHELL + authType === AuthType.COMPUTE_ADC ) { const authClient = await getOauthClient(authType, config); const userData = await setupUser(authClient); diff --git a/packages/core/src/code_assist/oauth2.test.ts b/packages/core/src/code_assist/oauth2.test.ts index b15a7aa89b..024f4ca739 100644 --- a/packages/core/src/code_assist/oauth2.test.ts +++ b/packages/core/src/code_assist/oauth2.test.ts @@ -318,7 +318,7 @@ describe('oauth2', () => { }); it('should use Compute to get a client if no cached credentials exist', async () => { - await getOauthClient(AuthType.CLOUD_SHELL, mockConfig); + await getOauthClient(AuthType.COMPUTE_ADC, mockConfig); expect(Compute).toHaveBeenCalledWith({}); expect(mockGetAccessToken).toHaveBeenCalled(); @@ -329,7 +329,7 @@ describe('oauth2', () => { mockComputeClient.credentials = newCredentials; mockGetAccessToken.mockResolvedValue({ token: 'new-adc-token' }); - await getOauthClient(AuthType.CLOUD_SHELL, mockConfig); + await getOauthClient(AuthType.COMPUTE_ADC, mockConfig); const credsPath = path.join( tempHomeDir, @@ -340,7 +340,7 @@ describe('oauth2', () => { }); it('should return the Compute client on successful ADC authentication', async () => { - const client = await getOauthClient(AuthType.CLOUD_SHELL, mockConfig); + const client = await getOauthClient(AuthType.COMPUTE_ADC, mockConfig); expect(client).toBe(mockComputeClient); }); @@ -349,9 +349,9 @@ describe('oauth2', () => { mockGetAccessToken.mockRejectedValue(testError); await expect( - getOauthClient(AuthType.CLOUD_SHELL, mockConfig), + getOauthClient(AuthType.COMPUTE_ADC, mockConfig), ).rejects.toThrow( - 'Could not authenticate using Cloud Shell credentials. Please select a different authentication method or ensure you are in a properly configured environment. Error: ADC Failed', + 'Could not authenticate using metadata server application default credentials. Please select a different authentication method or ensure you are in a properly configured environment. Error: ADC Failed', ); }); }); diff --git a/packages/core/src/code_assist/oauth2.ts b/packages/core/src/code_assist/oauth2.ts index 46ff9fcb00..4382195adb 100644 --- a/packages/core/src/code_assist/oauth2.ts +++ b/packages/core/src/code_assist/oauth2.ts @@ -155,12 +155,15 @@ async function initOauthClient( } } - // In Google Cloud Shell, we can use Application Default Credentials (ADC) - // provided via its metadata server to authenticate non-interactively using - // the identity of the user logged into Cloud Shell. - if (authType === AuthType.CLOUD_SHELL) { + // In Google Compute Engine based environments (including Cloud Shell), we can + // use Application Default Credentials (ADC) provided via its metadata server + // to authenticate non-interactively using the identity of the logged-in user. + if (authType === AuthType.COMPUTE_ADC) { try { - debugLogger.log("Attempting to authenticate via Cloud Shell VM's ADC."); + debugLogger.log( + 'Attempting to authenticate via metadata server application default credentials.', + ); + const computeClient = new Compute({ // We can leave this empty, since the metadata server will provide // the service account email. @@ -172,7 +175,7 @@ async function initOauthClient( return computeClient; } catch (e) { throw new Error( - `Could not authenticate using Cloud Shell credentials. Please select a different authentication method or ensure you are in a properly configured environment. Error: ${getErrorMessage( + `Could not authenticate using metadata server application default credentials. Please select a different authentication method or ensure you are in a properly configured environment. Error: ${getErrorMessage( e, )}`, ); diff --git a/packages/core/src/core/contentGenerator.test.ts b/packages/core/src/core/contentGenerator.test.ts index ac5cb2f13e..c31585f7a8 100644 --- a/packages/core/src/core/contentGenerator.test.ts +++ b/packages/core/src/core/contentGenerator.test.ts @@ -67,7 +67,7 @@ describe('createContentGenerator', () => { expect(generator).toBeInstanceOf(RecordingContentGenerator); }); - it('should create a CodeAssistContentGenerator', async () => { + it('should create a CodeAssistContentGenerator when AuthType is LOGIN_WITH_GOOGLE', async () => { const mockGenerator = {} as unknown as ContentGenerator; vi.mocked(createCodeAssistContentGenerator).mockResolvedValue( mockGenerator as never, @@ -84,6 +84,23 @@ describe('createContentGenerator', () => { ); }); + it('should create a CodeAssistContentGenerator when AuthType is COMPUTE_ADC', async () => { + const mockGenerator = {} as unknown as ContentGenerator; + vi.mocked(createCodeAssistContentGenerator).mockResolvedValue( + mockGenerator as never, + ); + const generator = await createContentGenerator( + { + authType: AuthType.COMPUTE_ADC, + }, + mockConfig, + ); + expect(createCodeAssistContentGenerator).toHaveBeenCalled(); + expect(generator).toEqual( + new LoggingContentGenerator(mockGenerator, mockConfig), + ); + }); + it('should create a GoogleGenAI content generator', async () => { const mockConfig = { getUsageStatisticsEnabled: () => true, diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index 6fac941e01..4e37a9c0cc 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -48,7 +48,8 @@ export enum AuthType { LOGIN_WITH_GOOGLE = 'oauth-personal', USE_GEMINI = 'gemini-api-key', USE_VERTEX_AI = 'vertex-ai', - CLOUD_SHELL = 'cloud-shell', + LEGACY_CLOUD_SHELL = 'cloud-shell', + COMPUTE_ADC = 'compute-default-credentials', } export type ContentGeneratorConfig = { @@ -79,7 +80,7 @@ export async function createContentGeneratorConfig( // If we are using Google auth or we are in Cloud Shell, there is nothing else to validate for now if ( authType === AuthType.LOGIN_WITH_GOOGLE || - authType === AuthType.CLOUD_SHELL + authType === AuthType.COMPUTE_ADC ) { return contentGeneratorConfig; } @@ -120,7 +121,7 @@ export async function createContentGenerator( }; if ( config.authType === AuthType.LOGIN_WITH_GOOGLE || - config.authType === AuthType.CLOUD_SHELL + config.authType === AuthType.COMPUTE_ADC ) { const httpOptions = { headers: baseHeaders }; return new LoggingContentGenerator( diff --git a/packages/core/src/telemetry/loggers.test.ts b/packages/core/src/telemetry/loggers.test.ts index 46357d699f..4be2ec6eeb 100644 --- a/packages/core/src/telemetry/loggers.test.ts +++ b/packages/core/src/telemetry/loggers.test.ts @@ -298,7 +298,7 @@ describe('loggers', () => { const event = new UserPromptEvent( 11, 'prompt-id-9', - AuthType.CLOUD_SHELL, + AuthType.COMPUTE_ADC, 'test-prompt', ); @@ -315,7 +315,7 @@ describe('loggers', () => { interactive: false, prompt_length: 11, prompt_id: 'prompt-id-9', - auth_type: 'cloud-shell', + auth_type: 'compute-default-credentials', }, }); }); diff --git a/packages/core/src/telemetry/metrics.ts b/packages/core/src/telemetry/metrics.ts index f2078aa62d..694643a2cc 100644 --- a/packages/core/src/telemetry/metrics.ts +++ b/packages/core/src/telemetry/metrics.ts @@ -886,7 +886,7 @@ export function getConventionAttributes(event: { function getGenAiProvider(authType?: string): GenAiProviderName { switch (authType) { case AuthType.USE_VERTEX_AI: - case AuthType.CLOUD_SHELL: + case AuthType.COMPUTE_ADC: case AuthType.LOGIN_WITH_GOOGLE: return GenAiProviderName.GCP_VERTEX_AI; case AuthType.USE_GEMINI: