From d9cea627bf2ed3a54a30cefcece267fec05b46a0 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 5 Apr 2026 01:05:08 -0400 Subject: [PATCH] fix: detect and block Gemini CLI OAuth tokens used as API keys (#3296) * fix: detect and block Gemini CLI OAuth tokens used as API keys Users who install Google's standalone Gemini CLI may inadvertently set GEMINI_API_KEY to an OAuth access token (ya29.*) instead of an AI Studio API key (AIza*). These tokens fail at the Google API with a confusing error. This adds early detection at three entry points: - AuthStorage.set(): throws when storing ya29.* as api_key for "google" - AuthStorage.getApiKey(): blocks ya29.* from runtime overrides (--api-key) - AuthStorage.getApiKey(): blocks ya29.* from environment variables Each path provides a clear error message explaining the issue and directing users to either get an API key from aistudio.google.com or use /login google-gemini-cli for OAuth-based access. Fixes #2157 Co-Authored-By: Claude Opus 4.6 (1M context) * chore: retrigger CI --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: trek-e --- .../src/core/auth-storage.test.ts | 53 +++++++++++++++ .../pi-coding-agent/src/core/auth-storage.ts | 67 ++++++++++++++++++- 2 files changed, 119 insertions(+), 1 deletion(-) diff --git a/packages/pi-coding-agent/src/core/auth-storage.test.ts b/packages/pi-coding-agent/src/core/auth-storage.test.ts index 7961edb73..a0d2cab20 100644 --- a/packages/pi-coding-agent/src/core/auth-storage.test.ts +++ b/packages/pi-coding-agent/src/core/auth-storage.test.ts @@ -356,6 +356,59 @@ describe("AuthStorage — oauth credential for non-OAuth provider (#2083)", () = }); }); +// ─── Gemini CLI OAuth token detection ───────────────────────────────────────── + +describe("AuthStorage — Gemini CLI OAuth token detection", () => { + it("rejects Google OAuth access token (ya29. prefix) stored as api_key for google provider", () => { + const storage = inMemory({}); + assert.throws( + () => storage.set("google", makeKey("ya29.a0ARrdaM_fake_oauth_token_from_gemini_cli")), + (err: Error) => { + assert.ok(err.message.includes("OAuth access token"), `Expected message about OAuth token, got: ${err.message}`); + assert.ok( + err.message.includes("GEMINI_API_KEY") || err.message.includes("google-gemini-cli"), + `Expected guidance about GEMINI_API_KEY or google-gemini-cli, got: ${err.message}`, + ); + return true; + }, + ); + }); + + it("rejects Google OAuth access token for google provider via getApiKey when set as env var", async () => { + const storage = inMemory({}); + // Simulate runtime override with OAuth token + storage.setRuntimeApiKey("google", "ya29.c.b0AXv0zTPQ_fake_oauth_token"); + const key = await storage.getApiKey("google"); + // Should return undefined (blocked) or throw + assert.equal(key, undefined, "OAuth token should be blocked for google provider"); + }); + + it("allows legitimate Google API keys (AIza prefix) for google provider", () => { + const storage = inMemory({}); + storage.set("google", makeKey("AIzaSyD_fake_legitimate_api_key_here")); + const creds = storage.getCredentialsForProvider("google"); + assert.equal(creds.length, 1); + }); + + it("allows ya29 tokens for google-gemini-cli provider (OAuth is expected there)", () => { + // google-gemini-cli stores OAuth credentials with type: "oauth", not "api_key" + // But if someone somehow stored an api_key, it shouldn't be blocked for OAuth providers + const storage = inMemory({}); + storage.set("google-gemini-cli", makeKey("ya29.a0ARrdaM_token_for_gemini_cli")); + const creds = storage.getCredentialsForProvider("google-gemini-cli"); + assert.equal(creds.length, 1); + }); + + it("rejects Google OAuth token (ya29. prefix) for openai provider that uses GEMINI_API_KEY indirectly", () => { + // Only google provider should be blocked, not others + const storage = inMemory({}); + // This should NOT throw - other providers can have whatever keys they want + storage.set("openai", makeKey("ya29.some_value")); + const creds = storage.getCredentialsForProvider("openai"); + assert.equal(creds.length, 1); + }); +}); + // ─── getAll truncation ──────────────────────────────────────────────────────── describe("AuthStorage — getAll()", () => { diff --git a/packages/pi-coding-agent/src/core/auth-storage.ts b/packages/pi-coding-agent/src/core/auth-storage.ts index 2791f326d..fb1532252 100644 --- a/packages/pi-coding-agent/src/core/auth-storage.ts +++ b/packages/pi-coding-agent/src/core/auth-storage.ts @@ -34,6 +34,46 @@ export type OAuthCredential = { export type AuthCredential = ApiKeyCredential | OAuthCredential; +// ============================================================================ +// Google OAuth token detection +// ============================================================================ + +/** + * Providers that use Google AI Studio API keys (not OAuth tokens). + * OAuth access tokens (ya29.*) are not valid API keys for these providers. + */ +const GOOGLE_API_KEY_PROVIDERS = new Set(["google"]); + +/** + * Detect if a string is a Google OAuth access token rather than an API key. + * Google OAuth access tokens start with "ya29." — these are issued by + * Google's OAuth2 token endpoint and are not valid as AI Studio API keys. + * + * Users who installed Google's Gemini CLI may have these tokens and + * mistakenly set them as GEMINI_API_KEY. + */ +export function isGoogleOAuthToken(key: string): boolean { + return key.startsWith("ya29."); +} + +/** + * Validate that an API key is not a Google OAuth token being used for + * a provider that requires actual API keys (e.g., Google AI Studio). + * Throws a descriptive error if the key appears to be an OAuth token. + */ +function validateNotGoogleOAuthToken(provider: string, key: string): void { + if (GOOGLE_API_KEY_PROVIDERS.has(provider) && isGoogleOAuthToken(key)) { + throw new Error( + `The provided key for "${provider}" appears to be a Google OAuth access token (ya29.*), ` + + `not a valid API key. Google AI Studio requires an API key starting with "AIza...". ` + + `\n\nIf you're using Google's Gemini CLI, its OAuth tokens are not compatible. ` + + `Either:\n` + + ` 1. Get an API key from https://aistudio.google.com/apikey and set GEMINI_API_KEY\n` + + ` 2. Use '/login google-gemini-cli' to authenticate via Cloud Code Assist`, + ); + } +} + /** * On-disk format: each provider maps to a single credential or an array of credentials. * Single credentials are normalized to arrays at load time for internal use. @@ -360,6 +400,9 @@ export class AuthStorage { */ set(provider: string, credential: AuthCredential): void { if (credential.type === "api_key") { + // Block Google OAuth tokens being stored as API keys for AI Studio providers + validateNotGoogleOAuthToken(provider, credential.key); + const existing = this.getCredentialsForProvider(provider); // Deduplicate: don't add if same key already exists const isDuplicate = existing.some( @@ -762,6 +805,16 @@ export class AuthStorage { // Runtime override takes highest priority const runtimeKey = this.runtimeOverrides.get(providerId); if (runtimeKey) { + // Block Google OAuth tokens used as runtime API key overrides + if (GOOGLE_API_KEY_PROVIDERS.has(providerId) && isGoogleOAuthToken(runtimeKey)) { + this.recordError( + new Error( + `Blocked Google OAuth access token (ya29.*) for provider "${providerId}". ` + + `Use an API key from https://aistudio.google.com/apikey or '/login google-gemini-cli'.`, + ), + ); + return undefined; + } return runtimeKey; } @@ -780,7 +833,19 @@ export class AuthStorage { // Fall back to environment variable const envKey = getEnvApiKey(providerId); - if (envKey) return envKey; + if (envKey) { + // Block Google OAuth tokens from environment variables (e.g., GEMINI_API_KEY=ya29.*) + if (GOOGLE_API_KEY_PROVIDERS.has(providerId) && isGoogleOAuthToken(envKey)) { + this.recordError( + new Error( + `GEMINI_API_KEY contains a Google OAuth access token (ya29.*), not an API key. ` + + `Get an API key from https://aistudio.google.com/apikey or use '/login google-gemini-cli'.`, + ), + ); + return undefined; + } + return envKey; + } // Fall back to custom resolver (e.g., models.json custom providers) return this.fallbackResolver?.(providerId) ?? undefined;