Support logging in with Application Default Credentials (#1157)
Co-authored-by: N. Taylor Mullen <ntaylormullen@google.com>
This commit is contained in:
parent
5b2cea8eda
commit
3453b977b8
18
README.md
18
README.md
|
@ -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:**
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = {},
|
||||||
) {}
|
) {}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue