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:
parent
290b8e9104
commit
d9cea627bf
2 changed files with 119 additions and 1 deletions
|
|
@ -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()", () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue