diff --git a/README.md b/README.md index 9431615..cb5a49f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,76 @@ your terminal: gemini extensions install https://github.com/gemini-cli-extensions/workspace ``` +### Headless / Docker Authentication + +In environments without a browser (Docker, SSH, CI), you can provide OAuth +credentials via a JSON file using the `GEMINI_CLI_WORKSPACE_OAUTH_CREDENTIALS` +environment variable (similar to `GOOGLE_APPLICATION_CREDENTIALS`): + +```bash +GEMINI_CLI_WORKSPACE_OAUTH_CREDENTIALS=/path/to/credentials.json gemini +``` + +The simplest way to generate this file is with `gcloud`: + +#### 1. Create an OAuth Client ID + +Create a **Desktop** OAuth client in the +[Google Cloud Console → Credentials](https://console.cloud.google.com/apis/credentials): + +1. Click **Create Credentials** → **OAuth client ID** +2. Application type: **Desktop app** +3. Download the client secret JSON file (e.g. `client_secret.json`) + +#### 2. Enable Required APIs + +Enable the following APIs in your Google Cloud Project +([APIs & Services → Library](https://console.cloud.google.com/apis/library)): + +- [Google Calendar API](https://console.cloud.google.com/apis/api/calendar-json.googleapis.com) +- [Google Drive API](https://console.cloud.google.com/apis/api/drive.googleapis.com) +- [Google Docs API](https://console.cloud.google.com/apis/api/docs.googleapis.com) +- [Google Sheets API](https://console.cloud.google.com/apis/api/sheets.googleapis.com) +- [Google Slides API](https://console.cloud.google.com/apis/api/slides.googleapis.com) +- [Gmail API](https://console.cloud.google.com/apis/api/gmail.googleapis.com) +- [Google Chat API](https://console.cloud.google.com/apis/api/chat.googleapis.com) +- [People API](https://console.cloud.google.com/apis/api/people.googleapis.com) +- [Admin SDK API](https://console.cloud.google.com/apis/api/admin.googleapis.com) + +#### 3. Generate Credentials + +```bash +gcloud auth application-default login \ + --client-id-file=client_secret.json \ + --project=YOUR_PROJECT_ID \ + --scopes="\ +https://www.googleapis.com/auth/cloud-platform,\ +https://www.googleapis.com/auth/documents,\ +https://www.googleapis.com/auth/drive,\ +https://www.googleapis.com/auth/calendar,\ +https://www.googleapis.com/auth/chat.spaces,\ +https://www.googleapis.com/auth/chat.messages,\ +https://www.googleapis.com/auth/chat.memberships,\ +https://www.googleapis.com/auth/userinfo.profile,\ +https://www.googleapis.com/auth/gmail.modify,\ +https://www.googleapis.com/auth/directory.readonly,\ +https://www.googleapis.com/auth/presentations.readonly,\ +https://www.googleapis.com/auth/spreadsheets.readonly" +``` + +You can customize the list of scopes used above to fit your needs. See the +[Google OAuth 2.0 Scopes](https://developers.google.com/identity/protocols/oauth2/scopes) +page for a list of available scopes. + +#### 4. Use the Credentials + +```bash +GEMINI_CLI_WORKSPACE_OAUTH_CREDENTIALS=~/.config/gcloud/application_default_credentials.json gemini +``` + +> **Note:** Both gcloud ADC format (`type: "authorized_user"`) and raw OAuth +> token JSON are supported. + ## Usage Once the extension is installed, you can use it to interact with your Google diff --git a/workspace-server/src/auth/AuthManager.ts b/workspace-server/src/auth/AuthManager.ts index 1aa8257..eccb220 100644 --- a/workspace-server/src/auth/AuthManager.ts +++ b/workspace-server/src/auth/AuthManager.ts @@ -6,6 +6,7 @@ import { google, Auth } from 'googleapis'; import crypto from 'node:crypto'; +import * as fs from 'node:fs'; import * as http from 'node:http'; import * as net from 'node:net'; import * as url from 'node:url'; @@ -80,10 +81,79 @@ export class AuthManager { return false; } + /** + * Load an OAuth2 client from a credentials file specified by the + * GEMINI_CLI_WORKSPACE_OAUTH_CREDENTIALS environment variable. + * Supports gcloud ADC format (type: "authorized_user") and raw + * Auth.Credentials JSON. + */ + private async loadClientFromCredentialsFile( + credentialsFile: string, + ): Promise { + logToFile(`Loading credentials from file: ${credentialsFile}`); + let parsed: Record; + try { + const fileContents = await fs.promises.readFile( + credentialsFile, + 'utf-8', + ); + parsed = JSON.parse(fileContents); + } catch (e) { + throw new Error( + `Failed to read or parse credentials from ${credentialsFile}: ${(e as Error).message}`, + ); + } + + // Support gcloud ADC format (type: "authorized_user") + if (parsed.type === 'authorized_user') { + if ( + typeof parsed.client_id !== 'string' || + typeof parsed.client_secret !== 'string' || + typeof parsed.refresh_token !== 'string' + ) { + throw new Error( + `Malformed "authorized_user" credentials in ${credentialsFile}. ` + + `'client_id', 'client_secret', and 'refresh_token' must be strings.`, + ); + } + const oAuth2Client = new google.auth.OAuth2({ + clientId: parsed.client_id, + clientSecret: parsed.client_secret, + }); + oAuth2Client.setCredentials({ + refresh_token: parsed.refresh_token, + }); + return oAuth2Client; + } + + // Otherwise treat as raw Auth.Credentials JSON + const creds = parsed as Auth.Credentials; + if (!creds.access_token && !creds.refresh_token) { + throw new Error( + `Malformed credentials in ${credentialsFile}. ` + + `The file must contain at least 'access_token' or 'refresh_token'.`, + ); + } + const oAuth2Client = new google.auth.OAuth2({ clientId: CLIENT_ID }); + oAuth2Client.setCredentials(creds); + return oAuth2Client; + } public async getAuthenticatedClient(): Promise { logToFile('getAuthenticatedClient called'); + // If a credentials file is provided via env var, use it directly + // (similar to GOOGLE_APPLICATION_CREDENTIALS) + const credentialsFile = + process.env['GEMINI_CLI_WORKSPACE_OAUTH_CREDENTIALS']; + if (credentialsFile) { + if (!this.client) { + this.client = + await this.loadClientFromCredentialsFile(credentialsFile); + } + return this.client; + } + // Check if we have a cached client with valid credentials if ( this.client &&