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) <noreply@anthropic.com>

* chore: retrigger CI

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: trek-e <trek-e@users.noreply.github.com>
This commit is contained in:
Tom Boucher 2026-04-05 01:05:08 -04:00 committed by GitHub
parent 290b8e9104
commit d9cea627bf
2 changed files with 119 additions and 1 deletions

View file

@ -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()", () => {

View file

@ -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;