{"record":{"id":"773124533acfdf99","repo":"google-gemini/gemini-cli","slug":"no-access-token-received-from-token-endpoint","errorCode":null,"errorMessage":"No access token received from token endpoint","messagePattern":"No access token received from token endpoint","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/core/src/agents/auth-provider/oauth2-provider.ts","lineNumber":290,"sourceCode":"        getErrorMessage(error),\n      );\n    }\n\n    const { code } = await callbackServer.response;\n    debugLogger.debug(\n      '✓ Authorization code received, exchanging for tokens...',\n    );\n\n    const tokenResponse = await exchangeCodeForToken(\n      flowConfig,\n      code,\n      pkceParams.codeVerifier,\n      redirectPort,\n      /* resource= */ undefined,\n    );\n\n    if (!tokenResponse.access_token) {\n      throw new Error('No access token received from token endpoint');\n    }\n\n    const token = this.toOAuthToken(tokenResponse);\n    this.cachedToken = token;\n    await this.persistToken();\n\n    debugLogger.debug('✓ OAuth2 authentication successful! Token saved.');\n    return token;\n  }\n\n  /**\n   * Convert an `OAuthTokenResponse` into the internal `OAuthToken` format.\n   */\n  private toOAuthToken(\n    response: {\n      access_token: string;\n      token_type?: string;\n      expires_in?: number;","sourceCodeStart":272,"sourceCodeEnd":308,"githubUrl":"https://github.com/google-gemini/gemini-cli/blob/5024443c7217464a66e98f80d73172a26440bd8f/packages/core/src/agents/auth-provider/oauth2-provider.ts#L272-L308","documentation":"Thrown by OAuth2AuthProvider.authenticateInteractively after exchangeCodeForToken() returned a response with no access_token field. The token endpoint was reached and did not error (no exception was raised by the exchange), but the response body lacked the expected access_token. This points at the Identity Provider returning a success-like or malformed payload rather than a transport failure.","triggerScenarios":"Calling headers() on an OAuth2AuthProvider with no valid cached token, no refresh token (or a refresh that failed), which forces authenticateInteractively() -> exchangeCodeForToken(). The IdP responds 200 with a body missing access_token (e.g. returns an error object like {error: 'invalid_grant'} with HTTP 200, or an opaque SAML/HTML page captured by the redirect). Also triggered when scopes requested do not match the client registration and the IdP returns a non-standard error body.","commonSituations":"Wrong/typo in client_id or client_secret where the IdP still returns 200 with an error JSON; requested scopes not allowed for the OAuth client; redirect URI/port mismatch causing a stale or replayed authorization code; IdP requires a resource parameter but the caller passes undefined (A2A hard-codes resource=undefined); clock skew or expired code_verifier (PKCE); corporate proxy returning an HTML block page with 200.","solutions":["Inspect the full token endpoint response body (add temporary logging around exchangeCodeForToken) to see exactly what the IdP returned.","Verify client_id and client_secret in the agent's auth config match a valid OAuth client registration.","Confirm the requested scopes are permitted for the client; trim scopes to the minimum the IdP allows.","Ensure the redirect URI port from getPortFromUrl(flowConfig.redirectUri) is not firewalled and is the exact URI registered with the IdP.","If the IdP requires a resource/audience parameter, this A2A flow does not send one (resource=undefined) — switch to an auth config / IdP variant that does not require it.","Retry the interactive flow from scratch: delete the persisted token via MCPOAuthTokenStorage (Storage.getA2AOAuthTokensPath()) to clear any stale state."],"exampleFix":"// before: scopes mismatch causes empty access_token\nconst auth = {\n  type: 'oauth2',\n  client_id: 'abc',\n  client_secret: 'secret',\n  scopes: ['openid', 'profile', 'https://unsupported.example/all']\n};\n\n// after: align scopes with the IdP client registration\nconst auth = {\n  type: 'oauth2',\n  client_id: 'abc',\n  client_secret: 'secret',\n  scopes: ['openid', 'email']\n};","handlingStrategy":"try-catch","validationCode":"// Before triggering the interactive flow, confirm the OAuth client config.\nfunction assertOAuthConfig(cfg) {\n  if (!cfg.client_id) throw new Error('client_id required');\n  if (!cfg.authorization_url || !cfg.token_url)\n    throw new Error('authorization_url and token_url required');\n}\n// Plus: pre-validate scopes against the IdP client registration if an introspection endpoint is available.","typeGuard":null,"tryCatchPattern":"try {\n  const headers = await provider.headers();\n} catch (e) {\n  if (e instanceof Error && /No access token received/.test(e.message)) {\n    // Inspect IdP response, fix scopes/redirect, then re-authenticate.\n    await provider.clearCredentials?.(); // if exposed\n  }\n  throw e;\n}","preventionTips":["Register exact redirect URI/port with the IdP before first use.","Keep scopes minimal and matching the client registration.","Clear persisted tokens when changing client_id or scopes."],"tags":["oauth2","auth","network","a2a"],"backgroundTag":null,"analyzedSha":"5024443c7217464a66e98f80d73172a26440bd8f","analyzedAt":"2026-08-12T06:01:53.711Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}