google-gemini/gemini-cli · error · Error

No access token received from token endpoint

Error message

No access token received from token endpoint

What it means

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.

Source

Thrown at packages/core/src/agents/auth-provider/oauth2-provider.ts:290

        getErrorMessage(error),
      );
    }

    const { code } = await callbackServer.response;
    debugLogger.debug(
      '✓ Authorization code received, exchanging for tokens...',
    );

    const tokenResponse = await exchangeCodeForToken(
      flowConfig,
      code,
      pkceParams.codeVerifier,
      redirectPort,
      /* resource= */ undefined,
    );

    if (!tokenResponse.access_token) {
      throw new Error('No access token received from token endpoint');
    }

    const token = this.toOAuthToken(tokenResponse);
    this.cachedToken = token;
    await this.persistToken();

    debugLogger.debug('✓ OAuth2 authentication successful! Token saved.');
    return token;
  }

  /**
   * Convert an `OAuthTokenResponse` into the internal `OAuthToken` format.
   */
  private toOAuthToken(
    response: {
      access_token: string;
      token_type?: string;
      expires_in?: number;

View on GitHub (pinned to 5024443c72)

Solutions

  1. Inspect the full token endpoint response body (add temporary logging around exchangeCodeForToken) to see exactly what the IdP returned.
  2. Verify client_id and client_secret in the agent's auth config match a valid OAuth client registration.
  3. Confirm the requested scopes are permitted for the client; trim scopes to the minimum the IdP allows.
  4. Ensure the redirect URI port from getPortFromUrl(flowConfig.redirectUri) is not firewalled and is the exact URI registered with the IdP.
  5. 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.
  6. Retry the interactive flow from scratch: delete the persisted token via MCPOAuthTokenStorage (Storage.getA2AOAuthTokensPath()) to clear any stale state.

Example fix

// before: scopes mismatch causes empty access_token
const auth = {
  type: 'oauth2',
  client_id: 'abc',
  client_secret: 'secret',
  scopes: ['openid', 'profile', 'https://unsupported.example/all']
};

// after: align scopes with the IdP client registration
const auth = {
  type: 'oauth2',
  client_id: 'abc',
  client_secret: 'secret',
  scopes: ['openid', 'email']
};
Defensive patterns

Strategy: try-catch

Validate before calling

// Before triggering the interactive flow, confirm the OAuth client config.
function assertOAuthConfig(cfg) {
  if (!cfg.client_id) throw new Error('client_id required');
  if (!cfg.authorization_url || !cfg.token_url)
    throw new Error('authorization_url and token_url required');
}
// Plus: pre-validate scopes against the IdP client registration if an introspection endpoint is available.

Try / catch

try {
  const headers = await provider.headers();
} catch (e) {
  if (e instanceof Error && /No access token received/.test(e.message)) {
    // Inspect IdP response, fix scopes/redirect, then re-authenticate.
    await provider.clearCredentials?.(); // if exposed
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of google-gemini/gemini-cli@5024443c72 (2026-08-12). Data as JSON: /api/errors/773124533acfdf99. Report an issue: GitHub.