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
- 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.
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
- 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.
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
- [A2AClientManager] sendMessageStream Error [${agentName}]: $
- A2AClient getTask Error [${agentName}]: ${error.message}
- A2AClient cancelTask Error [${agentName}]: ${error.message}
- OAuth2 authentication for agent "${this.agentName}" requires
- OAuth2 authentication for agent "${this.agentName}" requires
AI-assisted analysis of google-gemini/gemini-cli@5024443c72 (2026-08-12).
Data as JSON: /api/errors/773124533acfdf99.
Report an issue: GitHub.