Automatically detect non-interactive environments and fall back to a manual, code-based authentication flow (#4475)

This commit is contained in:
Marat Boshernitsan 2025-07-18 17:22:50 -07:00 committed by GitHub
parent 003609239f
commit 5b7b6fe608
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 73 additions and 8 deletions

View File

@ -37,6 +37,7 @@ import {
logUserPrompt,
AuthType,
getOauthClient,
shouldAttemptBrowserLaunch,
} from '@google/gemini-cli-core';
import { validateAuthMethod } from './config/auth.js';
import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js';
@ -184,7 +185,7 @@ export async function main() {
if (
settings.merged.selectedAuthType === AuthType.LOGIN_WITH_GOOGLE &&
config.getNoBrowser()
(config.getNoBrowser() || !shouldAttemptBrowserLaunch())
) {
// Do oauth before app renders to make copying the link possible.
await getOauthClient(settings.merged.selectedAuthType, config);

View File

@ -11,6 +11,7 @@ import {
Config,
clearCachedCredentialFile,
getErrorMessage,
shouldAttemptBrowserLaunch,
} from '@google/gemini-cli-core';
import { runExitCleanup } from '../../utils/cleanup.js';
@ -56,7 +57,10 @@ export const useAuthCommand = (
if (authType) {
await clearCachedCredentialFile();
settings.setValue(scope, 'selectedAuthType', authType);
if (authType === AuthType.LOGIN_WITH_GOOGLE && config.getNoBrowser()) {
if (
authType === AuthType.LOGIN_WITH_GOOGLE &&
(config.getNoBrowser() || !shouldAttemptBrowserLaunch())
) {
runExitCleanup();
console.log(
`

View File

@ -31,6 +31,9 @@ vi.mock('http');
vi.mock('open');
vi.mock('crypto');
vi.mock('node:readline');
vi.mock('../utils/browser.js', () => ({
shouldAttemptBrowserLaunch: () => true,
}));
const mockConfig = {
getNoBrowser: () => false,
@ -83,7 +86,7 @@ describe('oauth2', () => {
);
vi.spyOn(crypto, 'randomBytes').mockReturnValue(mockState as never);
(open as Mock).mockImplementation(async () => ({}) as never);
(open as Mock).mockImplementation(async () => ({ on: vi.fn() }) as never);
// Mock the UserInfo API response
(global.fetch as Mock).mockResolvedValue({
@ -236,7 +239,7 @@ describe('oauth2', () => {
expect(mockGetToken).toHaveBeenCalledWith({
code: mockCode,
codeVerifier: mockCodeVerifier.codeVerifier,
redirect_uri: 'https://sdk.cloud.google.com/authcode_cloudcode.html',
redirect_uri: 'https://codeassist.google.com/authcode',
});
expect(mockSetCredentials).toHaveBeenCalledWith(mockTokens);

View File

@ -26,6 +26,7 @@ import {
clearCachedGoogleAccount,
} from '../utils/user_account.js';
import { AuthType } from '../core/contentGenerator.js';
import { shouldAttemptBrowserLaunch } from '../utils/browser.js';
import readline from 'node:readline';
// OAuth Client ID used to initiate OAuth2Client class.
@ -121,7 +122,7 @@ export async function getOauthClient(
}
}
if (config.getNoBrowser()) {
if (config.getNoBrowser() || !shouldAttemptBrowserLaunch()) {
let success = false;
const maxRetries = 2;
for (let i = 0; !success && i < maxRetries; i++) {
@ -156,15 +157,17 @@ export async function getOauthClient(
// causing the entire Node.js process to crash.
childProcess.on('error', (_) => {
console.error(
'Failed to open browser automatically. Please open the URL manually:',
'Failed to open browser automatically. Please try running again with NO_BROWSER=true set.',
);
console.error(webLogin.authUrl);
process.exit(1);
});
} catch (err) {
console.error(
'An unexpected error occurred while trying to open the browser:',
err,
'\nPlease try running again with NO_BROWSER=true set.',
);
process.exit(1);
}
console.log('Waiting for authentication...');
@ -175,7 +178,7 @@ export async function getOauthClient(
}
async function authWithUserCode(client: OAuth2Client): Promise<boolean> {
const redirectUri = 'https://sdk.cloud.google.com/authcode_cloudcode.html';
const redirectUri = 'https://codeassist.google.com/authcode';
const codeVerifier = await client.generateCodeVerifierAsync();
const state = crypto.randomBytes(32).toString('hex');
const authUrl: string = client.generateAuthUrl({

View File

@ -80,3 +80,4 @@ export { OAuthUtils } from './mcp/oauth-utils.js';
// Export telemetry functions
export * from './telemetry/index.js';
export { sessionId } from './utils/session.js';
export * from './utils/browser.js';

View File

@ -0,0 +1,53 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Determines if we should attempt to launch a browser for authentication
* based on the user's environment.
*
* This is an adaptation of the logic from the Google Cloud SDK.
* @returns True if the tool should attempt to launch a browser.
*/
export function shouldAttemptBrowserLaunch(): boolean {
// A list of browser names that indicate we should not attempt to open a
// web browser for the user.
const browserBlocklist = ['www-browser'];
const browserEnv = process.env.BROWSER;
if (browserEnv && browserBlocklist.includes(browserEnv)) {
return false;
}
// Common environment variables used in CI/CD or other non-interactive shells.
if (process.env.CI || process.env.DEBIAN_FRONTEND === 'noninteractive') {
return false;
}
// The presence of SSH_CONNECTION indicates a remote session.
// We should not attempt to launch a browser unless a display is explicitly available
// (checked below for Linux).
const isSSH = !!process.env.SSH_CONNECTION;
// On Linux, the presence of a display server is a strong indicator of a GUI.
if (process.platform === 'linux') {
// These are environment variables that can indicate a running compositor on
// Linux.
const displayVariables = ['DISPLAY', 'WAYLAND_DISPLAY', 'MIR_SOCKET'];
const hasDisplay = displayVariables.some((v) => !!process.env[v]);
if (!hasDisplay) {
return false;
}
}
// If in an SSH session on a non-Linux OS (e.g., macOS), don't launch browser.
// The Linux case is handled above (it's allowed if DISPLAY is set).
if (isSSH && process.platform !== 'linux') {
return false;
}
// For non-Linux OSes, we generally assume a GUI is available
// unless other signals (like SSH) suggest otherwise.
// The `open` command's error handling will catch final edge cases.
return true;
}