Support logging in with Application Default Credentials (#1157)

Co-authored-by: N. Taylor Mullen <ntaylormullen@google.com>
This commit is contained in:
Tommaso Sciortino 2025-06-18 09:49:13 -07:00 committed by GitHub
parent 5b2cea8eda
commit 3453b977b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 50 additions and 42 deletions

View File

@ -33,19 +33,29 @@ The Gemini CLI requires you to authenticate with Google's AI services. You'll ne
1. **Gemini Code Assist:** 1. **Gemini Code Assist:**
- To enable this mode you only need set the GEMINI_CODE_ASSIST environment variable to true. - To enable this mode you only need set the GEMINI_CODE_ASSIST environment variable to true.
- Enterprise users must also provide a GOOGLE_CLOUD_PROJECT environment variable specifying their Google Cloud project.
In the following methods, replace `GOOGLE_CLOUD_PROJECT` with the relevant values for your project:
- You can temporarily set the environment variable in your current shell session using the following command: - You can temporarily set the environment variable in your current shell session using the following command:
```bash ```bash
export GEMINI_CODE_ASSIST="true" export GEMINI_CODE_ASSIST="true"
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID" // Enterprise users only.
``` ```
- For repeated use, you can add the environment variable to your `.env` file (located in the project directory or user home directory) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following command adds the environment variable to a `~/.bashrc` file: - For repeated use, you can add the environment variable to your `.env` file (located in the project directory or user home directory) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following command adds the environment variable to a `~/.bashrc` file:
```bash ```bash
echo 'export GEMINI_CODE_ASSIST="true"' >> ~/.bashrc echo 'export GEMINI_CODE_ASSIST="true"' >> ~/.bashrc
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc # Enterprise users only.
source ~/.bashrc source ~/.bashrc
``` ```
- There are two types of Google Accounts you can use with Gemini CLI:
- **Personal Google Account**: This is the standard, free account you use for services like Gmail, Google Photos, and Google Drive for personal use (e.g. your-name@gmail.com).
- **Google Workspace Account**: This is a paid service for businesses and organizations that provides a suite of productivity tools, including a custom email domain (e.g. your-name@your-company.com), enhanced security features, and administrative controls. These accounts are often managed by an employer or school.
- Google Workspace Account must configure a Google Cloud Project Id to use. You can temporarily set the environment variable in your current shell session using the following command:
```bash
export GOOGLE_CLOUD_PROJECT_ID="YOUR_PROJECT_ID"
```
- For repeated use, you can add the environment variable to your `.env` file (located in the project directory or user home directory) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following command adds the environment variable to a `~/.bashrc` file:
```bash
echo 'export GOOGLE_CLOUD_PROJECT_ID="YOUR_PROJECT_ID"' >> ~/.bashrc
source ~/.bashrc
```
- During start up, Gemini CLI will direct you to a webpage for authentication. Once authenticated, your credentials will be cached locally so the web login can be skipped on subsequent runs. Cached credentials last about 20 hours before expiring.
- Note that the the web login must be done in a browser that can communicate with the machine Gemini Cli is being run from. (Specifically, the browser will be redirected to a localhost url that Gemini CLI will be listening on).
2. **Gemini API key:** 2. **Gemini API key:**

View File

@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { GoogleAuth, AuthClient } from 'google-auth-library';
import { ContentGenerator } from '../core/contentGenerator.js'; import { ContentGenerator } from '../core/contentGenerator.js';
import { getOauthClient } from './oauth2.js'; import { getOauthClient } from './oauth2.js';
import { setupUser } from './setup.js'; import { setupUser } from './setup.js';
@ -12,10 +13,17 @@ import { CodeAssistServer, HttpOptions } from './server.js';
export async function createCodeAssistContentGenerator( export async function createCodeAssistContentGenerator(
httpOptions: HttpOptions, httpOptions: HttpOptions,
): Promise<ContentGenerator> { ): Promise<ContentGenerator> {
const oauth2Client = await getOauthClient(); const authClient = await getAuthClient();
const projectId = await setupUser( const projectId = await setupUser(authClient);
oauth2Client, return new CodeAssistServer(authClient, projectId, httpOptions);
process.env.GOOGLE_CLOUD_PROJECT, }
);
return new CodeAssistServer(oauth2Client, projectId, httpOptions); async function getAuthClient(): Promise<AuthClient> {
try {
// Try for Application Default Credentials.
return await new GoogleAuth().getClient();
} catch (_) {
// No Application Default Credentials so try Oauth.
return await getOauthClient();
}
} }

View File

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { OAuth2Client } from 'google-auth-library'; import { OAuth2Client, Credentials } from 'google-auth-library';
import * as http from 'http'; import * as http from 'http';
import url from 'url'; import url from 'url';
import crypto from 'crypto'; import crypto from 'crypto';
@ -42,23 +42,12 @@ const SIGN_IN_FAILURE_URL =
const GEMINI_DIR = '.gemini'; const GEMINI_DIR = '.gemini';
const CREDENTIAL_FILENAME = 'oauth_creds.json'; const CREDENTIAL_FILENAME = 'oauth_creds.json';
export async function clearCachedCredentials(): Promise<void> {
await fs.rm(getCachedCredentialPath());
}
export async function getOauthClient(): Promise<OAuth2Client> { export async function getOauthClient(): Promise<OAuth2Client> {
try { try {
return await getCachedCredentialClient(); return await getCachedCredentialClient();
} catch (_) { } catch (_) {
const loggedInClient = await webLoginClient(); const loggedInClient = await webLoginClient();
await setCachedCredentials(loggedInClient.credentials);
await fs.mkdir(path.dirname(getCachedCredentialPath()), {
recursive: true,
});
await fs.writeFile(
getCachedCredentialPath(),
JSON.stringify(loggedInClient.credentials, null, 2),
);
return loggedInClient; return loggedInClient;
} }
} }
@ -149,7 +138,6 @@ function getAvailablePort(): Promise<number> {
async function getCachedCredentialClient(): Promise<OAuth2Client> { async function getCachedCredentialClient(): Promise<OAuth2Client> {
try { try {
const creds = await fs.readFile(getCachedCredentialPath(), 'utf-8'); const creds = await fs.readFile(getCachedCredentialPath(), 'utf-8');
const oAuth2Client = new OAuth2Client({ const oAuth2Client = new OAuth2Client({
clientId: OAUTH_CLIENT_ID, clientId: OAUTH_CLIENT_ID,
clientSecret: OAUTH_CLIENT_SECRET, clientSecret: OAUTH_CLIENT_SECRET,
@ -165,6 +153,14 @@ async function getCachedCredentialClient(): Promise<OAuth2Client> {
} }
} }
async function setCachedCredentials(credentials: Credentials) {
const filePath = getCachedCredentialPath();
await fs.mkdir(path.dirname(filePath), { recursive: true });
const credString = JSON.stringify(credentials, null, 2);
await fs.writeFile(filePath, credString);
}
function getCachedCredentialPath(): string { function getCachedCredentialPath(): string {
return path.join(os.homedir(), GEMINI_DIR, CREDENTIAL_FILENAME); return path.join(os.homedir(), GEMINI_DIR, CREDENTIAL_FILENAME);
} }

View File

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { OAuth2Client } from 'google-auth-library'; import { AuthClient } from 'google-auth-library';
import { import {
LoadCodeAssistResponse, LoadCodeAssistResponse,
LoadCodeAssistRequest, LoadCodeAssistRequest,
@ -42,7 +42,7 @@ export const CODE_ASSIST_API_VERSION = 'v1internal';
export class CodeAssistServer implements ContentGenerator { export class CodeAssistServer implements ContentGenerator {
constructor( constructor(
readonly auth: OAuth2Client, readonly auth: AuthClient,
readonly projectId?: string, readonly projectId?: string,
readonly httpOptions: HttpOptions = {}, readonly httpOptions: HttpOptions = {},
) {} ) {}

View File

@ -6,31 +6,27 @@
import { ClientMetadata, OnboardUserRequest } from './types.js'; import { ClientMetadata, OnboardUserRequest } from './types.js';
import { CodeAssistServer } from './server.js'; import { CodeAssistServer } from './server.js';
import { OAuth2Client } from 'google-auth-library'; import { AuthClient } from 'google-auth-library';
import { clearCachedCredentials } from './oauth2.js';
/** /**
* *
* @param projectId the user's project id, if any * @param projectId the user's project id, if any
* @returns the user's actual project id * @returns the user's actual project id
*/ */
export async function setupUser( export async function setupUser(authClient: AuthClient): Promise<string> {
oAuth2Client: OAuth2Client, const projectId = process.env.GOOGLE_CLOUD_PROJECT;
projectId?: string, const caServer = new CodeAssistServer(authClient, projectId);
): Promise<string> {
const caServer = new CodeAssistServer(oAuth2Client, projectId);
const clientMetadata: ClientMetadata = { const clientMetadata: ClientMetadata = {
ideType: 'IDE_UNSPECIFIED', ideType: 'IDE_UNSPECIFIED',
platform: 'PLATFORM_UNSPECIFIED', platform: 'PLATFORM_UNSPECIFIED',
pluginType: 'GEMINI', pluginType: 'GEMINI',
duetProject: projectId,
}; };
if (process.env.GOOGLE_CLOUD_PROJECT) {
clientMetadata.duetProject = process.env.GOOGLE_CLOUD_PROJECT;
}
// TODO: Support Free Tier user without projectId. // TODO: Support Free Tier user without projectId.
const loadRes = await caServer.loadCodeAssist({ const loadRes = await caServer.loadCodeAssist({
cloudaicompanionProject: process.env.GOOGLE_CLOUD_PROJECT, cloudaicompanionProject: projectId,
metadata: clientMetadata, metadata: clientMetadata,
}); });
@ -39,7 +35,7 @@ export async function setupUser(
const onboardReq: OnboardUserRequest = { const onboardReq: OnboardUserRequest = {
tierId: onboardTier, tierId: onboardTier,
cloudaicompanionProject: loadRes.cloudaicompanionProject || '', cloudaicompanionProject: loadRes.cloudaicompanionProject || projectId || '',
metadata: clientMetadata, metadata: clientMetadata,
}; };
try { try {
@ -49,14 +45,12 @@ export async function setupUser(
await new Promise((f) => setTimeout(f, 5000)); await new Promise((f) => setTimeout(f, 5000));
lroRes = await caServer.onboardUser(onboardReq); lroRes = await caServer.onboardUser(onboardReq);
} }
return lroRes.response?.cloudaicompanionProject?.id || ''; return lroRes.response?.cloudaicompanionProject?.id || '';
} catch (e) { } catch (e) {
await clearCachedCredentials();
console.log( console.log(
'\n\nError onboarding with Code Assist.\n' + '\n\nError onboarding with Code Assist.\n' +
'Enterprise users must specify GOOGLE_CLOUD_PROJECT ' + 'Google Workspace Account (e.g. your-name@your-company.com)' +
'in their environment variables or .env file.\n\n', ' must specify a GOOGLE_CLOUD_PROJECT environment variable.\n\n',
); );
throw e; throw e;
} }