|
| 1 | +# Case Study: Issue #100 - Google OAuth Scope Mismatch |
| 2 | + |
| 3 | +## Issue Reference |
| 4 | + |
| 5 | +- **Issue URL**: https://github.com/link-assistant/agent/issues/100 |
| 6 | +- **Title**: We should try all available auth credentials we have for Google |
| 7 | +- **Labels**: bug |
| 8 | +- **Date Reported**: December 23, 2025 |
| 9 | + |
| 10 | +## Timeline of Events |
| 11 | + |
| 12 | +1. **User installs agent v0.6.3**: `bun install -g @link-assistant/agent@latest` |
| 13 | +2. **User authenticates with Google OAuth**: Uses "Google AI Pro/Ultra (OAuth - Manual Code Entry)" method |
| 14 | +3. **Login appears successful**: Terminal shows "Login successful" |
| 15 | +4. **API request fails**: When using `echo "hi" | agent --model google/gemini-3-pro`, the request fails with error code 403 |
| 16 | + |
| 17 | +## Error Analysis |
| 18 | + |
| 19 | +### Error Response Details |
| 20 | + |
| 21 | +```json |
| 22 | +{ |
| 23 | + "error": { |
| 24 | + "code": 403, |
| 25 | + "message": "Request had insufficient authentication scopes.", |
| 26 | + "status": "PERMISSION_DENIED", |
| 27 | + "details": [ |
| 28 | + { |
| 29 | + "@type": "type.googleapis.com/google.rpc.ErrorInfo", |
| 30 | + "reason": "ACCESS_TOKEN_SCOPE_INSUFFICIENT", |
| 31 | + "domain": "googleapis.com", |
| 32 | + "metadata": { |
| 33 | + "method": "google.ai.generativelanguage.v1beta.GenerativeService.StreamGenerateContent", |
| 34 | + "service": "generativelanguage.googleapis.com" |
| 35 | + } |
| 36 | + } |
| 37 | + ] |
| 38 | + } |
| 39 | +} |
| 40 | +``` |
| 41 | + |
| 42 | +### WWW-Authenticate Header |
| 43 | + |
| 44 | +``` |
| 45 | +Bearer realm="https://accounts.google.com/", |
| 46 | +error="insufficient_scope", |
| 47 | +scope="https://www.googleapis.com/auth/generative-language |
| 48 | + https://www.googleapis.com/auth/generative-language.tuning |
| 49 | + https://www.googleapis.com/auth/generative-language.tuning.readonly |
| 50 | + https://www.googleapis.com/auth/generative-language.retriever |
| 51 | + https://www.googleapis.com/auth/generative-language.retriever.readonly" |
| 52 | +``` |
| 53 | + |
| 54 | +## Root Cause Analysis |
| 55 | + |
| 56 | +### Current Implementation (src/auth/plugins.ts) |
| 57 | + |
| 58 | +The Google OAuth plugin uses these scopes: |
| 59 | + |
| 60 | +```typescript |
| 61 | +const GOOGLE_OAUTH_SCOPES = [ |
| 62 | + 'https://www.googleapis.com/auth/cloud-platform', |
| 63 | + 'https://www.googleapis.com/auth/userinfo.email', |
| 64 | + 'https://www.googleapis.com/auth/userinfo.profile', |
| 65 | +]; |
| 66 | +``` |
| 67 | + |
| 68 | +### What Our API Requires |
| 69 | + |
| 70 | +We make API calls to `generativelanguage.googleapis.com` which requires specific scopes: |
| 71 | + |
| 72 | +- `https://www.googleapis.com/auth/generative-language` |
| 73 | +- `https://www.googleapis.com/auth/generative-language.tuning` |
| 74 | +- `https://www.googleapis.com/auth/generative-language.retriever` |
| 75 | + |
| 76 | +### The Real Difference: API Endpoint |
| 77 | + |
| 78 | +**Our Implementation**: |
| 79 | + |
| 80 | +- Uses `generativelanguage.googleapis.com` (standard Generative Language API) |
| 81 | +- This API requires `generative-language.*` scopes |
| 82 | +- OAuth client doesn't have these scopes registered → **SCOPE MISMATCH** |
| 83 | + |
| 84 | +**Gemini CLI Implementation**: |
| 85 | + |
| 86 | +- Uses `https://cloudcode-pa.googleapis.com/v1internal` (Cloud Code API) |
| 87 | +- This is a **different API endpoint** that wraps the Generative Language API |
| 88 | +- The Cloud Code API accepts `cloud-platform` scope! |
| 89 | +- Same OAuth client, same scopes, but **different API that works with those scopes** |
| 90 | + |
| 91 | +### Gemini CLI Architecture (Key Discovery) |
| 92 | + |
| 93 | +``` |
| 94 | +User OAuth (cloud-platform scope) |
| 95 | + ↓ |
| 96 | +Gemini CLI → Cloud Code API (cloudcode-pa.googleapis.com/v1internal) |
| 97 | + ↓ |
| 98 | +Cloud Code Server → Generative Language API (internal) |
| 99 | +``` |
| 100 | + |
| 101 | +The Gemini CLI doesn't call `generativelanguage.googleapis.com` directly. It calls Google's **Cloud Code API** which: |
| 102 | + |
| 103 | +1. Accepts `cloud-platform` OAuth tokens |
| 104 | +2. Handles subscription validation (FREE tier, STANDARD tier) |
| 105 | +3. Proxies requests to the Generative Language API internally |
| 106 | + |
| 107 | +### Code Evidence from Gemini CLI |
| 108 | + |
| 109 | +From `/tmp/gemini-cli/packages/core/src/code_assist/server.ts`: |
| 110 | + |
| 111 | +```typescript |
| 112 | +export const CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com'; |
| 113 | +export const CODE_ASSIST_API_VERSION = 'v1internal'; |
| 114 | + |
| 115 | +export class CodeAssistServer implements ContentGenerator { |
| 116 | + // Makes requests to cloudcode-pa.googleapis.com, not generativelanguage.googleapis.com |
| 117 | + async generateContentStream(req, userPromptId) { |
| 118 | + return this.requestStreamingPost('streamGenerateContent', ...); |
| 119 | + } |
| 120 | + |
| 121 | + getMethodUrl(method: string): string { |
| 122 | + return `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:${method}`; |
| 123 | + } |
| 124 | +} |
| 125 | +``` |
| 126 | + |
| 127 | +## Key Findings |
| 128 | + |
| 129 | +### Authentication Methods for Google AI |
| 130 | + |
| 131 | +| Method | Required Scope | Works? | |
| 132 | +| --------------------------- | ----------------------------------- | ------------ | |
| 133 | +| API Key | None (uses `x-goog-api-key` header) | Yes | |
| 134 | +| OAuth (cloud-platform) | `cloud-platform` | Inconsistent | |
| 135 | +| OAuth (generative-language) | `generative-language.*` | Should work | |
| 136 | + |
| 137 | +### The Issue Title's Insight |
| 138 | + |
| 139 | +The issue title "We should try all available auth credentials we have for Google" suggests that: |
| 140 | + |
| 141 | +1. Users may have both OAuth tokens and API keys stored |
| 142 | +2. When OAuth fails due to scope issues, the system should fall back to API key authentication |
| 143 | +3. Currently, the system does not attempt alternative credentials |
| 144 | + |
| 145 | +## Current Auth Storage |
| 146 | + |
| 147 | +From `src/auth/index.ts`: |
| 148 | + |
| 149 | +```typescript |
| 150 | +export namespace Auth { |
| 151 | + export const Oauth = z.object({ |
| 152 | + type: z.literal('oauth'), |
| 153 | + refresh: z.string(), |
| 154 | + access: z.string(), |
| 155 | + expires: z.number(), |
| 156 | + enterpriseUrl: z.string().optional(), |
| 157 | + }); |
| 158 | + |
| 159 | + export const Api = z.object({ |
| 160 | + type: z.literal('api'), |
| 161 | + key: z.string(), |
| 162 | + }); |
| 163 | + |
| 164 | + // Auth is stored per-provider in auth.json |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +Auth credentials are stored by provider ID, meaning: |
| 169 | + |
| 170 | +- `google` -> OAuth credentials (with limited scope) |
| 171 | +- API keys from environment variables (`GOOGLE_GENERATIVE_AI_API_KEY` or `GEMINI_API_KEY`) |
| 172 | + |
| 173 | +## Proposed Solutions |
| 174 | + |
| 175 | +### Solution 1: Use Cloud Code API (Recommended - Matches Gemini CLI) |
| 176 | + |
| 177 | +**The proper fix**: Use the same API endpoint as Gemini CLI. |
| 178 | + |
| 179 | +When Google OAuth is active, route API calls through: |
| 180 | + |
| 181 | +- `https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent` |
| 182 | + |
| 183 | +Instead of: |
| 184 | + |
| 185 | +- `https://generativelanguage.googleapis.com/v1beta/models/...:streamGenerateContent` |
| 186 | + |
| 187 | +**Pros**: |
| 188 | + |
| 189 | +- Exact same approach as official Gemini CLI |
| 190 | +- Works with existing `cloud-platform` scope |
| 191 | +- Supports subscription tiers (FREE, STANDARD, etc.) |
| 192 | +- No need for users to set API keys |
| 193 | + |
| 194 | +**Cons**: |
| 195 | + |
| 196 | +- Requires implementing Cloud Code API request/response translation |
| 197 | +- More complex implementation |
| 198 | + |
| 199 | +### Solution 2: Add Generative Language Scopes to OAuth |
| 200 | + |
| 201 | +Update `GOOGLE_OAUTH_SCOPES` in `src/auth/plugins.ts`: |
| 202 | + |
| 203 | +```typescript |
| 204 | +const GOOGLE_OAUTH_SCOPES = [ |
| 205 | + 'https://www.googleapis.com/auth/cloud-platform', |
| 206 | + 'https://www.googleapis.com/auth/userinfo.email', |
| 207 | + 'https://www.googleapis.com/auth/userinfo.profile', |
| 208 | + 'https://www.googleapis.com/auth/generative-language', |
| 209 | +]; |
| 210 | +``` |
| 211 | + |
| 212 | +**Pros**: Simple fix |
| 213 | +**Cons**: |
| 214 | + |
| 215 | +- Won't work - scopes aren't registered for the OAuth client (causes `403: restricted_client`) |
| 216 | +- See issue #93 |
| 217 | + |
| 218 | +### Solution 3: Credential Fallback Mechanism (Current Implementation) |
| 219 | + |
| 220 | +Implement a fallback strategy in `src/auth/plugins.ts`: |
| 221 | + |
| 222 | +1. Try OAuth credentials first |
| 223 | +2. If OAuth fails with scope error (403), try API key |
| 224 | +3. If API key fails, report the original error |
| 225 | + |
| 226 | +**Pros**: Works as workaround |
| 227 | +**Cons**: |
| 228 | + |
| 229 | +- Requires users to have an API key set |
| 230 | +- Doesn't leverage subscription benefits properly |
| 231 | +- Not the same experience as Gemini CLI |
| 232 | + |
| 233 | +## Implementation Recommendation |
| 234 | + |
| 235 | +**Proper Fix**: Solution 1 - Use Cloud Code API |
| 236 | + |
| 237 | +This requires implementing a `CodeAssistServer`-like client that: |
| 238 | + |
| 239 | +1. Calls `https://cloudcode-pa.googleapis.com/v1internal` endpoints |
| 240 | +2. Translates between our request format and Cloud Code API format |
| 241 | +3. Uses the `google-auth-library` OAuth client for Bearer token authentication |
| 242 | + |
| 243 | +The Gemini CLI has already implemented this in: |
| 244 | + |
| 245 | +- `/packages/core/src/code_assist/server.ts` - API client |
| 246 | +- `/packages/core/src/code_assist/converter.ts` - Request/response translation |
| 247 | + |
| 248 | +**Temporary Fix**: Solution 3 - Credential Fallback (Already Implemented) |
| 249 | + |
| 250 | +Until Solution 1 is implemented, users can set an API key as a fallback. |
| 251 | + |
| 252 | +## Implementation |
| 253 | + |
| 254 | +### Changes Made |
| 255 | + |
| 256 | +#### Key Insight: OAuth Client Scope Limitations |
| 257 | + |
| 258 | +The generative-language scopes cannot simply be added to the OAuth flow because: |
| 259 | + |
| 260 | +1. The Gemini CLI OAuth client (`681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j`) only has certain scopes registered |
| 261 | +2. Attempting to request unregistered scopes causes a `403: restricted_client` error (see issue #93) |
| 262 | +3. Therefore, the solution is NOT to add more scopes, but to fall back to API key authentication |
| 263 | + |
| 264 | +#### Implemented Solution: Fallback to API Key on OAuth Scope Errors (`src/auth/plugins.ts`) |
| 265 | + |
| 266 | +In the Google OAuth loader, added logic to: |
| 267 | + |
| 268 | +1. Detect OAuth scope errors (HTTP 403 with `insufficient_scope` in `www-authenticate` header) |
| 269 | +2. Fall back to API key authentication if available (`GOOGLE_GENERATIVE_AI_API_KEY` or `GEMINI_API_KEY`) |
| 270 | +3. Log helpful warnings and hints for users |
| 271 | + |
| 272 | +```typescript |
| 273 | +const isScopeError = (response: Response): boolean => { |
| 274 | + if (response.status !== 403) return false; |
| 275 | + const wwwAuth = response.headers.get('www-authenticate') || ''; |
| 276 | + return ( |
| 277 | + wwwAuth.includes('insufficient_scope') || |
| 278 | + wwwAuth.includes('ACCESS_TOKEN_SCOPE_INSUFFICIENT') |
| 279 | + ); |
| 280 | +}; |
| 281 | + |
| 282 | +// In the fetch handler: |
| 283 | +if (isScopeError(oauthResponse)) { |
| 284 | + const fallbackApiKey = getFallbackApiKey(); |
| 285 | + if (fallbackApiKey) { |
| 286 | + log.warn('oauth scope error, falling back to api key authentication'); |
| 287 | + // Use API key instead of OAuth |
| 288 | + } |
| 289 | +} |
| 290 | +``` |
| 291 | + |
| 292 | +### How to Test |
| 293 | + |
| 294 | +1. **Users with OAuth + API key**: Set `GOOGLE_GENERATIVE_AI_API_KEY` or `GEMINI_API_KEY` environment variable. When OAuth fails with scope error, it will automatically fall back to the API key. |
| 295 | + |
| 296 | +2. **Manual verification**: Check the logs for messages like: |
| 297 | + - `using google oauth credentials` - OAuth being used |
| 298 | + - `oauth scope error, falling back to api key authentication` - Fallback triggered |
| 299 | + - `oauth scope error and no api key fallback available` - No fallback available (user needs to set API key) |
| 300 | + |
| 301 | +## References |
| 302 | + |
| 303 | +- [Google OAuth Scopes Documentation](https://ai.google.dev/gemini-api/docs/oauth) |
| 304 | +- [Gemini CLI OAuth Implementation](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/oauth2.ts) |
| 305 | +- [Issue #51 - PaLM API 403 Insufficient Scopes](https://github.com/google/generative-ai-python/issues/51) |
| 306 | +- [Google Developer Forum - ACCESS_TOKEN_SCOPE_INSUFFICIENT](https://discuss.google.dev/t/googlegenerativeaierror-access-token-scope-insufficient/170693) |
0 commit comments